diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 00000000..48f7937f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,100 @@ +name: Bug Report +description: Found a bug? Report it here! +title: "[BUG] " +labels: ["bug"] +assignees: + - Patricklumowa +body: + - type: markdown + attributes: + value: | + Thank you for taking the time to fill out this template! + **The more information you provide, the easier it will be to identify and fix the bug.** + If you want a guide on filing bug reports effectively, you can [find it here](https://www.chiark.greenend.org.uk/~sgtatham/bugs.html). + - type: checkboxes + id: checked-bugs + attributes: + label: Have you checked to see if this issue has already been reported? + options: + - label: This bug has not been previously reported. + validations: + required: true + - type: textarea + id: describe-bug + attributes: + label: Describe The Bug + description: "Explain what goes wrong when using the bot." + placeholder: "E.g. The bot places pixels on the wrong coordinates or skips areas." + validations: + required: true + - type: textarea + id: expected-behavior + attributes: + label: Expected Behavior + description: "Describe what you expected to happen or why you think the current behavior is wrong." + placeholder: | + E.g. The bot should place pixels exactly according to the template image. + E.g. The bot should respect rate limits and avoid bans. + validations: + required: false + - type: textarea + id: things-tried + attributes: + label: Things You Tried + description: "List all attempts you made to fix or work around the bug." + placeholder: "E.g. Restarted the bot, tried a different template, or updated the pixel map." + validations: + required: false + - type: textarea + id: reproduce-bug + attributes: + label: Reproduce The Bug + description: "Explain exactly how to trigger the bug." + placeholder: | + E.g. + 1. Launch the bot + 2. Load a template image + 3. Start pixel placement + 4. Observe that some pixels are placed incorrectly or skipped + validations: + required: false + - type: textarea + id: error-message + attributes: + label: Error Message + description: "If there are error messages, add them here." + placeholder: "E.g. Error: Coordinates out of range!" + validations: + required: false + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: "Include screenshots or links to illustrate the problem." + placeholder: "E.g. Imgur link showing the pixel placement error" + validations: + required: false + - type: textarea + id: system-information + attributes: + label: System Information + description: "Provide details about your OS, bot version, and browser. Please fill this out!" + placeholder: | + OS: (E.g. Windows) + wplace-autoBOT Version: 2.0 + Browser: Mozilla 142.0 + You found an easter egg :3 + value: | + OS: + wplace-autoBOT Version: + Browser: + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional Information + description: "Add any other context about the problem here." + placeholder: "E.g. This issue started after the last update or happens only with large templates." + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..c92e6333 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Community Support & Questions (Discord) + url: https://discord.gg/knkNRYyQcm + about: Join the Discord if you have questions or want to discuss AutoBOT with the community. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 00000000..8d442ebf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,48 @@ +name: Feature Request +description: Have an idea you want added? Suggest it here! +title: "[FEAT] " +labels: ["enhancement"] +assignees: + - Patricklumowa +body: + - type: textarea + id: related-problem + attributes: + label: Is your feature request related to a problem? Please describe. + description: "Describe what the problem is." + placeholder: | + E.g. It's tedious to manually place pixels to recreate complex designs. + E.g. The bot currently can't detect areas that need fixing automatically. + validations: + required: false + - type: textarea + id: feature-solution + attributes: + label: Describe the solution you'd like + description: "Describe what you want to happen." + placeholder: | + E.g. Add an automatic pixel placement feature that matches a template image. + E.g. Implement prioritization of important areas first, like outlines or text. + E.g. Allow configuration of rate limits to avoid getting banned. + validations: + required: true + - type: textarea + id: feature-alternatives + attributes: + label: Describe alternatives you've considered + description: "Describe any alternative solutions or features you've considered." + placeholder: | + E.g. Manually placing pixels myself. + E.g. Using a different bot that doesn't support template images. + E.g. Semi-automatic scripts that still require manual correction. + validations: + required: false + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. If your feature is based on "something," please add any links relative to that "something" here. + placeholder: | + E.g. A sample template image the bot should replicate. + E.g. A video of the bot in action or a similar tool online. + E.g. Notes on color palette, priority zones, or timing restrictions. \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 00000000..5b2ef769 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,28 @@ +# Pull Request +Fill out the following details to submit your PR. + +## Summary +Please briefly describe the changes in your PR. +E.g. Fixes display bug with templates. +E.g. Adds a template tab that users can manage all templates through. + +## Related Issue(s) +Link to the related issues your PR would solve here. +E.g. Fixes #14 +E.g. Adds #4 + +## Changes +Select the type of change your PR is: +- [ ] Feature +- [ ] Bug fix +- [ ] Documentation +- [ ] Refactoring +- [ ] Build +- [ ] Other + +## Checklist +- [ ] This PR follows the project's style of coding and documentation. +- [ ] Auto-Bot has been verified to work correctly for this PR. + +## Additional Notes +Anything else reviewers should know? \ No newline at end of file diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 00000000..8d1c7f47 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,187 @@ +name: Pages – aggregate all branches + +on: + push: + branches: ["**"] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + aggregate_deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deploy.outputs.page_url }} + + steps: + - name: Checkout (shallow) + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Configure Pages + uses: actions/configure-pages@v5 + + - name: Aggregate and build recursive directory pages + shell: bash + run: | + set -euo pipefail + + # jq for JSON escaping (present on runners, but ensure) + if ! command -v jq >/dev/null 2>&1; then + sudo apt-get update -y + sudo apt-get install -y jq + fi + + REPO="${GITHUB_REPOSITORY#*/}" + SITE_ROOT="/${REPO}/" + + mkdir -p public + : > public/.nojekyll + + # Tiny favicon assets to avoid 404s + printf "%s" \ + "🚀" \ + > public/favicon.svg + + # List remote branches + mapfile -t BRANCHES < <( + git ls-remote --heads origin | awk '{print $2}' \ + | sed 's@refs/heads/@@' + ) + + # Landing page (no globals except window.__branches) + cat > public/index.html <<'HTML' + + + + + Branches + +

Branches

+ +
+ + HTML + + # Emit index.html for a directory + gen_index_dir() { + local DIR="$1" ROOT="$2" BR="$3" + local rel="${DIR#$ROOT}" + local title="Branch: ${BR}${rel:-/}" + + { + echo "" + echo "" + echo "" + echo "${title}" + echo "" + echo "

${title}

" + if [[ "$DIR" != "$ROOT" ]]; then + echo "

⬆ UpAll branches

" + else + echo "

All branches

" + fi + echo "" + + # Subdirectories + while IFS= read -r d; do + [[ -n "$d" ]] || continue + printf "\n" "$d" "$d" + done < <(find "$DIR" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort) + + # Files + while IFS= read -r f; do + [[ -n "$f" ]] || continue + size=$(stat -c %s "$DIR/$f" 2>/dev/null || echo 0) + size_h=$(numfmt --to=iec --suffix=B "$size" 2>/dev/null || echo "$size") + mod=$(date -r "$DIR/$f" "+%Y-%m-%d %H:%M" 2>/dev/null || echo "") + printf "\n" "$f" "$f" "$size_h" "$mod" + done < <(find "$DIR" -mindepth 1 -maxdepth 1 -type f ! -name 'index.html' -printf '%f\n' | sort) + + echo "
NameSizeModified
📁 %s/
📄 %s%s%s
" + echo "

Generated $(date -u '+%Y-%m-%d %H:%M UTC')

" + } > "$DIR/index.html" + } + + # Build per-branch trees and indexes + for BR in "${BRANCHES[@]}"; do + SAFE="$(echo "$BR" | tr '/:@ ' '----' | sed 's/[^A-Za-z0-9._-]/-/g')" + mkdir -p "public/$SAFE" + + git fetch --depth=1 origin "$BR" + git archive --format=tar FETCH_HEAD | tar -x -C "public/$SAFE" + + # Keep only static assets; prune empty dirs + rm -rf "public/$SAFE/.git" "public/$SAFE/.github" \ + "public/$SAFE/node_modules" + find "public/$SAFE" -type f ! \ + \( -name '*.html' -o -name '*.js' -o -name '*.css' -o \ + -name '*.json' -o -name '*.png' -o -name '*.jpg' -o \ + -name '*.jpeg' -o -name '*.gif' -o -name '*.svg' \) \ + -delete + find "public/$SAFE" -type d -empty -delete + + ROOT_DIR="public/$SAFE" + while IFS= read -r -d '' d; do + gen_index_dir "$d" "$ROOT_DIR" "$BR" + done < <(find "$ROOT_DIR" -type d -print0) + + # Append to landing page (JSON-escaped) + SAFE_JSON=$(printf '%s' "$SAFE" | jq -Rr @json) + BR_JSON=$(printf '%s' "$BR" | jq -Rr @json) + echo "" \ + >> public/index.html + done + + # Finish landing page + cat >> public/index.html <<'HTML' + + HTML + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v4 + with: + path: public + + - name: Deploy to GitHub Pages + id: deploy + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..7ec2b313 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Node modules +node_modules/ + +# NPM/Yarn lock +package-lock.json +package.json + +# Prettier +.prettierrc.json + +# ESLint +eslint.config.cjs + +# Staged changes +staged_changes.diff + +# IDE (WebStorm/IntelliJ IDEA) +.idea/ diff --git a/AccountSwapper/background.js b/AccountSwapper/background.js new file mode 100644 index 00000000..500060e1 --- /dev/null +++ b/AccountSwapper/background.js @@ -0,0 +1,85 @@ +const cookieDomain = '.backend.wplace.live'; + +async function preserveAndResetJ() { + let savedValue = null; + + try { + const oldJ = await chrome.cookies.get({ + url: 'https://backend.wplace.live/', + name: 'j', + }); + + if (oldJ && oldJ.value) { + savedValue = oldJ.value; + console.log('[bg] Saved j cookie:', savedValue); + } else { + console.warn("[bg] No 'j' cookie found → will only nuke cf_clearance"); + } + + setTimeout(async () => { + await new Promise((resolve) => { + chrome.browsingData.remove({ origins: ['https://wplace.live'] }, { cookies: true }, () => { + console.log('[bg] Nuke done (delayed)'); + resolve(); + }); + }); + + if (savedValue) { + setCookie(savedValue); + } else { + console.log('[bg] No j to restore, leaving nuked'); + } + }, 2000); + } catch (err) { + console.error('[bg] Error in preserveAndResetJ:', err); + } +} + +chrome.webNavigation.onCompleted.addListener( + (details) => { + if (details.url.includes('wplace.live')) { + console.log('[bg] Page load detected → nuking cookies with double pass'); + preserveAndResetJ(); + } + }, + { url: [{ hostContains: 'wplace.live' }] } +); + +function setCookie(value) { + const cleaned = value.trim(); + console.log('[bg] setCookie CALLED with:', cleaned); + + chrome.cookies.set( + { + url: 'https://backend.wplace.live/', + name: 'j', + value: cleaned, + domain: cookieDomain, + path: '/', + }, + (cookie) => { + if (chrome.runtime.lastError) { + console.error('[bg] cookie set error:', chrome.runtime.lastError.message); + } else { + console.log('[bg] cookie set result:', cookie); + + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + if (tabs.length > 0) { + chrome.tabs.sendMessage(tabs[0].id, { + type: 'cookieSet', + value: cleaned, + }); + } + }); + } + } + ); +} + +chrome.runtime.onMessage.addListener(async (msg, sender, sendResponse) => { + if (msg.type === 'setCookie' && msg.value) { + await setCookie(msg.value); + sendResponse({ status: 'ok' }); + } + return true; +}); diff --git a/AccountSwapper/content.js b/AccountSwapper/content.js new file mode 100644 index 00000000..ec80bd65 --- /dev/null +++ b/AccountSwapper/content.js @@ -0,0 +1,32 @@ +window.addEventListener('message', (event) => { + if (event.source !== window) return; + if (event.data.source !== 'my-userscript') return; + + const message = event.data; + if (message.type === 'setCookie' && message.value) { + chrome.runtime.sendMessage( + { + type: 'setCookie', + value: message.value, + }, + (response) => { + if (response?.status === 'ok') { + console.log('✅ Forwarded token to background.'); + } + } + ); + } +}); + +chrome.runtime.onMessage.addListener((msg) => { + if (msg.type === 'cookieSet') { + window.postMessage( + { + source: 'my-extension', + type: 'cookieSet', + value: msg.value, + }, + '*' + ); + } +}); diff --git a/AccountSwapper/manifest.json b/AccountSwapper/manifest.json new file mode 100644 index 00000000..3de491ea --- /dev/null +++ b/AccountSwapper/manifest.json @@ -0,0 +1,27 @@ +{ + "manifest_version": 3, + "name": "WPlace Cookie Setter", + "version": "1.0.0", + "permissions": [ + "cookies", + "webRequest", + "scripting", + "tabs", + "storage", + "browsingData", + "webNavigation" + ], + "host_permissions": [ + "https://backend.wplace.live/*", + "https://wplace.live/*", + "https://*.wplace.live/*" + ], + "background": { "service_worker": "background.js" }, + "content_scripts": [ + { + "matches": ["https://wplace.live/*", "https://*.wplace.live/*"], + "js": ["content.js"], + "run_at": "document_idle" + } + ] +} diff --git a/Auto-Farm.js b/Auto-Farm.js index 1a903bd5..6199f25e 100644 --- a/Auto-Farm.js +++ b/Auto-Farm.js @@ -1,4 +1,5 @@ -(async () => { +// eslint-disable-next-line prettier/prettier +; (async () => { const CONFIG = { START_X: 742, START_Y: 1148, @@ -11,8 +12,8 @@ text: '#ffffff', highlight: '#775ce3', success: '#00ff00', - error: '#ff0000' - } + error: '#ff0000', + }, }; const state = { @@ -25,10 +26,10 @@ menuOpen: false, language: 'en', autoRefresh: true, - pausedForManual: false + pausedForManual: false, }; - const sleep = ms => new Promise(r => setTimeout(r, ms)); + const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const waitForSelector = async (selector, interval = 200, timeout = 5000) => { const start = Date.now(); while (Date.now() - start < timeout) { @@ -58,8 +59,7 @@ paintLoop(); } } - } catch (e) { - } + } catch (e) {} } return originalFetch(url, options); }; @@ -68,7 +68,7 @@ try { const res = await fetch(url, { credentials: 'include', - ...options + ...options, }); return await res.json(); } catch (e) { @@ -78,19 +78,23 @@ const getRandomPosition = () => ({ x: Math.floor(Math.random() * CONFIG.PIXELS_PER_LINE), - y: Math.floor(Math.random() * CONFIG.PIXELS_PER_LINE) + y: Math.floor(Math.random() * CONFIG.PIXELS_PER_LINE), }); const paintPixel = async (x, y) => { const randomColor = Math.floor(Math.random() * 31) + 1; const url = `https://backend.wplace.live/s0/pixel/${CONFIG.START_X}/${CONFIG.START_Y}`; - const payload = JSON.stringify({ coords: [x, y], colors: [randomColor], t: capturedCaptchaToken }); + const payload = JSON.stringify({ + coords: [x, y], + colors: [randomColor], + t: capturedCaptchaToken, + }); try { const res = await originalFetch(url, { method: 'POST', headers: { 'Content-Type': 'text/plain;charset=UTF-8' }, credentials: 'include', - body: payload + body: payload, }); if (res.status === 403) { console.error('❌ 403 Forbidden. CAPTCHA token might be invalid or expired.'); @@ -112,7 +116,7 @@ state.charges = { count: Math.floor(data.charges.count), max: Math.floor(data.charges.max), - cooldownMs: data.charges.cooldownMs + cooldownMs: data.charges.cooldownMs, }; if (state.userInfo.level) { state.userInfo.level = Math.floor(state.userInfo.level); @@ -140,9 +144,14 @@ const paintLoop = async () => { while (state.running) { const { count, cooldownMs } = state.charges; - + if (count < 1) { - updateUI(state.language === 'pt' ? `⌛ Sem cargas. Esperando ${Math.ceil(cooldownMs/1000)}s...` : `⌛ No charges. Waiting ${Math.ceil(cooldownMs/1000)}s...`, 'status'); + updateUI( + state.language === 'pt' + ? `⌛ Sem cargas. Esperando ${Math.ceil(cooldownMs / 1000)}s...` + : `⌛ No charges. Waiting ${Math.ceil(cooldownMs / 1000)}s...`, + 'status' + ); await sleep(cooldownMs); await getCharge(); continue; @@ -176,7 +185,9 @@ : '❌ CAPTCHA token expired. Waiting for Paint button...', 'error' ); - const mainPaintBtn = await waitForSelector('button.btn.btn-primary.btn-lg, button.btn-primary.sm\\:btn-xl'); + const mainPaintBtn = await waitForSelector( + 'button.btn.btn-primary.btn-lg, button.btn-primary.sm\\:btn-xl' + ); if (mainPaintBtn) mainPaintBtn.click(); await sleep(500); updateUI( @@ -196,10 +207,14 @@ const moveEvt = new MouseEvent('mousemove', { clientX: centerX, clientY: centerY, - bubbles: true + bubbles: true, }); canvas.dispatchEvent(moveEvt); - const keyDown = new KeyboardEvent('keydown', { key: ' ', code: 'Space', bubbles: true }); + const keyDown = new KeyboardEvent('keydown', { + key: ' ', + code: 'Space', + bubbles: true, + }); const keyUp = new KeyboardEvent('keyup', { key: ' ', code: 'Space', bubbles: true }); canvas.dispatchEvent(keyDown); canvas.dispatchEvent(keyUp); @@ -242,21 +257,21 @@ await sleep(1000); continue; } - + if (paintResult?.painted === 1) { state.paintedCount++; - state.lastPixel = { + state.lastPixel = { x: CONFIG.START_X + randomPos.x, y: CONFIG.START_Y + randomPos.y, - time: new Date() + time: new Date(), }; state.charges.count--; - + document.getElementById('paintEffect').style.animation = 'pulse 0.5s'; setTimeout(() => { document.getElementById('paintEffect').style.animation = ''; }, 500); - + updateUI(state.language === 'pt' ? '✅ Pixel pintado!' : '✅ Pixel painted!', 'success'); } else { updateUI(state.language === 'pt' ? '❌ Falha ao pintar' : '❌ Failed to paint', 'error'); @@ -283,7 +298,7 @@ 70% { box-shadow: 0 0 0 10px rgba(0, 255, 0, 0); } 100% { box-shadow: 0 0 0 0 rgba(0, 255, 0, 0); } } - @keyframes slideIn { + @keyframes slide-in { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } @@ -300,7 +315,7 @@ z-index: 9999; font-family: 'Segoe UI', Roboto, sans-serif; color: ${CONFIG.THEME.text}; - animation: slideIn 0.4s ease-out; + animation: slide-in 0.4s ease-out; overflow: hidden; } .wplace-header { @@ -417,25 +432,25 @@ const translations = { pt: { - title: "WPlace Auto-Farm", - start: "Iniciar", - stop: "Parar", - ready: "Pronto para começar", - user: "Usuário", - pixels: "Pixels", - charges: "Cargas", - level: "Level" + title: 'WPlace Auto-Farm', + start: 'Iniciar', + stop: 'Parar', + ready: 'Pronto para começar', + user: 'Usuário', + pixels: 'Pixels', + charges: 'Cargas', + level: 'Level', }, en: { - title: "WPlace Auto-Farm", - start: "Start", - stop: "Stop", - ready: "Ready to start", - user: "User", - pixels: "Pixels", - charges: "Charges", - level: "Level" - } + title: 'WPlace Auto-Farm', + start: 'Start', + stop: 'Stop', + ready: 'Ready to start', + user: 'User', + pixels: 'Pixels', + charges: 'Charges', + level: 'Level', + }, }; const t = translations[state.language] || translations.en; @@ -480,17 +495,20 @@ `; - + document.body.appendChild(panel); - + const header = panel.querySelector('.wplace-header'); - let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; - + let pos1 = 0, + pos2 = 0, + pos3 = 0, + pos4 = 0; + header.onmousedown = dragMouseDown; - + function dragMouseDown(e) { if (e.target.closest('.wplace-header-btn')) return; - + e = e || window.event; e.preventDefault(); pos3 = e.clientX; @@ -498,7 +516,7 @@ document.onmouseup = closeDragElement; document.onmousemove = elementDrag; } - + function elementDrag(e) { e = e || window.event; e.preventDefault(); @@ -506,35 +524,43 @@ pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; - panel.style.top = (panel.offsetTop - pos2) + "px"; - panel.style.left = (panel.offsetLeft - pos1) + "px"; + panel.style.top = panel.offsetTop - pos2 + 'px'; + panel.style.left = panel.offsetLeft - pos1 + 'px'; } - + function closeDragElement() { document.onmouseup = null; document.onmousemove = null; } - + const toggleBtn = panel.querySelector('#toggleBtn'); const minimizeBtn = panel.querySelector('#minimizeBtn'); const statusText = panel.querySelector('#statusText'); const content = panel.querySelector('.wplace-content'); const statsArea = panel.querySelector('#statsArea'); - + toggleBtn.addEventListener('click', () => { state.running = !state.running; - + if (state.running && !capturedCaptchaToken) { - updateUI(state.language === 'pt' ? '❌ Token não capturado. Clique em qualquer pixel primeiro.' : '❌ CAPTCHA token not captured. Please click any pixel manually first.', 'error'); + updateUI( + state.language === 'pt' + ? '❌ Token não capturado. Clique em qualquer pixel primeiro.' + : '❌ CAPTCHA token not captured. Please click any pixel manually first.', + 'error' + ); state.running = false; return; } - + if (state.running) { toggleBtn.innerHTML = ` ${t.stop}`; toggleBtn.classList.remove('wplace-btn-primary'); toggleBtn.classList.add('wplace-btn-stop'); - updateUI(state.language === 'pt' ? '🚀 Pintura iniciada!' : '🚀 Painting started!', 'success'); + updateUI( + state.language === 'pt' ? '🚀 Pintura iniciada!' : '🚀 Painting started!', + 'success' + ); paintLoop(); } else { toggleBtn.innerHTML = ` ${t.start}`; @@ -544,18 +570,18 @@ updateUI(state.language === 'pt' ? '⏹️ Parado' : '⏹️ Stopped', 'default'); } }); - + minimizeBtn.addEventListener('click', () => { state.minimized = !state.minimized; content.style.display = state.minimized ? 'none' : 'block'; minimizeBtn.innerHTML = ``; }); - + const autoRefreshCheckbox = panel.querySelector('#autoRefreshCheckbox'); autoRefreshCheckbox.addEventListener('change', () => { state.autoRefresh = autoRefreshCheckbox.checked; }); - + window.addEventListener('beforeunload', () => { state.menuOpen = false; }); @@ -568,7 +594,7 @@ statusText.className = `wplace-status status-${type}`; statusText.style.animation = 'none'; void statusText.offsetWidth; - statusText.style.animation = 'slideIn 0.3s ease-out'; + statusText.style.animation = 'slide-in 0.3s ease-out'; } }; @@ -578,22 +604,22 @@ if (statsArea) { const t = { pt: { - user: "Usuário", - pixels: "Pixels", - charges: "Cargas", - level: "Level" + user: 'Usuário', + pixels: 'Pixels', + charges: 'Cargas', + level: 'Level', }, en: { - user: "User", - pixels: "Pixels", - charges: "Charges", - level: "Level" - } + user: 'User', + pixels: 'Pixels', + charges: 'Charges', + level: 'Level', + }, }[state.language] || { - user: "User", - pixels: "Pixels", - charges: "Charges", - level: "Level" + user: 'User', + pixels: 'Pixels', + charges: 'Charges', + level: 'Level', }; statsArea.innerHTML = ` diff --git a/Auto-Image.js b/Auto-Image.js index 7c438d99..cbbc75df 100644 --- a/Auto-Image.js +++ b/Auto-Image.js @@ -1,205 +1,534 @@ -;(async () => { +// eslint-disable-next-line prettier/prettier +; (async () => { // CONFIGURATION CONSTANTS const CONFIG = { COOLDOWN_DEFAULT: 31000, TRANSPARENCY_THRESHOLD: 100, WHITE_THRESHOLD: 250, LOG_INTERVAL: 10, - THEMES: { - "Classic Autobot": { - primary: "#000000", - secondary: "#111111", - accent: "#222222", - text: "#ffffff", - highlight: "#775ce3", - success: "#00ff00", - error: "#ff0000", - warning: "#ffaa00", + PAINTING_SPEED: { + MIN: 1, // Minimum 1 pixel batch size + MAX: 1000, // Maximum 1000 pixels batch size + DEFAULT: 5, // Default 5 pixels batch size + }, + BATCH_MODE: 'normal', // "normal" or "random" - default to normal + RANDOM_BATCH_RANGE: { + MIN: 3, // Random range minimum + MAX: 20, // Random range maximum + }, + PAINTING_SPEED_ENABLED: true, // On by default + AUTO_CAPTCHA_ENABLED: true, // Turnstile generator enabled by default + TOKEN_SOURCE: 'generator', // "generator", "manual", or "hybrid" - default to generator + COOLDOWN_CHARGE_THRESHOLD: 1, // Default wait threshold + // Desktop Notifications (defaults) + NOTIFICATIONS: { + ENABLED: false, + ON_CHARGES_REACHED: true, + ONLY_WHEN_UNFOCUSED: true, + REPEAT_MINUTES: 5, // repeat reminder while threshold condition holds + }, + OVERLAY: { + OPACITY_DEFAULT: 0.2, + BLUE_MARBLE_DEFAULT: false, + ditheringEnabled: false, + }, // --- START: Color data from colour-converter.js --- + // New color structure with proper ID mapping + COLOR_MAP: { + 0: { id: 1, name: 'Black', rgb: { r: 0, g: 0, b: 0 } }, + 1: { id: 2, name: 'Dark Gray', rgb: { r: 60, g: 60, b: 60 } }, + 2: { id: 3, name: 'Gray', rgb: { r: 120, g: 120, b: 120 } }, + 3: { id: 4, name: 'Light Gray', rgb: { r: 210, g: 210, b: 210 } }, + 4: { id: 5, name: 'White', rgb: { r: 255, g: 255, b: 255 } }, + 5: { id: 6, name: 'Deep Red', rgb: { r: 96, g: 0, b: 24 } }, + 6: { id: 7, name: 'Red', rgb: { r: 237, g: 28, b: 36 } }, + 7: { id: 8, name: 'Orange', rgb: { r: 255, g: 127, b: 39 } }, + 8: { id: 9, name: 'Gold', rgb: { r: 246, g: 170, b: 9 } }, + 9: { id: 10, name: 'Yellow', rgb: { r: 249, g: 221, b: 59 } }, + 10: { id: 11, name: 'Light Yellow', rgb: { r: 255, g: 250, b: 188 } }, + 11: { id: 12, name: 'Dark Green', rgb: { r: 14, g: 185, b: 104 } }, + 12: { id: 13, name: 'Green', rgb: { r: 19, g: 230, b: 123 } }, + 13: { id: 14, name: 'Light Green', rgb: { r: 135, g: 255, b: 94 } }, + 14: { id: 15, name: 'Dark Teal', rgb: { r: 12, g: 129, b: 110 } }, + 15: { id: 16, name: 'Teal', rgb: { r: 16, g: 174, b: 166 } }, + 16: { id: 17, name: 'Light Teal', rgb: { r: 19, g: 225, b: 190 } }, + 17: { id: 20, name: 'Cyan', rgb: { r: 96, g: 247, b: 242 } }, + 18: { id: 44, name: 'Light Cyan', rgb: { r: 187, g: 250, b: 242 } }, + 19: { id: 18, name: 'Dark Blue', rgb: { r: 40, g: 80, b: 158 } }, + 20: { id: 19, name: 'Blue', rgb: { r: 64, g: 147, b: 228 } }, + 21: { id: 21, name: 'Indigo', rgb: { r: 107, g: 80, b: 246 } }, + 22: { id: 22, name: 'Light Indigo', rgb: { r: 153, g: 177, b: 251 } }, + 23: { id: 23, name: 'Dark Purple', rgb: { r: 120, g: 12, b: 153 } }, + 24: { id: 24, name: 'Purple', rgb: { r: 170, g: 56, b: 185 } }, + 25: { id: 25, name: 'Light Purple', rgb: { r: 224, g: 159, b: 249 } }, + 26: { id: 26, name: 'Dark Pink', rgb: { r: 203, g: 0, b: 122 } }, + 27: { id: 27, name: 'Pink', rgb: { r: 236, g: 31, b: 128 } }, + 28: { id: 28, name: 'Light Pink', rgb: { r: 243, g: 141, b: 169 } }, + 29: { id: 29, name: 'Dark Brown', rgb: { r: 104, g: 70, b: 52 } }, + 30: { id: 30, name: 'Brown', rgb: { r: 149, g: 104, b: 42 } }, + 31: { id: 31, name: 'Beige', rgb: { r: 248, g: 178, b: 119 } }, + 32: { id: 52, name: 'Light Beige', rgb: { r: 255, g: 197, b: 165 } }, + 33: { id: 32, name: 'Medium Gray', rgb: { r: 170, g: 170, b: 170 } }, + 34: { id: 33, name: 'Dark Red', rgb: { r: 165, g: 14, b: 30 } }, + 35: { id: 34, name: 'Light Red', rgb: { r: 250, g: 128, b: 114 } }, + 36: { id: 35, name: 'Dark Orange', rgb: { r: 228, g: 92, b: 26 } }, + 37: { id: 37, name: 'Dark Goldenrod', rgb: { r: 156, g: 132, b: 49 } }, + 38: { id: 38, name: 'Goldenrod', rgb: { r: 197, g: 173, b: 49 } }, + 39: { id: 39, name: 'Light Goldenrod', rgb: { r: 232, g: 212, b: 95 } }, + 40: { id: 40, name: 'Dark Olive', rgb: { r: 74, g: 107, b: 58 } }, + 41: { id: 41, name: 'Olive', rgb: { r: 90, g: 148, b: 74 } }, + 42: { id: 42, name: 'Light Olive', rgb: { r: 132, g: 197, b: 115 } }, + 43: { id: 43, name: 'Dark Cyan', rgb: { r: 15, g: 121, b: 159 } }, + 44: { id: 45, name: 'Light Blue', rgb: { r: 125, g: 199, b: 255 } }, + 45: { id: 46, name: 'Dark Indigo', rgb: { r: 77, g: 49, b: 184 } }, + 46: { id: 47, name: 'Dark Slate Blue', rgb: { r: 74, g: 66, b: 132 } }, + 47: { id: 48, name: 'Slate Blue', rgb: { r: 122, g: 113, b: 196 } }, + 48: { id: 49, name: 'Light Slate Blue', rgb: { r: 181, g: 174, b: 241 } }, + 49: { id: 53, name: 'Dark Peach', rgb: { r: 155, g: 82, b: 73 } }, + 50: { id: 54, name: 'Peach', rgb: { r: 209, g: 128, b: 120 } }, + 51: { id: 55, name: 'Light Peach', rgb: { r: 250, g: 182, b: 164 } }, + 52: { id: 50, name: 'Light Brown', rgb: { r: 219, g: 164, b: 99 } }, + 53: { id: 56, name: 'Dark Tan', rgb: { r: 123, g: 99, b: 82 } }, + 54: { id: 57, name: 'Tan', rgb: { r: 156, g: 132, b: 107 } }, + 55: { id: 36, name: 'Light Tan', rgb: { r: 214, g: 181, b: 148 } }, + 56: { id: 51, name: 'Dark Beige', rgb: { r: 209, g: 128, b: 81 } }, + 57: { id: 61, name: 'Dark Stone', rgb: { r: 109, g: 100, b: 63 } }, + 58: { id: 62, name: 'Stone', rgb: { r: 148, g: 140, b: 107 } }, + 59: { id: 63, name: 'Light Stone', rgb: { r: 205, g: 197, b: 158 } }, + 60: { id: 58, name: 'Dark Slate', rgb: { r: 51, g: 57, b: 65 } }, + 61: { id: 59, name: 'Slate', rgb: { r: 109, g: 117, b: 141 } }, + 62: { id: 60, name: 'Light Slate', rgb: { r: 179, g: 185, b: 209 } }, + 63: { id: 0, name: 'Transparent', rgb: null }, + }, // --- END: Color data --- + // Optimized CSS Classes for reuse + CSS_CLASSES: { + BUTTON_PRIMARY: ` + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + color: white; border: none; border-radius: 8px; padding: 10px 16px; + cursor: pointer; font-weight: 500; transition: all 0.3s ease; + display: flex; align-items: center; gap: 8px; + `, + BUTTON_SECONDARY: ` + background: rgba(255,255,255,0.1); color: white; + border: 1px solid rgba(255,255,255,0.2); border-radius: 8px; + padding: 8px 12px; cursor: pointer; transition: all 0.3s ease; + `, + MODERN_CARD: ` + background: rgba(255,255,255,0.1); border-radius: 12px; + padding: 18px; border: 1px solid rgba(255,255,255,0.1); + backdrop-filter: blur(5px); + `, + GRADIENT_TEXT: ` + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + -webkit-background-clip: text; -webkit-text-fill-color: transparent; + background-clip: text; font-weight: bold; + `, + }, + THEMES: { + 'Classic Autobot': { + primary: '#000000', + secondary: '#111111', + accent: '#222222', + text: '#ffffff', + highlight: '#775ce3', + success: '#00ff00', + error: '#ff0000', + warning: '#ffaa00', + fontFamily: "'Segoe UI', Roboto, sans-serif", + borderRadius: '12px', + borderStyle: 'solid', + borderWidth: '1px', + boxShadow: '0 8px 32px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.1)', + backdropFilter: 'blur(10px)', + animations: { + glow: false, + scanline: false, + 'pixel-blink': 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.6), 0 0 0 1px rgba(255,255,255,0.1)", - backdropFilter: "blur(10px)", + borderRadius: '12px', + borderStyle: 'solid', + borderWidth: '1px', + boxShadow: '0 8px 32px rgba(0,0,0,0.15), 0 0 0 1px rgba(0,0,0,0.08)', + backdropFilter: 'blur(10px)', animations: { glow: false, scanline: false, - pixelBlink: false, + 'pixel-blink': false, }, }, - "Neon Retro": { - primary: "#1a1a2e", - secondary: "#16213e", - accent: "#0f3460", - text: "#00ff41", - highlight: "#ff6b35", - success: "#39ff14", - error: "#ff073a", - warning: "#ffff00", - neon: "#00ffff", - purple: "#bf00ff", - pink: "#ff1493", + 'Neon Retro': { + primary: '#1a1a2e', + secondary: '#16213e', + accent: '#0f3460', + text: '#00ff41', + highlight: '#ff6b35', + success: '#39ff14', + error: '#ff073a', + warning: '#ffff00', + neon: '#00ffff', + purple: '#bf00ff', + pink: '#ff1493', fontFamily: "'Press Start 2P', monospace", - borderRadius: "0", - borderStyle: "solid", - borderWidth: "3px", - boxShadow: "0 0 20px rgba(0, 255, 65, 0.3), inset 0 0 20px rgba(0, 255, 65, 0.1)", - backdropFilter: "none", + borderRadius: '0', + borderStyle: 'solid', + borderWidth: '3px', + boxShadow: '0 0 20px rgba(0, 255, 65, 0.3), inset 0 0 20px rgba(0, 255, 65, 0.1)', + backdropFilter: 'none', animations: { glow: true, scanline: true, - pixelBlink: true, + 'pixel-blink': true, }, }, }, - currentTheme: "Classic Autobot", - } + currentTheme: 'Classic Autobot', + PAINT_UNAVAILABLE: true, + COORDINATE_MODE: 'rows', + COORDINATE_DIRECTION: 'top-left', + COORDINATE_SNAKE: true, + COORDINATE_BLOCK_WIDTH: 6, + COORDINATE_BLOCK_HEIGHT: 2, + }; - const getCurrentTheme = () => CONFIG.THEMES[CONFIG.currentTheme] + const getCurrentTheme = () => CONFIG.THEMES[CONFIG.currentTheme]; const switchTheme = (themeName) => { if (CONFIG.THEMES[themeName]) { - CONFIG.currentTheme = themeName - saveThemePreference() - - // Remove existing theme styles - const existingStyle = document.querySelector('style[data-wplace-theme="true"]') - if (existingStyle) { - existingStyle.remove() - } + CONFIG.currentTheme = themeName; + saveThemePreference(); - // Recreate UI with new theme - const existingContainer = document.getElementById("wplace-image-bot-container") - const existingStats = document.getElementById("wplace-stats-container") - if (existingContainer) existingContainer.remove() - if (existingStats) existingStats.remove() + // APPLY THEME VARS/CLASS (new) + applyTheme(); - 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 { - localStorage.setItem("wplace-theme", CONFIG.currentTheme) + localStorage.setItem('wplace-theme', CONFIG.currentTheme); } catch (e) { - console.warn("Could not save theme preference:", e) + console.warn('Could not save theme preference:', e); } - } + }; const loadThemePreference = () => { try { - const saved = localStorage.getItem("wplace-theme") + const saved = localStorage.getItem('wplace-theme'); if (saved && CONFIG.THEMES[saved]) { - CONFIG.currentTheme = saved + CONFIG.currentTheme = saved; } } catch (e) { - console.warn("Could not load theme preference:", e) + console.warn('Could not load theme preference:', e); } - } + }; + + // Simple translation cache + const translationCache = new Map(); + + // Dynamically loaded translations + let loadedTranslations = {}; + + // Available languages + const AVAILABLE_LANGUAGES = [ + 'en', + 'ru', + 'pt', + 'vi', + 'fr', + 'id', + 'tr', + 'zh-CN', + 'zh-TW', + 'ja', + 'ko', + 'uk', + ]; + + // Function to load translations from JSON file with retry mechanism + const loadTranslations = async (language, retryCount = 0) => { + if (loadedTranslations[language]) { + return loadedTranslations[language]; + } + + // Load translations from CDN + const url = `https://wplace-autobot.github.io/WPlace-AutoBOT/main/lang/${language}.json`; + const maxRetries = 3; + const baseDelay = 1000; // 1 second + + try { + if (retryCount === 0) { + console.log(`🔄 Loading ${language} translations from CDN...`); + } else { + console.log( + `🔄 Retrying ${language} translations (attempt ${retryCount + 1}/${maxRetries + 1})...` + ); + } + + const response = await fetch(url); + if (response.ok) { + const translations = await response.json(); + + // Validate that translations is an object with keys + if ( + typeof translations === 'object' && + translations !== null && + Object.keys(translations).length > 0 + ) { + loadedTranslations[language] = translations; + console.log( + `📚 Loaded ${language} translations successfully from CDN (${ + Object.keys(translations).length + } keys)` + ); + return translations; + } else { + console.warn(`❌ Invalid translation format for ${language}`); + throw new Error('Invalid translation format'); + } + } else { + console.warn( + `❌ CDN returned HTTP ${response.status}: ${response.statusText} for ${language} translations` + ); + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + } catch (error) { + console.error( + `❌ Failed to load ${language} translations from CDN (attempt ${retryCount + 1}):`, + error + ); + + // Retry with exponential backoff + if (retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount); + console.log(`⏳ Retrying in ${delay}ms...`); + await new Promise((resolve) => setTimeout(resolve, delay)); + return loadTranslations(language, retryCount + 1); + } + } + + return null; + }; - // BILINGUAL TEXT STRINGS - const TEXT = { + const loadLanguagePreference = async () => { + const savedLanguage = localStorage.getItem('wplace_language'); + const browserLocale = navigator.language; + const browserLanguage = browserLocale.split('-')[0]; + + let selectedLanguage = 'en'; // Default fallback + + try { + // Check if we have the saved language available + if (savedLanguage && AVAILABLE_LANGUAGES.includes(savedLanguage)) { + selectedLanguage = savedLanguage; + console.log(`🔄 Using saved language preference: ${selectedLanguage}`); + } + // Try full locale match (e.g. "zh-CN", "zh-TW" etc) + else if (AVAILABLE_LANGUAGES.includes(browserLocale)) { + selectedLanguage = browserLocale; + localStorage.setItem('wplace_language', browserLocale); + console.log(`🔄 Using browser locale: ${selectedLanguage}`); + } + // Try base language match (e.g. "en" for "en-US" or "en-GB" etc) + else if (AVAILABLE_LANGUAGES.includes(browserLanguage)) { + selectedLanguage = browserLanguage; + localStorage.setItem('wplace_language', browserLanguage); + console.log(`🔄 Using browser language: ${selectedLanguage}`); + } + // Use English as fallback + else { + console.log(`🔄 No matching language found, using English fallback`); + } + + // Set the language in state first + state.language = selectedLanguage; + + // Only load translations if not already loaded and not English (which should already be loaded) + if (selectedLanguage !== 'en' && !loadedTranslations[selectedLanguage]) { + const loaded = await loadTranslations(selectedLanguage); + if (!loaded) { + console.warn( + `⚠️ Failed to load ${selectedLanguage} translations, falling back to English` + ); + state.language = 'en'; + localStorage.setItem('wplace_language', 'en'); + } + } + } catch (error) { + console.error(`❌ Error in loadLanguagePreference:`, error); + state.language = 'en'; // Always ensure we have a valid language + } + }; + + // Simple user notification function for critical issues + const showTranslationWarning = (message) => { + try { + // Create a simple temporary notification banner + const warning = document.createElement('div'); + warning.style.cssText = ` + position: fixed; top: 10px; right: 10px; z-index: 10001; + background: rgba(255, 193, 7, 0.95); color: #212529; padding: 12px 16px; + border-radius: 8px; font-size: 14px; font-weight: 500; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); border: 1px solid rgba(255, 193, 7, 0.8); + max-width: 300px; word-wrap: break-word; + `; + warning.textContent = message; + document.body.appendChild(warning); + + // Auto-remove after 8 seconds + setTimeout(() => { + if (warning.parentNode) { + warning.remove(); + } + }, 8000); + } catch (e) { + // If DOM manipulation fails, just log + console.warn('Failed to show translation warning UI:', e); + } + }; + + // Initialize translations function + const initializeTranslations = async () => { + try { + console.log('🌐 Initializing translation system...'); + + // Always ensure English is loaded as fallback first + if (!loadedTranslations['en']) { + const englishLoaded = await loadTranslations('en'); + if (!englishLoaded) { + console.warn('⚠️ Failed to load English translations from CDN, using fallback'); + showTranslationWarning('⚠️ Translation loading failed, using basic fallbacks'); + } + } + + // Then load user's language preference + await loadLanguagePreference(); + + console.log(`✅ Translation system initialized. Active language: ${state.language}`); + } catch (error) { + console.error('❌ Translation initialization failed:', error); + // Ensure state has a valid language even if loading fails + if (!state.language) { + state.language = 'en'; + } + console.warn('⚠️ Using fallback translations due to initialization failure'); + showTranslationWarning('⚠️ Translation system error, using basic English'); + } + }; + + // Emergency fallback TEXT (minimal) + const FALLBACK_TEXT = { en: { - title: "WPlace Auto-Image", - initBot: "Start Auto-BOT", - uploadImage: "Upload Image", - resizeImage: "Resize Image", - selectPosition: "Select Position", - startPainting: "Start Painting", - stopPainting: "Stop Painting", - checkingColors: "🔍 Checking available colors...", - noColorsFound: "❌ Open the color palette on the site and try again!", - colorsFound: "✅ {count} available colors found", - loadingImage: "🖼️ Loading image...", - imageLoaded: "✅ Image loaded with {count} valid pixels", - imageError: "❌ Error loading image", - selectPositionAlert: "Paint the first pixel at the location where you want the art to start!", - waitingPosition: "👆 Waiting for you to paint the reference pixel...", - positionSet: "✅ Position set successfully!", - positionTimeout: "❌ Timeout for position selection", - startPaintingMsg: "🎨 Starting painting...", - paintingProgress: "🧱 Progress: {painted}/{total} pixels...", - noCharges: "⌛ No charges. Waiting {time}...", - paintingStopped: "⏹️ Painting stopped by user", - paintingComplete: "✅ Painting complete! {count} pixels painted.", - paintingError: "❌ Error during painting", - missingRequirements: "❌ Load an image and select a position first", - progress: "Progress", - pixels: "Pixels", - charges: "Charges", - estimatedTime: "Estimated time", - initMessage: "Click 'Start Auto-BOT' to begin", - waitingInit: "Waiting for initialization...", - resizeSuccess: "✅ Image resized to {width}x{height}", - paintingPaused: "⏸️ Painting paused at position X: {x}, Y: {y}", - captchaNeeded: "❗ CAPTCHA token needed. Paint one pixel manually to continue.", - saveData: "Save Progress", - loadData: "Load Progress", - saveToFile: "Save to File", - loadFromFile: "Load from File", - dataManager: "Data Manager", - autoSaved: "✅ Progress saved automatically", - dataLoaded: "✅ Progress loaded successfully", - fileSaved: "✅ Progress saved to file successfully", - fileLoaded: "✅ Progress loaded from file successfully", - noSavedData: "❌ No saved progress found", - savedDataFound: "✅ Saved progress found! Load to continue?", - savedDate: "Saved on: {date}", - clickLoadToContinue: "Click 'Load Progress' to continue.", - fileError: "❌ Error processing file", - invalidFileFormat: "❌ Invalid file format", - }, - pt: { - title: "WPlace Auto-Image", - initBot: "Iniciar Auto-BOT", - uploadImage: "Upload da Imagem", - resizeImage: "Redimensionar Imagem", - selectPosition: "Selecionar Posição", - startPainting: "Iniciar Pintura", - stopPainting: "Parar Pintura", - checkingColors: "🔍 Verificando cores disponíveis...", - noColorsFound: "❌ Abra a paleta de cores no site e tente novamente!", - colorsFound: "✅ {count} cores disponíveis encontradas", - loadingImage: "🖼️ Carregando imagem...", - imageLoaded: "✅ Imagem carregada com {count} pixels válidos", - imageError: "❌ Erro ao carregar imagem", - selectPositionAlert: "Pinte o primeiro pixel na localização onde deseja que a arte comece!", - waitingPosition: "👆 Aguardando você pintar o pixel de referência...", - positionSet: "✅ Posição definida com sucesso!", - positionTimeout: "❌ Tempo esgotado para selecionar posição", - startPaintingMsg: "🎨 Iniciando pintura...", - paintingProgress: "🧱 Progresso: {painted}/{total} pixels...", - noCharges: "⌛ Sem cargas. Aguardando {time}...", - paintingStopped: "⏹️ Pintura interrompida pelo usuário", - paintingComplete: "✅ Pintura concluída! {count} pixels pintados.", - paintingError: "❌ Erro durante a pintura", - missingRequirements: "❌ Carregue uma imagem e selecione uma posição primeiro", - progress: "Progresso", - pixels: "Pixels", - charges: "Cargas", - estimatedTime: "Tempo estimado", - initMessage: "Clique em 'Iniciar Auto-BOT' para começar", - waitingInit: "Aguardando inicialização...", - resizeSuccess: "✅ Imagem redimensionada para {width}x{height}", - paintingPaused: "⏸️ Pintura pausada na posição X: {x}, Y: {y}", - captchaNeeded: "❗ Token CAPTCHA necessário. Pinte um pixel manualmente para continuar.", - saveData: "Salvar Progresso", - loadData: "Carregar Progresso", - saveToFile: "Salvar em Arquivo", - loadFromFile: "Carregar de Arquivo", - dataManager: "Dados", - autoSaved: "✅ Progresso salvo automaticamente", - dataLoaded: "✅ Progresso carregado com sucesso", - fileSaved: "✅ Salvo em arquivo com sucesso", - fileLoaded: "✅ Carregado de arquivo com sucesso", - noSavedData: "❌ Nenhum progresso salvo encontrado", - savedDataFound: "✅ Progresso salvo encontrado! Carregar para continuar?", - savedDate: "Salvo em: {date}", - clickLoadToContinue: "Clique em 'Carregar Progresso' para continuar.", - fileError: "❌ Erro ao processar arquivo", - invalidFileFormat: "❌ Formato de arquivo inválido", + title: 'WPlace Auto-Image', + toggleOverlay: 'Toggle Overlay', + scanColors: 'Scan Colors', + uploadImage: 'Upload Image', + resizeImage: 'Resize Image', + selectPosition: 'Select Position', + startPainting: 'Start Painting', + stopPainting: 'Stop Painting', + progress: 'Progress', + pixels: 'Pixels', + charges: 'Charges', + batchSize: 'Batch Size', + cooldownSettings: 'Cooldown Settings', + waitCharges: 'Wait for Charges', + settings: 'Settings', + showStats: 'Show Statistics', + compactMode: 'Compact Mode', + minimize: 'Minimize', + tokenCapturedSuccess: 'Token captured successfully', + turnstileInstructions: 'Complete the verification', + hideTurnstileBtn: 'Hide', + notificationsNotSupported: 'Notifications not supported', + chargesReadyMessage: 'Charges are ready', + chargesReadyNotification: 'WPlace AutoBot', + initMessage: "Click 'Upload Image' to begin", }, - } + }; + + // Safe translation function with multiple fallback levels + const getText = (key, replacements = {}) => { + // Try current language first + let text = loadedTranslations[state.language]?.[key]; + + // Fallback to English translations + if (!text && state.language !== 'en') { + text = loadedTranslations['en']?.[key]; + } + + // Fallback to hardcoded English + if (!text) { + text = FALLBACK_TEXT['en']?.[key]; + } + + // Last resort - return the key itself + if (!text) { + console.warn(`⚠️ Missing translation for key: ${key}`); + return key; + } + + // Handle string replacements like {count}, {time}, etc. + return Object.entries(replacements).reduce((result, [placeholder, value]) => { + return result.replace(new RegExp(`\\{${placeholder}\\}`, 'g'), value); + }, text); + }; // GLOBAL STATE const state = { @@ -209,7 +538,14 @@ totalPixels: 0, paintedPixels: 0, availableColors: [], - currentCharges: 0, + activeColorPalette: [], // User-selected colors for conversion + paintWhitePixels: true, // Default to ON + fullChargeData: null, + fullChargeInterval: null, + paintTransparentPixels: false, // Default to OFF + displayCharges: 0, + preciseCurrentCharges: 0, + maxCharges: 1, // Default max charges cooldown: CONFIG.COOLDOWN_DEFAULT, imageData: null, stopFlag: false, @@ -220,1275 +556,2862 @@ minimized: false, lastPosition: { x: 0, y: 0 }, estimatedTime: 0, - language: "en", - } - - // Global variable to store the captured CAPTCHA token. - let capturedCaptchaToken = null + language: 'en', + paintingSpeed: CONFIG.PAINTING_SPEED.DEFAULT, // pixels batch size + batchMode: CONFIG.BATCH_MODE, // "normal" or "random" + randomBatchMin: CONFIG.RANDOM_BATCH_RANGE.MIN, // Random range minimum + randomBatchMax: CONFIG.RANDOM_BATCH_RANGE.MAX, // Random range maximum + cooldownChargeThreshold: CONFIG.COOLDOWN_CHARGE_THRESHOLD, + chargesThresholdInterval: null, + tokenSource: CONFIG.TOKEN_SOURCE, // "generator" or "manual" + initialSetupComplete: false, // Track if initial startup setup is complete (only happens once) + overlayOpacity: CONFIG.OVERLAY.OPACITY_DEFAULT, + blueMarbleEnabled: CONFIG.OVERLAY.BLUE_MARBLE_DEFAULT, + ditheringEnabled: true, + // Advanced color matching settings + colorMatchingAlgorithm: 'lab', + enableChromaPenalty: true, + chromaPenaltyWeight: 0.15, + customTransparencyThreshold: CONFIG.TRANSPARENCY_THRESHOLD, + customWhiteThreshold: CONFIG.WHITE_THRESHOLD, + resizeSettings: null, + originalImage: null, + resizeIgnoreMask: null, + paintUnavailablePixels: CONFIG.PAINT_UNAVAILABLE, + // Coordinate generation settings + coordinateMode: CONFIG.COORDINATE_MODE, + coordinateDirection: CONFIG.COORDINATE_DIRECTION, + coordinateSnake: CONFIG.COORDINATE_SNAKE, + blockWidth: CONFIG.COORDINATE_BLOCK_WIDTH, + blockHeight: CONFIG.COORDINATE_BLOCK_HEIGHT, + notificationsEnabled: CONFIG.NOTIFICATIONS.ENABLED, + notifyOnChargesReached: CONFIG.NOTIFICATIONS.ON_CHARGES_REACHED, + notifyOnlyWhenUnfocused: CONFIG.NOTIFICATIONS.ONLY_WHEN_UNFOCUSED, + notificationIntervalMinutes: CONFIG.NOTIFICATIONS.REPEAT_MINUTES, + _lastChargesNotifyAt: 0, + _lastChargesBelow: true, + // Smart save tracking + _lastSavePixelCount: 0, + _lastSaveTime: 0, + _saveInProgress: false, + paintedMap: null, + }; + + let _updateResizePreview = () => {}; + let _resizeDialogCleanup = null; + + // --- OVERLAY UPDATE: Optimized OverlayManager class with performance improvements --- + class OverlayManager { + constructor() { + this.isEnabled = false; + this.startCoords = null; // { region: {x, y}, pixel: {x, y} } + this.imageBitmap = null; + this.chunkedTiles = new Map(); // Map<"tileX,tileY", ImageBitmap> + this.originalTiles = new Map(); // Map<"tileX,tileY", ImageBitmap> store latest original tile bitmaps + this.originalTilesData = new Map(); // Map<"tileX,tileY", {w,h,data:Uint8ClampedArray}> cache full ImageData for fast pixel reads + this.tileSize = 1000; + this.processPromise = null; // Track ongoing processing + this.lastProcessedHash = null; // Cache invalidation + this.workerPool = null; // Web worker pool for heavy processing + } - // Intercept the original window.fetch function to "listen" for network requests. - const originalFetch = window.fetch - window.fetch = async (url, options) => { - // Check if the request is for painting a pixel on wplace. - if (typeof url === "string" && url.includes("https://backend.wplace.live/s0/pixel/")) { - try { - const payload = JSON.parse(options.body) - // If the request body contains the 't' field, it's our CAPTCHA token. - if (payload.t) { - console.log("✅ CAPTCHA Token Captured:", payload.t) - // Store the token for our bot to use. - capturedCaptchaToken = payload.t - - // Notify the user that the token is captured and they can start the bot. - if (document.querySelector("#statusText")?.textContent.includes("CAPTCHA")) { - Utils.showAlert("Token captured successfully! You can start the bot now.", "success") - updateUI("colorsFound", "success", { - count: state.availableColors.length, - }) - } - } - } catch (e) { - /* Ignore errors if the request body isn't valid JSON */ - } + toggle() { + this.isEnabled = !this.isEnabled; + console.log(`Overlay ${this.isEnabled ? 'enabled' : 'disabled'}.`); + return this.isEnabled; } - // Finally, execute the original request, whether we inspected it or not. - return originalFetch(url, options) - } - // LANGUAGE DETECTION - 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" + enable() { + this.isEnabled = true; } - } - // UTILITY FUNCTIONS - const Utils = { - sleep: (ms) => new Promise((r) => setTimeout(r, ms)), + disable() { + this.isEnabled = false; + } - t: (key, params = {}) => { - let text = TEXT[state.language]?.[key] || TEXT.en[key] || key - Object.keys(params).forEach((param) => { - text = text.replace(`{${param}}`, params[param]) - }) - return text - }, + clear() { + this.disable(); + this.imageBitmap = null; + this.chunkedTiles.clear(); + this.originalTiles.clear(); + this.originalTilesData.clear(); + this.lastProcessedHash = null; + if (this.processPromise) { + this.processPromise = null; + } + } - 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; - ` + async setImage(imageBitmap) { + this.imageBitmap = imageBitmap; + this.lastProcessedHash = null; // Invalidate cache + if (this.imageBitmap && this.startCoords) { + await this.processImageIntoChunks(); + } + } - 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);", + async setPosition(startPosition, region) { + if (!startPosition || !region) { + this.startCoords = null; + this.chunkedTiles.clear(); + this.lastProcessedHash = null; + return; } + this.startCoords = { region, pixel: startPosition }; + this.lastProcessedHash = null; // Invalidate cache + if (this.imageBitmap) { + await this.processImageIntoChunks(); + } + } - alertDiv.style.cssText += colors[type] || colors.info + // Generate hash for cache invalidation + _generateProcessHash() { + if (!this.imageBitmap || !this.startCoords) return null; + const { width, height } = this.imageBitmap; + const { x: px, y: py } = this.startCoords.pixel; + const { x: rx, y: ry } = this.startCoords.region; + return `${width}x${height}_${px},${py}_${rx},${ry}_${state.blueMarbleEnabled}_${state.overlayOpacity}`; + } - 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) + // --- OVERLAY UPDATE: Optimized chunking with caching and batch processing --- + async processImageIntoChunks() { + if (!this.imageBitmap || !this.startCoords) return; - alertDiv.textContent = message - document.body.appendChild(alertDiv) + // Check if we're already processing to avoid duplicate work + if (this.processPromise) { + return this.processPromise; + } - setTimeout(() => { - alertDiv.style.animation = "slideDown 0.3s ease-out reverse" - setTimeout(() => { - document.body.removeChild(alertDiv) - document.head.removeChild(style) - }, 300) - }, 4000) - }, + // Check cache validity + const currentHash = this._generateProcessHash(); + if (this.lastProcessedHash === currentHash && this.chunkedTiles.size > 0) { + console.log(`📦 Using cached overlay chunks (${this.chunkedTiles.size} tiles)`); + return; + } + + // Start processing + this.processPromise = this._doProcessImageIntoChunks(); + try { + await this.processPromise; + this.lastProcessedHash = currentHash; + } finally { + this.processPromise = null; + } + } - colorDistance: (a, b) => Math.sqrt(Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2) + Math.pow(a[2] - b[2], 2)), + async _doProcessImageIntoChunks() { + const startTime = performance.now(); + this.chunkedTiles.clear(); + + const { width: imageWidth, height: imageHeight } = this.imageBitmap; + const { x: startPixelX, y: startPixelY } = this.startCoords.pixel; + const { x: startRegionX, y: startRegionY } = this.startCoords.region; + + const { startTileX, startTileY, endTileX, endTileY } = Utils.calculateTileRange( + startRegionX, + startRegionY, + startPixelX, + startPixelY, + imageWidth, + imageHeight, + this.tileSize + ); + + const totalTiles = (endTileX - startTileX + 1) * (endTileY - startTileY + 1); + console.log(`🔄 Processing ${totalTiles} overlay tiles...`); + + // Process tiles in batches to avoid blocking the main thread + const batchSize = 4; // Process 4 tiles at a time + const tilesToProcess = []; + + for (let ty = startTileY; ty <= endTileY; ty++) { + for (let tx = startTileX; tx <= endTileX; tx++) { + tilesToProcess.push({ tx, ty }); + } + } - isWhitePixel: (r, g, b) => - r >= CONFIG.WHITE_THRESHOLD && g >= CONFIG.WHITE_THRESHOLD && b >= CONFIG.WHITE_THRESHOLD, + // Process tiles in batches with yielding + for (let i = 0; i < tilesToProcess.length; i += batchSize) { + const batch = tilesToProcess.slice(i, i + batchSize); + + await Promise.all( + batch.map(async ({ tx, ty }) => { + const tileKey = `${tx},${ty}`; + const chunkBitmap = await this._processTile( + tx, + ty, + imageWidth, + imageHeight, + startPixelX, + startPixelY, + startRegionX, + startRegionY + ); + if (chunkBitmap) { + this.chunkedTiles.set(tileKey, chunkBitmap); + } + }) + ); - 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]) + // Yield control to prevent blocking + if (i + batchSize < tilesToProcess.length) { + await new Promise((resolve) => setTimeout(resolve, 0)); } - input.click() - }), + } - createFileDownloader: (data, filename) => { - const blob = new Blob([data], { type: "application/json" }) - const url = URL.createObjectURL(blob) - const a = document.createElement("a") - a.href = url - a.download = filename - document.body.appendChild(a) - a.click() - document.body.removeChild(a) - URL.revokeObjectURL(url) - }, + const processingTime = performance.now() - startTime; + console.log( + `✅ Overlay processed ${this.chunkedTiles.size} tiles in ${Math.round(processingTime)}ms` + ); + } - 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")) - } - } - reader.onerror = () => reject(new Error("File reading error")) - reader.readAsText(file) - } else { - reject(new Error("No file selected")) + async _processTile( + tx, + ty, + imageWidth, + imageHeight, + startPixelX, + startPixelY, + startRegionX, + startRegionY + ) { + const tileKey = `${tx},${ty}`; + + // Calculate the portion of the image that overlaps with this tile + const imgStartX = (tx - startRegionX) * this.tileSize - startPixelX; + const imgStartY = (ty - startRegionY) * this.tileSize - startPixelY; + + // Crop coordinates within the source image + const sX = Math.max(0, imgStartX); + const sY = Math.max(0, imgStartY); + const sW = Math.min(imageWidth - sX, this.tileSize - (sX - imgStartX)); + const sH = Math.min(imageHeight - sY, this.tileSize - (sY - imgStartY)); + + if (sW <= 0 || sH <= 0) return null; + + // Destination coordinates on the new chunk canvas + const dX = Math.max(0, -imgStartX); + const dY = Math.max(0, -imgStartY); + + const chunkCanvas = new OffscreenCanvas(this.tileSize, this.tileSize); + const chunkCtx = chunkCanvas.getContext('2d'); + chunkCtx.imageSmoothingEnabled = false; + + chunkCtx.drawImage(this.imageBitmap, sX, sY, sW, sH, dX, dY, sW, sH); + + // --- OPTIMIZED: Blue marble effect with faster pixel manipulation --- + 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 } } - input.click() - }), - - extractAvailableColors: () => { - const colorElements = document.querySelectorAll('[id^="color-"]') - return Array.from(colorElements) - .filter((el) => !el.querySelector("svg")) - .filter((el) => { - const id = Number.parseInt(el.id.replace("color-", "")) - return id !== 0 && id !== 5 - }) - .map((el) => { - const id = Number.parseInt(el.id.replace("color-", "")) - const rgbStr = el.style.backgroundColor.match(/\d+/g) - const rgb = rgbStr ? rgbStr.map(Number) : [0, 0, 0] - return { id, rgb } - }) - }, - formatTime: (ms) => { - const seconds = Math.floor((ms / 1000) % 60) - const minutes = Math.floor((ms / (1000 * 60)) % 60) - const hours = Math.floor((ms / (1000 * 60 * 60)) % 24) - const days = Math.floor(ms / (1000 * 60 * 60 * 24)) - - let result = "" - if (days > 0) result += `${days}d ` - if (hours > 0 || days > 0) result += `${hours}h ` - if (minutes > 0 || hours > 0 || days > 0) result += `${minutes}m ` - result += `${seconds}s` - - return result - }, + chunkCtx.putImageData(imageData, dX, dY); + } - calculateEstimatedTime: (remainingPixels, charges, cooldown) => { - if (remainingPixels <= 0) return 0 - const cyclesNeeded = Math.ceil(remainingPixels / Math.max(charges, 1)) - return cyclesNeeded * cooldown - }, + return await chunkCanvas.transferToImageBitmap(); + } - // Save/Load Progress Functions - saveProgress: () => { - try { - const progressData = { - timestamp: Date.now(), - state: { - totalPixels: state.totalPixels, - paintedPixels: state.paintedPixels, - lastPosition: state.lastPosition, - startPosition: state.startPosition, - region: state.region, - imageLoaded: state.imageLoaded, - colorsChecked: state.colorsChecked, - availableColors: state.availableColors, - }, - imageData: state.imageData - ? { - width: state.imageData.width, - height: state.imageData.height, - pixels: Array.from(state.imageData.pixels), - totalPixels: state.imageData.totalPixels, + // --- OVERLAY UPDATE: Optimized compositing with caching --- + async processAndRespondToTileRequest(eventData) { + const { endpoint, blobID, blobData } = eventData; + + let finalBlob = blobData; + + if (this.isEnabled && this.chunkedTiles.size > 0) { + const tileMatch = endpoint.match(/(\d+)\/(\d+)\.png/); + if (tileMatch) { + const tileX = parseInt(tileMatch[1], 10); + const tileY = parseInt(tileMatch[2], 10); + const tileKey = `${tileX},${tileY}`; + + const chunkBitmap = this.chunkedTiles.get(tileKey); + // Also store the original tile bitmap for later pixel color checks + try { + const originalBitmap = await createImageBitmap(blobData); + this.originalTiles.set(tileKey, originalBitmap); + // Cache full ImageData for fast pixel access (avoid repeated drawImage/getImageData) + try { + let canvas, ctx; + if (typeof OffscreenCanvas !== 'undefined') { + canvas = new OffscreenCanvas(originalBitmap.width, originalBitmap.height); + ctx = canvas.getContext('2d'); + } else { + canvas = document.createElement('canvas'); + canvas.width = originalBitmap.width; + canvas.height = originalBitmap.height; + ctx = canvas.getContext('2d'); } - : null, - paintedMap: state.paintedMap ? state.paintedMap.map((row) => Array.from(row)) : null, + ctx.imageSmoothingEnabled = false; + ctx.drawImage(originalBitmap, 0, 0); + const imgData = ctx.getImageData(0, 0, originalBitmap.width, originalBitmap.height); + // Store typed array copy to avoid retaining large canvas + this.originalTilesData.set(tileKey, { + w: originalBitmap.width, + h: originalBitmap.height, + data: new Uint8ClampedArray(imgData.data), + }); + } catch (e) { + // If ImageData extraction fails, still keep the bitmap as fallback + console.warn('OverlayManager: could not cache ImageData for', tileKey, e); + } + } catch (e) { + console.warn('OverlayManager: could not create original bitmap for', tileKey, e); + } + if (chunkBitmap) { + try { + // Use faster compositing for better performance + finalBlob = await this._compositeTileOptimized(blobData, chunkBitmap); + } catch (e) { + console.error('Error compositing overlay:', e); + // Fallback to original tile on error + finalBlob = blobData; + } + } } - - localStorage.setItem("wplace-bot-progress", JSON.stringify(progressData)) - return true - } catch (error) { - console.error("Error saving progress:", error) - return false } - }, - loadProgress: () => { - try { - const saved = localStorage.getItem("wplace-bot-progress") - return saved ? JSON.parse(saved) : null - } catch (error) { - console.error("Error loading progress:", error) - return null - } - }, + // Send the (possibly modified) blob back to the injected script + window.postMessage( + { + source: 'auto-image-overlay', + blobID: blobID, + blobData: finalBlob, + }, + '*' + ); + } - clearProgress: () => { - try { - localStorage.removeItem("wplace-bot-progress") - return true - } catch (error) { - console.error("Error clearing progress:", error) - return false + // Returns [r,g,b,a] for a pixel inside a region tile (tileX, tileY are region coords) + async getTilePixelColor(tileX, tileY, pixelX, pixelY) { + const tileKey = `${tileX},${tileY}`; + const alphaThresh = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; + + // 1. Prefer cached ImageData if available + const cached = this.originalTilesData.get(tileKey); + if (cached && cached.data && cached.w > 0 && cached.h > 0) { + const x = Math.max(0, Math.min(cached.w - 1, pixelX)); + const y = Math.max(0, Math.min(cached.h - 1, pixelY)); + const idx = (y * cached.w + x) * 4; + const d = cached.data; + const a = d[idx + 3]; + + if (!state.paintTransparentPixels && a < alphaThresh) { + // Treat as transparent / unavailable + // Lightweight debug: show when transparency causes skip (only if verbose enabled) + if (window._overlayDebug) + console.debug('OverlayManager: pixel transparent (cached), skipping', tileKey, x, y, a); + return null; + } + return [d[idx], d[idx + 1], d[idx + 2], a]; } - }, - restoreProgress: (savedData) => { - try { - // Restore state - Object.assign(state, savedData.state) - - // Restore image data - if (savedData.imageData) { - state.imageData = { - ...savedData.imageData, - pixels: new Uint8ClampedArray(savedData.imageData.pixels), + // 2. Fallback: use bitmap, with retry + const maxRetries = 3; + for (let attempt = 1; attempt <= maxRetries; attempt++) { + const bitmap = this.originalTiles.get(tileKey); + if (!bitmap) { + if (attempt === maxRetries) { + console.warn('OverlayManager: no bitmap for', tileKey, 'after', maxRetries, 'attempts'); + } else { + await Utils.sleep(50 * attempt); // exponential delay } + continue; } - // Restore painted map - if (savedData.paintedMap) { - state.paintedMap = savedData.paintedMap.map((row) => Array.from(row)) - } + try { + let canvas, ctx; + if (typeof OffscreenCanvas !== 'undefined') { + canvas = new OffscreenCanvas(bitmap.width, bitmap.height); + ctx = canvas.getContext('2d'); + } else { + canvas = document.createElement('canvas'); + canvas.width = bitmap.width; + canvas.height = bitmap.height; + ctx = canvas.getContext('2d'); + } + ctx.imageSmoothingEnabled = false; + ctx.drawImage(bitmap, 0, 0); + + const x = Math.max(0, Math.min(bitmap.width - 1, pixelX)); + const y = Math.max(0, Math.min(bitmap.height - 1, pixelY)); + const data = ctx.getImageData(x, y, 1, 1).data; + const a = data[3]; + + if (!state.paintTransparentPixels && a < alphaThresh) { + if (window._overlayDebug) + console.debug('OverlayManager: pixel transparent (fallback)', tileKey, x, y, a); + return null; + } - return true - } catch (error) { - console.error("Error restoring progress:", error) - return false + return [data[0], data[1], data[2], a]; + } catch (e) { + console.warn('OverlayManager: failed to read pixel (attempt', attempt, ')', tileKey, e); + if (attempt < maxRetries) { + await Utils.sleep(50 * attempt); + } else { + console.error( + 'OverlayManager: failed to read pixel after', + maxRetries, + 'attempts', + tileKey + ); + } + } } - }, - saveProgressToFile: () => { - try { - const progressData = { - timestamp: Date.now(), - version: "1.0", - state: { - totalPixels: state.totalPixels, - paintedPixels: state.paintedPixels, - lastPosition: state.lastPosition, - startPosition: state.startPosition, - region: state.region, - imageLoaded: state.imageLoaded, - colorsChecked: state.colorsChecked, - availableColors: state.availableColors, - }, - imageData: state.imageData - ? { - width: state.imageData.width, - height: state.imageData.height, - pixels: Array.from(state.imageData.pixels), - totalPixels: state.imageData.totalPixels, - } - : null, - paintedMap: state.paintedMap ? state.paintedMap.map((row) => Array.from(row)) : null, - } + // 3. If everything fails — you can return null or [0,0,0,0] + // Prefer null — to avoid misleading + return null; + } - const filename = `wplace-bot-progress-${new Date().toISOString().slice(0, 19).replace(/:/g, "-")}.json` - Utils.createFileDownloader(JSON.stringify(progressData, null, 2), filename) - return true - } catch (error) { - console.error("Error saving to file:", error) - return false - } - }, + async _compositeTileOptimized(originalBlob, overlayBitmap) { + const originalBitmap = await createImageBitmap(originalBlob); + const canvas = new OffscreenCanvas(originalBitmap.width, originalBitmap.height); + const ctx = canvas.getContext('2d'); - loadProgressFromFile: async () => { - try { - const data = await Utils.createFileUploader() + // Disable antialiasing for pixel-perfect rendering + ctx.imageSmoothingEnabled = false; - if (!data.version || !data.state) { - throw new Error("Invalid file format") - } + // Draw original tile first + ctx.drawImage(originalBitmap, 0, 0); - const success = Utils.restoreProgress(data) - return success - } catch (error) { - console.error("Error loading from file:", error) - throw error - } - }, - } + // Set opacity and draw overlay with optimized blend mode + ctx.globalAlpha = state.overlayOpacity; + ctx.globalCompositeOperation = 'source-over'; + ctx.drawImage(overlayBitmap, 0, 0); - // IMAGE PROCESSOR CLASS - class ImageProcessor { - constructor(imageSrc) { - this.imageSrc = imageSrc - this.img = null - this.canvas = null - this.ctx = null + // Use faster blob conversion with compression settings + return await canvas.convertToBlob({ + type: 'image/png', + quality: 0.95, // Slight compression for faster processing + }); } - async load() { - return new Promise((resolve, reject) => { - this.img = new Image() - this.img.crossOrigin = "anonymous" - this.img.onload = () => { - this.canvas = document.createElement("canvas") - this.ctx = this.canvas.getContext("2d") - this.canvas.width = this.img.width - this.canvas.height = this.img.height - this.ctx.drawImage(this.img, 0, 0) - resolve() + /** + * Wait until all required tiles are loaded and cached + * @param {number} startRegionX + * @param {number} startRegionY + * @param {number} pixelWidth + * @param {number} pixelHeight + * @param {number} startPixelX + * @param {number} startPixelY + * @param {number} timeoutMs + * @returns {Promise} true if tiles are ready + */ + async waitForTiles( + startRegionX, + startRegionY, + pixelWidth, + pixelHeight, + startPixelX = 0, + startPixelY = 0, + timeoutMs = 10000 + ) { + const { startTileX, startTileY, endTileX, endTileY } = Utils.calculateTileRange( + startRegionX, + startRegionY, + startPixelX, + startPixelY, + pixelWidth, + pixelHeight, + this.tileSize + ); + + const requiredTiles = []; + for (let ty = startTileY; ty <= endTileY; ty++) { + for (let tx = startTileX; tx <= endTileX; tx++) { + requiredTiles.push(`${tx},${ty}`); } - this.img.onerror = reject - this.img.src = this.imageSrc - }) - } + } - getDimensions() { - return { - width: this.canvas.width, - height: this.canvas.height, + if (requiredTiles.length === 0) return true; + + const startTime = Date.now(); + + while (Date.now() - startTime < timeoutMs) { + if (state.stopFlag) { + console.log('waitForTiles: stopped by user'); + return false; + } + + const missing = requiredTiles.filter((key) => !this.originalTiles.has(key)); + if (missing.length === 0) { + console.log(`✅ All ${requiredTiles.length} required tiles are loaded`); + return true; + } + + await Utils.sleep(100); } + + console.warn(`❌ Timeout waiting for tiles: ${requiredTiles.length} required, + ${requiredTiles.filter((k) => this.originalTiles.has(k)).length} loaded`); + return false; } + } - getPixelData() { - return this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height).data + const overlayManager = new OverlayManager(); + + // Optimized Turnstile token handling with improved caching and retry logic + let turnstileToken = null; + let tokenExpiryTime = 0; + let tokenGenerationInProgress = false; + let _resolveToken = null; + let tokenPromise = new Promise((resolve) => { + _resolveToken = resolve; + }); + let retryCount = 0; + const MAX_RETRIES = 10; + const MAX_BATCH_RETRIES = 10; // Maximum attempts for batch sending + const TOKEN_LIFETIME = 240000; // 4 minutes (tokens typically last 5 min, use 4 for safety) + + function setTurnstileToken(token) { + if (_resolveToken) { + _resolveToken(token); + _resolveToken = null; } + turnstileToken = token; + tokenExpiryTime = Date.now() + TOKEN_LIFETIME; + console.log('✅ Turnstile token set successfully'); + } - resize(newWidth, newHeight) { - const tempCanvas = document.createElement("canvas") - const tempCtx = tempCanvas.getContext("2d") + function isTokenValid() { + return turnstileToken && Date.now() < tokenExpiryTime; + } - tempCanvas.width = newWidth - tempCanvas.height = newHeight + function invalidateToken() { + turnstileToken = null; + tokenExpiryTime = 0; + console.log('🗑️ Token invalidated, will force fresh generation'); + } - tempCtx.imageSmoothingEnabled = false - tempCtx.drawImage(this.canvas, 0, 0, newWidth, newHeight) + async function ensureToken(forceRefresh = false) { + // Return cached token if still valid and not forcing refresh + if (isTokenValid() && !forceRefresh) { + return turnstileToken; + } - this.canvas.width = newWidth - this.canvas.height = newHeight - this.ctx.imageSmoothingEnabled = false - this.ctx.drawImage(tempCanvas, 0, 0) + // Invalidate token if forcing refresh + if (forceRefresh) invalidateToken(); - return this.ctx.getImageData(0, 0, newWidth, newHeight).data + // Avoid multiple simultaneous token generations + if (tokenGenerationInProgress) { + console.log('🔄 Token generation already in progress, waiting...'); + await Utils.sleep(2000); + return isTokenValid() ? turnstileToken : null; } - generatePreview(width, height) { - const previewCanvas = document.createElement("canvas") - const previewCtx = previewCanvas.getContext("2d") + tokenGenerationInProgress = true; - previewCanvas.width = width - previewCanvas.height = height + try { + console.log('🔄 Token expired or missing, generating new one...'); + const token = await handleCaptchaWithRetry(); + if (token && token.length > 20) { + setTurnstileToken(token); + console.log('✅ Token captured and cached successfully'); + return token; + } - previewCtx.imageSmoothingEnabled = false - previewCtx.drawImage(this.img, 0, 0, width, height) + console.log('⚠️ Invisible Turnstile failed, forcing browser automation...'); + const fallbackToken = await handleCaptchaFallback(); + if (fallbackToken && fallbackToken.length > 20) { + setTurnstileToken(fallbackToken); + console.log('✅ Fallback token captured successfully'); + return fallbackToken; + } - return previewCanvas.toDataURL() + console.log('❌ All token generation methods failed'); + return null; + } finally { + tokenGenerationInProgress = false; } } - // WPLACE API SERVICE - const WPlaceService = { - async paintPixelInRegion(regionX, regionY, pixelX, pixelY, color) { - try { - // Construct the payload including the captured 't' token. - const payload = { - coords: [pixelX, pixelY], - colors: [color], - t: capturedCaptchaToken, - } - const res = await fetch(`https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, { - method: "POST", - headers: { "Content-Type": "text/plain;charset=UTF-8" }, - credentials: "include", - body: JSON.stringify(payload), - }) + async function handleCaptchaWithRetry() { + const startTime = performance.now(); - // If we get a 403 Forbidden error, our token is likely expired. - if (res.status === 403) { - console.error("❌ 403 Forbidden. CAPTCHA token might be invalid or expired.") - capturedCaptchaToken = null // Invalidate our stored token. - return "token_error" // Return a special status to stop the bot. - } + try { + const { sitekey, token: preGeneratedToken } = await Utils.obtainSitekeyAndToken(); - const data = await res.json() - return data?.painted === 1 - } catch (e) { - console.error("Paint request failed:", e) - return false + if (!sitekey) { + throw new Error('No valid sitekey found'); } - }, - async getCharges() { - try { - const res = await fetch("https://backend.wplace.live/me", { - credentials: "include", - }) - const data = await res.json() - return { - charges: data.charges?.count || 0, - cooldown: data.charges?.next || CONFIG.COOLDOWN_DEFAULT, - } - } catch (e) { - console.error("Failed to get charges:", e) - return { - charges: 0, - cooldown: CONFIG.COOLDOWN_DEFAULT, + console.log('🔑 Using sitekey:', sitekey); + + if (typeof window !== 'undefined' && window.navigator) { + console.log( + '🧭 UA:', + window.navigator.userAgent.substring(0, 50) + '...', + 'Platform:', + window.navigator.platform + ); + } + + let token = null; + + if ( + preGeneratedToken && + typeof preGeneratedToken === 'string' && + preGeneratedToken.length > 20 + ) { + console.log('♻️ Reusing pre-generated Turnstile token'); + token = preGeneratedToken; + } else { + if (isTokenValid()) { + console.log('♻️ Using existing cached token (from previous session)'); + token = turnstileToken; + } else { + console.log('🔐 Generating new token with executeTurnstile...'); + token = await Utils.executeTurnstile(sitekey, 'paint'); + if (token) setTurnstileToken(token); } } - }, - } - - // COLOR MATCHING FUNCTION - function findClosestColor(targetRgb, availableColors) { - let minDistance = Number.POSITIVE_INFINITY - let closestColorId = availableColors[0]?.id || 1 - for (const color of availableColors) { - const distance = Utils.colorDistance(targetRgb, color.rgb) - if (distance < minDistance) { - minDistance = distance - closestColorId = color.id + if (token && typeof token === 'string' && token.length > 20) { + const elapsed = Math.round(performance.now() - startTime); + console.log(`✅ Turnstile token generated successfully in ${elapsed}ms`); + return token; + } else { + throw new Error(`Invalid or empty token received - Length: ${token?.length || 0}`); } + } catch (error) { + const elapsed = Math.round(performance.now() - startTime); + console.error(`❌ Turnstile token generation failed after ${elapsed}ms:`, error); + throw error; } + } - return closestColorId + async function handleCaptchaFallback() { + // Implementation for fallback token generation would go here + // This is a placeholder for browser automation fallback + console.log('🔄 Attempting fallback token generation...'); + return null; } - // UI UPDATE FUNCTIONS (declared early to avoid reference errors) - let updateUI = () => {} - let updateStats = () => {} - let updateDataButtons = () => {} - - const createThemePopup = () => { - // Remove existing popup if it exists - const existingPopup = document.getElementById("theme-popup") - if (existingPopup) { - existingPopup.remove() - return - } - - const popup = document.createElement("div") - popup.id = "theme-popup" - popup.style.cssText = ` - position: fixed; - top: 60px; - right: 20px; - background: ${getCurrentTheme().secondary}; - border: ${getCurrentTheme().borderWidth} ${getCurrentTheme().borderStyle} ${getCurrentTheme().accent}; - border-radius: ${getCurrentTheme().borderRadius}; - box-shadow: ${getCurrentTheme().boxShadow}; - backdrop-filter: ${getCurrentTheme().backdropFilter}; - z-index: 10001; - min-width: 200px; - padding: 10px 0; - font-family: ${getCurrentTheme().fontFamily}; - ` - - Object.keys(CONFIG.THEMES).forEach((themeName) => { - const themeOption = document.createElement("div") - themeOption.style.cssText = ` - padding: 10px 20px; - color: ${themeName === CONFIG.currentTheme ? getCurrentTheme().highlight : getCurrentTheme().text}; - cursor: pointer; - font-size: 12px; - transition: all 0.2s ease; - background: ${themeName === CONFIG.currentTheme ? getCurrentTheme().accent : "transparent"}; - ` - themeOption.textContent = themeName + function inject(callback) { + const script = document.createElement('script'); + script.textContent = `(${callback})();`; + document.documentElement?.appendChild(script); + script.remove(); + } - themeOption.addEventListener("mouseenter", () => { - if (themeName !== CONFIG.currentTheme) { - themeOption.style.background = getCurrentTheme().accent - themeOption.style.color = getCurrentTheme().highlight + inject(() => { + const fetchedBlobQueue = new Map(); + + window.addEventListener('message', (event) => { + const { source, blobID, blobData } = event.data; + if (source === 'auto-image-overlay' && blobID && blobData) { + const callback = fetchedBlobQueue.get(blobID); + if (typeof callback === 'function') { + callback(blobData); + } + fetchedBlobQueue.delete(blobID); + } + }); + + const originalFetch = window.fetch; + window.fetch = async function (...args) { + const response = await originalFetch.apply(this, args); + const url = args[0] instanceof Request ? args[0].url : args[0]; + + if (typeof url === 'string') { + if (url.includes('https://backend.wplace.live/s0/pixel/')) { + try { + const payload = JSON.parse(args[1].body); + if (payload.t) { + // 📊 Debug log + console.log( + `🔍✅ Turnstile Token Captured - Type: ${typeof payload.t}, Value: ${ + payload.t + ? typeof payload.t === 'string' + ? payload.t.length > 50 + ? payload.t.substring(0, 50) + '...' + : payload.t + : JSON.stringify(payload.t) + : 'null/undefined' + }, Length: ${payload.t?.length || 0}` + ); + window.postMessage({ source: 'turnstile-capture', token: payload.t }, '*'); + } + } catch (_) { + /* ignore */ + } } - }) - themeOption.addEventListener("mouseleave", () => { - if (themeName !== CONFIG.currentTheme) { - themeOption.style.background = "transparent" - themeOption.style.color = getCurrentTheme().text + const contentType = response.headers.get('content-type') || ''; + if (contentType.includes('image/png') && url.includes('.png')) { + const cloned = response.clone(); + return new Promise(async (resolve) => { + const blobUUID = crypto.randomUUID(); + const originalBlob = await cloned.blob(); + + fetchedBlobQueue.set(blobUUID, (processedBlob) => { + resolve( + new Response(processedBlob, { + headers: cloned.headers, + status: cloned.status, + statusText: cloned.statusText, + }) + ); + }); + + window.postMessage( + { + source: 'auto-image-tile', + endpoint: url, + blobID: blobUUID, + blobData: originalBlob, + }, + '*' + ); + }); } - }) + } - themeOption.addEventListener("click", () => { - switchTheme(themeName) - popup.remove() - }) + return response; + }; + }); - popup.appendChild(themeOption) - }) + window.addEventListener('message', (event) => { + const { source, endpoint, blobID, blobData, token } = event.data; - document.body.appendChild(popup) + if (source === 'auto-image-tile' && endpoint && blobID && blobData) { + overlayManager.processAndRespondToTileRequest(event.data); + } - // Close popup when clicking outside - const closePopup = (e) => { - if (!popup.contains(e.target) && !e.target.closest("#themeBtn")) { - popup.remove() - document.removeEventListener("click", closePopup) + if (source === 'turnstile-capture' && token) { + setTurnstileToken(token); + if (document.querySelector('#statusText')?.textContent.includes('CAPTCHA')) { + Utils.showAlert(Utils.t('tokenCapturedSuccess'), 'success'); + updateUI('colorsFound', 'success', { count: state.availableColors.length }); } } + }); - setTimeout(() => { - document.addEventListener("click", closePopup) - }, 100) + 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'; + } } - async function createUI() { - await detectLanguage() + // UTILITY FUNCTIONS + const Utils = { + sleep: (ms) => new Promise((r) => setTimeout(r, ms)), - loadThemePreference() + dynamicSleep: async function (tickAndGetRemainingMs) { + let remaining = Math.max(0, await tickAndGetRemainingMs()); + while (remaining > 0) { + const interval = remaining > 5000 ? 2000 : remaining > 1000 ? 500 : 100; + await this.sleep(Math.min(interval, remaining)); + remaining = Math.max(0, await tickAndGetRemainingMs()); + } + }, - const theme = getCurrentTheme() + waitForSelector: async (selector, interval = 200, timeout = 5000) => { + const start = Date.now(); + while (Date.now() - start < timeout) { + const el = document.querySelector(selector); + if (el) return el; + await Utils.sleep(interval); + } + return null; + }, - const fontAwesome = document.createElement("link") - fontAwesome.rel = "stylesheet" - fontAwesome.href = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" - document.head.appendChild(fontAwesome) + msToTimeText(ms) { + const totalSeconds = Math.ceil(ms / 1000); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; - if (theme.fontFamily.includes("Press Start 2P")) { - const googleFonts = document.createElement("link") - googleFonts.rel = "stylesheet" - googleFonts.href = "https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" - document.head.appendChild(googleFonts) - } + if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`; + if (minutes > 0) return `${minutes}m ${seconds}s`; + return `${seconds}s`; + }, - const style = document.createElement("style") - style.setAttribute("data-wplace-theme", "true") + // Debounced scroll-to-adjust handler for sliders + createScrollToAdjust: (element, updateCallback, min, max, step = 1) => { + let debounceTimer = null; + + const handleWheel = (e) => { + // Only trigger when hovering over the slider + if (e.target !== element) return; + + e.preventDefault(); + e.stopPropagation(); + + // Clear existing debounce timer + if (debounceTimer) { + clearTimeout(debounceTimer); + } + + // Debounce the adjustment to make it precise + debounceTimer = setTimeout(() => { + const currentValue = parseInt(element.value) || 0; + const delta = e.deltaY > 0 ? -step : step; + const newValue = Math.max(min, Math.min(max, currentValue + delta)); + + if (newValue !== currentValue) { + element.value = newValue; + updateCallback(newValue); + } + }, 50); // 50ms debounce for precise control + }; + + element.addEventListener('wheel', handleWheel, { passive: false }); + + // Return cleanup function + return () => { + if (debounceTimer) clearTimeout(debounceTimer); + element.removeEventListener('wheel', handleWheel); + }; + }, - style.textContent = ` - ${ - theme.animations.glow - ? ` - @keyframes neonGlow { - 0%, 100% { - text-shadow: 0 0 5px currentColor, 0 0 10px currentColor, 0 0 15px currentColor; + /** + * Calculate the range of tile coordinates (in region space) that cover a given image area. + * @param {number} startRegionX - Base region X + * @param {number} startRegionY - Base region Y + * @param {number} startPixelX - Starting pixel X within the region grid + * @param {number} startPixelY - Starting pixel Y within the region grid + * @param {number} width - Image width in pixels + * @param {number} height - Image height in pixels + * @param {number} tileSize - Size of a tile (default 1000) + * @returns {{ startTileX: number, startTileY: number, endTileX: number, endTileY: number }} + */ + calculateTileRange( + startRegionX, + startRegionY, + startPixelX, + startPixelY, + width, + height, + tileSize = 1000 + ) { + const endPixelX = startPixelX + width; + const endPixelY = startPixelY + height; + + return { + startTileX: startRegionX + Math.floor(startPixelX / tileSize), + startTileY: startRegionY + Math.floor(startPixelY / tileSize), + endTileX: startRegionX + Math.floor((endPixelX - 1) / tileSize), + endTileY: startRegionY + Math.floor((endPixelY - 1) / tileSize), + }; + }, // Turnstile Generator Integration - Optimized with widget reuse and proper cleanup + turnstileLoaded: false, + _turnstileContainer: null, + _turnstileOverlay: null, + _turnstileWidgetId: null, + _lastSitekey: null, + + async loadTurnstile() { + // If Turnstile is already present, just resolve. + if (window.turnstile) { + 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"]' + ) + ) { + const checkReady = () => { + if (window.turnstile) { + this.turnstileLoaded = true; + resolve(); + } else { + setTimeout(checkReady, 100); + } + }; + return checkReady(); } - 50% { - text-shadow: 0 0 2px currentColor, 0 0 5px currentColor, 0 0 8px currentColor; + + const script = document.createElement('script'); + script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; + script.async = true; + script.defer = true; + script.onload = () => { + this.turnstileLoaded = true; + console.log('✅ Turnstile script loaded successfully'); + resolve(); + }; + script.onerror = () => { + console.error('❌ Failed to load Turnstile script'); + reject(new Error('Failed to load Turnstile')); + }; + document.head.appendChild(script); + }); + }, + + // Create or reuse the turnstile container - completely hidden for token generation + ensureTurnstileContainer() { + if (!this._turnstileContainer || !document.body.contains(this._turnstileContainer)) { + // Clean up old container if it exists + if (this._turnstileContainer) { + this._turnstileContainer.remove(); } - }` - : "" + + this._turnstileContainer = document.createElement('div'); + this._turnstileContainer.className = 'wplace-turnstile-hidden'; + this._turnstileContainer.setAttribute('aria-hidden', 'true'); + this._turnstileContainer.id = 'turnstile-widget-container'; + document.body.appendChild(this._turnstileContainer); } - - ${ - theme.animations.pixelBlink - ? ` - @keyframes pixelBlink { - 0%, 50% { opacity: 1; } - 51%, 100% { opacity: 0.7; } - }` - : "" + return this._turnstileContainer; + }, + + // Interactive overlay container for visible widgets when needed + ensureTurnstileOverlayContainer() { + if (this._turnstileOverlay && document.body.contains(this._turnstileOverlay)) { + return this._turnstileOverlay; } - - ${ - theme.animations.scanline - ? ` - @keyframes scanline { - 0% { transform: translateY(-100%); } - 100% { transform: translateY(400px); } - }` - : "" + + const overlay = document.createElement('div'); + overlay.id = 'turnstile-overlay-container'; + overlay.className = 'wplace-turnstile-overlay wplace-overlay-hidden'; + + const title = document.createElement('div'); + title.textContent = Utils.t('turnstileInstructions'); + title.className = 'wplace-turnstile-title'; + + const host = document.createElement('div'); + host.id = 'turnstile-overlay-host'; + host.className = 'wplace-turnstile-host'; + + const hideBtn = document.createElement('button'); + hideBtn.textContent = Utils.t('hideTurnstileBtn'); + hideBtn.className = 'wplace-turnstile-hide-btn'; + hideBtn.addEventListener('click', () => overlay.remove()); + + overlay.appendChild(title); + overlay.appendChild(host); + overlay.appendChild(hideBtn); + document.body.appendChild(overlay); + + this._turnstileOverlay = overlay; + return overlay; + }, + + async executeTurnstile(sitekey, action = 'paint') { + await this.loadTurnstile(); + + // Try reusing existing widget first if sitekey matches + if (this._turnstileWidgetId && this._lastSitekey === sitekey && window.turnstile?.execute) { + try { + console.log('🔄 Reusing existing Turnstile widget...'); + const token = await Promise.race([ + window.turnstile.execute(this._turnstileWidgetId, { action }), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Execute timeout')), 15000) + ), + ]); + if (token && token.length > 20) { + console.log('✅ Token generated via widget reuse'); + return token; + } + } catch (error) { + console.log('� Widget reuse failed, will create a fresh widget:', error.message); + } } - - @keyframes pulse { - 0% { box-shadow: 0 0 0 0 rgba(0, 255, 0, 0.7); } - 70% { box-shadow: 0 0 0 10px rgba(0, 255, 0, 0); } - 100% { box-shadow: 0 0 0 0 rgba(0, 255, 0, 0); } + + // Try invisible widget first + const invisibleToken = await this.createTurnstileWidget(sitekey, action); + if (invisibleToken && invisibleToken.length > 20) { + return invisibleToken; } - @keyframes slideIn { - from { transform: translateY(-10px); opacity: 0; } - to { transform: translateY(0); opacity: 1; } + + console.log('� Falling back to interactive Turnstile (visible).'); + return await this.createTurnstileWidgetInteractive(sitekey, action); + }, + + async createTurnstileWidget(sitekey, action) { + return new Promise((resolve) => { + try { + // Force cleanup of any existing widget + if (this._turnstileWidgetId && window.turnstile?.remove) { + 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, + action, + size: 'invisible', + retry: 'auto', + 'retry-interval': 8000, + callback: (token) => { + console.log('✅ Invisible Turnstile callback'); + resolve(token); + }, + 'error-callback': () => resolve(null), + 'timeout-callback': () => resolve(null), + }); + + this._turnstileWidgetId = widgetId; + this._lastSitekey = sitekey; + + if (!widgetId) { + return resolve(null); + } + + // Execute the widget and race with timeout + Promise.race([ + window.turnstile.execute(widgetId, { action }), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Invisible execute timeout')), 12000) + ), + ]) + .then(resolve) + .catch(() => resolve(null)); + } catch (e) { + console.error('❌ Invisible Turnstile creation failed:', e); + resolve(null); + } + }); + }, + + 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); + } 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, + size: 'normal', + theme: 'light', + callback: (token) => { + clearTimeout(timeout); + 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 { + console.warn('❌ Invalid token from interactive widget'); + resolve(null); + } + }, + 'error-callback': (error) => { + clearTimeout(timeout); + 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); + } else { + console.log('✅ Interactive Turnstile widget created, waiting for user interaction...'); + } + } catch (e) { + console.error('❌ Interactive Turnstile creation failed:', e); + resolve(null); + } + }); + }, + + // Cleanup method for when the script is disabled/reloaded + cleanupTurnstile() { + if (this._turnstileWidgetId && window.turnstile?.remove) { + try { + window.turnstile.remove(this._turnstileWidgetId); + } catch (e) { + console.warn('Failed to cleanup Turnstile widget:', e); + } } - @keyframes shimmer { - 0% { transform: translateX(-100%); } - 100% { transform: translateX(100%); } + + if (this._turnstileContainer && document.body.contains(this._turnstileContainer)) { + this._turnstileContainer.remove(); } - - #wplace-image-bot-container { - position: fixed; - top: 20px; - right: 20px; - width: ${CONFIG.currentTheme === "Neon Retro" ? "320px" : "280px"}; - max-height: calc(100vh - 40px); - background: ${ - CONFIG.currentTheme === "Classic Autobot" - ? `linear-gradient(135deg, ${theme.primary} 0%, #1a1a1a 100%)` - : theme.primary - }; - border: ${theme.borderWidth} ${theme.borderStyle} ${CONFIG.currentTheme === "Classic Autobot" ? theme.accent : theme.text}; - border-radius: ${theme.borderRadius}; - padding: 0; - box-shadow: ${theme.boxShadow}; - z-index: 9998; - font-family: ${theme.fontFamily}; - color: ${theme.text}; - animation: slideIn 0.4s ease-out; - overflow: hidden; - ${theme.backdropFilter ? `backdrop-filter: ${theme.backdropFilter};` : ""} - transition: all 0.3s ease; - user-select: none; - ${CONFIG.currentTheme === "Neon Retro" ? "image-rendering: pixelated;" : ""} + + if (this._turnstileOverlay && document.body.contains(this._turnstileOverlay)) { + this._turnstileOverlay.remove(); } - - ${ - theme.animations.scanline - ? ` - #wplace-image-bot-container::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 2px; - background: linear-gradient(90deg, transparent, ${theme.neon}, transparent); - animation: scanline 3s linear infinite; - z-index: 1; - pointer-events: none; - }` - : "" + + this._turnstileWidgetId = null; + this._turnstileContainer = null; + this._turnstileOverlay = null; + this._lastSitekey = null; + }, + + async obtainSitekeyAndToken(fallback = '0x4AAAAAABpqJe8FO0N84q0F') { + // Cache sitekey to avoid repeated DOM queries + if (this._cachedSitekey) { + console.log('🔍 Using cached sitekey:', this._cachedSitekey); + + return isTokenValid() + ? { + sitekey: this._cachedSitekey, + token: turnstileToken, + } + : { sitekey: this._cachedSitekey, token: null }; } - - ${ - CONFIG.currentTheme === "Neon Retro" - ? ` - #wplace-image-bot-container::after { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: - repeating-linear-gradient( - 0deg, - transparent, - transparent 2px, - rgba(0, 255, 65, 0.03) 2px, - rgba(0, 255, 65, 0.03) 4px + + // List of potential sitekeys to try + const potentialSitekeys = [ + '0x4AAAAAABpqJe8FO0N84q0F', // WPlace common sitekey + '0x4AAAAAAAJ7xjKAp6Mt_7zw', // Alternative WPlace sitekey + '0x4AAAAAADm5QWx6Ov2LNF2g', // Another common sitekey + ]; + const trySitekey = async (sitekey, source) => { + if (!sitekey || sitekey.length < 10) return null; + + console.log(`🔍 Testing sitekey from ${source}:`, sitekey); + const token = await this.executeTurnstile(sitekey); + + if (token && token.length >= 20) { + console.log(`✅ Valid token generated from ${source} sitekey`); + setTurnstileToken(token); + this._cachedSitekey = sitekey; + return { sitekey, token }; + } else { + console.log(`❌ Failed to get token from ${source} sitekey`); + return null; + } + }; + + try { + // 1️⃣ data-sitekey attribute + const sitekeySel = document.querySelector('[data-sitekey]'); + if (sitekeySel) { + const sitekey = sitekeySel.getAttribute('data-sitekey'); + const result = await trySitekey(sitekey, 'data attribute'); + if (result) { + return result; + } + } + + // 2️⃣ Turnstile element + const turnstileEl = document.querySelector('.cf-turnstile'); + if (turnstileEl?.dataset?.sitekey) { + const sitekey = turnstileEl.dataset.sitekey; + const result = await trySitekey(sitekey, 'turnstile element'); + if (result) { + return result; + } + } + + // 3️⃣ Meta tags + const metaTags = document.querySelectorAll( + 'meta[name*="turnstile"], meta[property*="turnstile"]' + ); + for (const meta of metaTags) { + const content = meta.getAttribute('content'); + const result = await trySitekey(content, 'meta tag'); + if (result) { + return result; + } + } + + // 4️⃣ Global variable + if (window.__TURNSTILE_SITEKEY) { + const result = await trySitekey(window.__TURNSTILE_SITEKEY, 'global variable'); + if (result) { + return result; + } + } + + // 5️⃣ Script tags + const scripts = document.querySelectorAll('script'); + for (const script of scripts) { + const content = script.textContent || script.innerHTML; + const match = content.match( + /(?:sitekey|data-sitekey)['"\s\[\]:\=\(]*['"]?([0-9a-zA-Z_-]{20,})['"]?/i ); - pointer-events: none; - z-index: 1; - }` - : "" + if (match && match[1]) { + const extracted = match[1].replace(/['"]/g, ''); + const result = await trySitekey(extracted, 'script content'); + if (result) { + return result; + } + } + } + + // 6️⃣ Known potential sitekeys + console.log('🔍 Testing known potential sitekeys...'); + for (const testSitekey of potentialSitekeys) { + const result = await trySitekey(testSitekey, 'known list'); + if (result) { + return result; + } + } + } catch (error) { + console.warn('⚠️ Error during sitekey detection:', error); } - - #wplace-image-bot-container.wplace-dragging { - transition: none; - box-shadow: 0 12px 40px rgba(0,0,0,0.8), 0 0 0 2px rgba(255,255,255,0.2); - transform: scale(1.02); - z-index: 9999; + + // 7️⃣ Fallback + console.log('🔧 Trying fallback sitekey:', fallback); + const fallbackResult = await trySitekey(fallback, 'fallback'); + if (fallbackResult) { + return fallbackResult; + } + + console.error('❌ No working sitekey or token found.'); + return { sitekey: null, token: null }; + }, + + createElement: (tag, props = {}, children = []) => { + const element = document.createElement(tag); + + Object.entries(props).forEach(([key, value]) => { + if (key === 'style' && typeof value === 'object') { + Object.assign(element.style, value); + } else if (key === 'className') { + element.className = value; + } else if (key === 'innerHTML') { + element.innerHTML = value; + } else { + element.setAttribute(key, value); + } + }); + + if (typeof children === 'string') { + element.textContent = children; + } else if (Array.isArray(children)) { + children.forEach((child) => { + if (typeof child === 'string') { + element.appendChild(document.createTextNode(child)); + } else { + element.appendChild(child); + } + }); + } + + return element; + }, + + createButton: (id, text, icon, onClick, style = CONFIG.CSS_CLASSES.BUTTON_PRIMARY) => { + const button = Utils.createElement('button', { + id: id, + style: style, + innerHTML: `${icon ? `` : ''}${text}`, + }); + if (onClick) button.addEventListener('click', onClick); + return button; + }, + + // Synchronous translation function for UI rendering + t: (key, params = {}) => { + // Try to get from cache first + const cacheKey = `${state.language}_${key}`; + if (translationCache.has(cacheKey)) { + let text = translationCache.get(cacheKey); + Object.keys(params).forEach((param) => { + text = text.replace(`{${param}}`, params[param]); + }); + return text; } - #wplace-image-bot-container.wplace-minimized { - width: 200px; - height: auto; + + // Try dynamically loaded translations (already loaded) + if (loadedTranslations[state.language]?.[key]) { + let text = loadedTranslations[state.language][key]; + // Cache for future use + translationCache.set(cacheKey, text); + Object.keys(params).forEach((param) => { + text = text.replace(`{${param}}`, params[param]); + }); + return text; } - #wplace-image-bot-container.wplace-compact { - width: 240px; + + // Fallback to English if current language failed + if (state.language !== 'en' && loadedTranslations['en']?.[key]) { + let text = loadedTranslations['en'][key]; + Object.keys(params).forEach((param) => { + text = text.replace(`{${param}}`, params[param]); + }); + return text; } - - /* Stats Container */ - #wplace-stats-container { - position: fixed; - top: 20px; - left: 20px; - width: ${CONFIG.currentTheme === "Neon Retro" ? "320px" : "280px"}; - max-height: calc(100vh - 40px); - background: ${ - CONFIG.currentTheme === "Classic Autobot" - ? `linear-gradient(135deg, ${theme.primary} 0%, #1a1a1a 100%)` - : theme.primary + + // Final fallback to emergency fallback or key + let text = FALLBACK_TEXT[state.language]?.[key] || FALLBACK_TEXT.en?.[key] || key; + Object.keys(params).forEach((param) => { + text = text.replace(new RegExp(`\\{${param}\\}`, 'g'), params[param]); + }); + + // Log missing translations for debugging + if (text === key && key !== 'undefined') { + console.warn(`⚠️ Missing translation for key: ${key} (language: ${state.language})`); + } + + return text; + }, + + showAlert: (message, type = 'info') => { + const alertDiv = document.createElement('div'); + alertDiv.className = `wplace-alert-base wplace-alert-${type}`; + + alertDiv.textContent = message; + document.body.appendChild(alertDiv); + + setTimeout(() => { + alertDiv.style.animation = 'slide-down 0.3s ease-out reverse'; + setTimeout(() => { + document.body.removeChild(alertDiv); + }, 300); + }, 4000); + }, + + colorDistance: (a, b) => + Math.sqrt(Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2) + Math.pow(a[2] - b[2], 2)), + _labCache: new Map(), // key: (r<<16)|(g<<8)|b value: [L,a,b] + _rgbToLab: (r, g, b) => { + // sRGB -> linear + const srgbToLinear = (v) => { + v /= 255; + return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); + }; + const rl = srgbToLinear(r); + const gl = srgbToLinear(g); + const bl = srgbToLinear(b); + let X = rl * 0.4124 + gl * 0.3576 + bl * 0.1805; + let Y = rl * 0.2126 + gl * 0.7152 + bl * 0.0722; + let Z = rl * 0.0193 + gl * 0.1192 + bl * 0.9505; + X /= 0.95047; + Y /= 1.0; + Z /= 1.08883; + const f = (t) => (t > 0.008856 ? Math.cbrt(t) : 7.787 * t + 16 / 116); + const fX = f(X), + fY = f(Y), + fZ = f(Z); + const L = 116 * fY - 16; + const a = 500 * (fX - fY); + const b2 = 200 * (fY - fZ); + return [L, a, b2]; + }, + _lab: (r, g, b) => { + const key = (r << 16) | (g << 8) | b; + let v = Utils._labCache.get(key); + if (!v) { + v = Utils._rgbToLab(r, g, b); + Utils._labCache.set(key, v); + } + return v; + }, + findClosestPaletteColor: (r, g, b, palette) => { + // Use provided palette or derive from COLOR_MAP + if (!palette || palette.length === 0) { + palette = Object.values(CONFIG.COLOR_MAP) + .filter((c) => c.rgb) + .map((c) => [c.rgb.r, c.rgb.g, c.rgb.b]); + } + if (state.colorMatchingAlgorithm === 'legacy') { + let menorDist = Infinity; + let cor = [0, 0, 0]; + for (let i = 0; i < palette.length; i++) { + const [pr, pg, pb] = palette[i]; + const rmean = (pr + r) / 2; + const rdiff = pr - r; + const gdiff = pg - g; + const bdiff = pb - b; + const dist = Math.sqrt( + (((512 + rmean) * rdiff * rdiff) >> 8) + + 4 * gdiff * gdiff + + (((767 - rmean) * bdiff * bdiff) >> 8) + ); + if (dist < menorDist) { + menorDist = dist; + cor = [pr, pg, pb]; + } + } + return cor; + } + // LAB algorithm + const [Lt, at, bt] = Utils._lab(r, g, b); + const targetChroma = Math.sqrt(at * at + bt * bt); + let best = null; + let bestDist = Infinity; + for (let i = 0; i < palette.length; i++) { + const [pr, pg, pb] = palette[i]; + const [Lp, ap, bp] = Utils._lab(pr, pg, pb); + const dL = Lt - Lp; + const da = at - ap; + const db = bt - bp; + let dist = dL * dL + da * da + db * db; + if (state.enableChromaPenalty && targetChroma > 20) { + const candChroma = Math.sqrt(ap * ap + bp * bp); + if (candChroma < targetChroma) { + const chromaDiff = targetChroma - candChroma; + dist += chromaDiff * chromaDiff * state.chromaPenaltyWeight; + } + } + if (dist < bestDist) { + bestDist = dist; + best = palette[i]; + if (bestDist === 0) break; + } + } + return best || [0, 0, 0]; + }, + + isWhitePixel: (r, g, b) => { + const wt = state.customWhiteThreshold || CONFIG.WHITE_THRESHOLD; + return r >= wt && g >= wt && b >= wt; + }, + + resolveColor(targetRgb, availableColors, exactMatch = false) { + if (!availableColors || availableColors.length === 0) { + return { + id: null, + rgb: targetRgb, }; - border: ${theme.borderWidth} ${theme.borderStyle} ${CONFIG.currentTheme === "Classic Autobot" ? theme.accent : theme.text}; - border-radius: ${theme.borderRadius}; - padding: 0; - box-shadow: ${theme.boxShadow}; - z-index: 9997; - font-family: ${theme.fontFamily}; - color: ${theme.text}; - animation: slideIn 0.4s ease-out; - overflow: hidden; - ${theme.backdropFilter ? `backdrop-filter: ${theme.backdropFilter};` : ""} - transition: all 0.3s ease; - user-select: none; - ${CONFIG.currentTheme === "Neon Retro" ? "image-rendering: pixelated;" : ""} } - - .wplace-header { - padding: ${CONFIG.currentTheme === "Neon Retro" ? "12px 15px" : "8px 12px"}; - background: ${ - CONFIG.currentTheme === "Classic Autobot" - ? `linear-gradient(135deg, ${theme.secondary} 0%, #2a2a2a 100%)` - : theme.secondary + + const cacheKey = `${targetRgb[0]},${targetRgb[1]},${targetRgb[2]}|${state.colorMatchingAlgorithm}|${ + state.enableChromaPenalty ? 'c' : 'nc' + }|${state.chromaPenaltyWeight}|${exactMatch ? 'exact' : 'closest'}`; + + if (colorCache.has(cacheKey)) return colorCache.get(cacheKey); + + // Check for an exact color match in availableColors. + // If found, return the matched color with its ID. + // If not found, return the target color with null ID. + // Cache the result for future lookups. + if (exactMatch) { + const match = availableColors.find( + (c) => c.rgb[0] === targetRgb[0] && c.rgb[1] === targetRgb[1] && c.rgb[2] === targetRgb[2] + ); + const result = match ? { id: match.id, rgb: [...match.rgb] } : { id: null, rgb: targetRgb }; + colorCache.set(cacheKey, result); + return result; + } + + // check for white using threshold + const whiteThreshold = state.customWhiteThreshold || CONFIG.WHITE_THRESHOLD; + if ( + targetRgb[0] >= whiteThreshold && + targetRgb[1] >= whiteThreshold && + targetRgb[2] >= whiteThreshold + ) { + const whiteEntry = availableColors.find( + (c) => + c.rgb[0] >= whiteThreshold && c.rgb[1] >= whiteThreshold && c.rgb[2] >= whiteThreshold + ); + if (whiteEntry) { + const result = { id: whiteEntry.id, rgb: [...whiteEntry.rgb] }; + colorCache.set(cacheKey, result); + return result; + } + } + + // find nearest color + let bestId = availableColors[0].id; + let bestRgb = [...availableColors[0].rgb]; + let bestScore = Infinity; + + if (state.colorMatchingAlgorithm === 'legacy') { + for (let i = 0; i < availableColors.length; i++) { + const c = availableColors[i]; + const [r, g, b] = c.rgb; + const rmean = (r + targetRgb[0]) / 2; + const rdiff = r - targetRgb[0]; + const gdiff = g - targetRgb[1]; + const bdiff = b - targetRgb[2]; + const dist = Math.sqrt( + (((512 + rmean) * rdiff * rdiff) >> 8) + + 4 * gdiff * gdiff + + (((767 - rmean) * bdiff * bdiff) >> 8) + ); + if (dist < bestScore) { + bestScore = dist; + bestId = c.id; + bestRgb = [...c.rgb]; + if (dist === 0) break; + } + } + } else { + const [Lt, at, bt] = Utils._lab(targetRgb[0], targetRgb[1], targetRgb[2]); + const targetChroma = Math.sqrt(at * at + bt * bt); + const penaltyWeight = state.enableChromaPenalty ? state.chromaPenaltyWeight || 0.15 : 0; + + for (let i = 0; i < availableColors.length; i++) { + const c = availableColors[i]; + const [r, g, b] = c.rgb; + const [L2, a2, b2] = Utils._lab(r, g, b); + const dL = Lt - L2, + da = at - a2, + db = bt - b2; + let dist = dL * dL + da * da + db * db; + + if (penaltyWeight > 0 && targetChroma > 20) { + const candChroma = Math.sqrt(a2 * a2 + b2 * b2); + if (candChroma < targetChroma) { + const cd = targetChroma - candChroma; + dist += cd * cd * penaltyWeight; + } + } + + if (dist < bestScore) { + bestScore = dist; + bestId = c.id; + bestRgb = [...c.rgb]; + if (dist === 0) break; + } + } + } + + const result = { id: bestId, rgb: bestRgb }; + colorCache.set(cacheKey, result); + + // limit the size of the cache + if (colorCache.size > 15000) { + const firstKey = colorCache.keys().next().value; + colorCache.delete(firstKey); + } + + return result; + }, + + createImageUploader: () => + new Promise((resolve) => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'image/png,image/jpeg'; + input.onchange = () => { + const fr = new FileReader(); + fr.onload = () => resolve(fr.result); + fr.readAsDataURL(input.files[0]); }; - color: ${theme.highlight}; - font-size: ${CONFIG.currentTheme === "Neon Retro" ? "10px" : "13px"}; - font-weight: ${CONFIG.currentTheme === "Neon Retro" ? "normal" : "700"}; - display: flex; - justify-content: space-between; - align-items: center; - cursor: move; - user-select: none; - border-bottom: ${CONFIG.currentTheme === "Neon Retro" ? "2px" : "1px"} solid ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(255,255,255,0.1)" : theme.text}; - ${CONFIG.currentTheme === "Classic Autobot" ? "text-shadow: 0 1px 2px rgba(0,0,0,0.5);" : "text-transform: uppercase; letter-spacing: 1px;"} - transition: background 0.2s ease; - position: relative; - z-index: 2; - ${theme.animations.glow ? "animation: neonGlow 2s ease-in-out infinite alternate;" : ""} + input.click(); + }), + + createFileDownloader: (data, filename) => { + const blob = new Blob([data], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }, + + 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')); + } + }; + reader.onerror = () => reject(new Error('File reading error')); + reader.readAsText(file); + } else { + reject(new Error('No file selected')); + } + }; + input.click(); + }), + + extractAvailableColors: () => { + const colorElements = document.querySelectorAll('.tooltip button[id^="color-"]'); + if (colorElements.length === 0) { + console.log('❌ No color elements found on page'); + return null; } - - .wplace-header-title { - display: flex; - align-items: center; - gap: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "6px"}; + // Separate available and unavailable colors + const availableColors = []; + const unavailableColors = []; + + Array.from(colorElements).forEach((el) => { + const id = Number.parseInt(el.id.replace('color-', '')); + if (id === 0) return; // Skip transparent color + + const rgbStr = el.style.backgroundColor.match(/\d+/g); + if (!rgbStr || rgbStr.length < 3) { + console.warn(`Skipping color element ${el.id} — cannot parse RGB`); + return; + } + const rgb = rgbStr.map(Number); + + // Find color name from COLOR_MAP + const colorInfo = Object.values(CONFIG.COLOR_MAP).find((color) => color.id === id); + const name = colorInfo ? colorInfo.name : `Unknown Color ${id}`; + + const colorData = { id, name, rgb }; + + // Check if color is available (no SVG overlay means available) + if (!el.querySelector('svg')) { + availableColors.push(colorData); + } else { + unavailableColors.push(colorData); + } + }); + + // Console log detailed color information + console.log('=== CAPTURED COLORS STATUS ==='); + console.log(`Total available colors: ${availableColors.length}`); + console.log(`Total unavailable colors: ${unavailableColors.length}`); + console.log(`Total colors scanned: ${availableColors.length + unavailableColors.length}`); + + if (availableColors.length > 0) { + console.log('\n--- AVAILABLE COLORS ---'); + availableColors.forEach((color, index) => { + console.log( + `${ + index + 1 + }. ID: ${color.id}, Name: "${color.name}", RGB: (${color.rgb[0]}, ${color.rgb[1]}, ${color.rgb[2]})` + ); + }); } - - .wplace-header-controls { - display: flex; - gap: ${CONFIG.currentTheme === "Neon Retro" ? "10px" : "6px"}; + + if (unavailableColors.length > 0) { + console.log('\n--- UNAVAILABLE COLORS ---'); + unavailableColors.forEach((color, index) => { + console.log( + `${ + index + 1 + }. ID: ${color.id}, Name: "${color.name}", RGB: (${color.rgb[0]}, ${color.rgb[1]}, ${color.rgb[2]}) [LOCKED]` + ); + }); } - - .wplace-header-btn { - background: ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(255,255,255,0.1)" : theme.accent}; - border: ${CONFIG.currentTheme === "Neon Retro" ? `2px solid ${theme.text}` : "none"}; - color: ${theme.text}; - cursor: pointer; - border-radius: ${CONFIG.currentTheme === "Classic Autobot" ? "4px" : "0"}; - width: ${CONFIG.currentTheme === "Classic Autobot" ? "18px" : "auto"}; - height: ${CONFIG.currentTheme === "Classic Autobot" ? "18px" : "auto"}; - padding: ${CONFIG.currentTheme === "Neon Retro" ? "4px 6px" : "0"}; - font-size: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "10px"}; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.2s; - font-family: ${theme.fontFamily}; - ${CONFIG.currentTheme === "Neon Retro" ? "image-rendering: pixelated;" : ""} - } - .wplace-header-btn:hover { - background: ${CONFIG.currentTheme === "Classic Autobot" ? theme.accent : theme.text}; - color: ${CONFIG.currentTheme === "Classic Autobot" ? theme.text : theme.primary}; - transform: ${CONFIG.currentTheme === "Classic Autobot" ? "scale(1.1)" : "none"}; - ${CONFIG.currentTheme === "Neon Retro" ? `box-shadow: 0 0 10px ${theme.text};` : ""} + + console.log('=== END COLOR STATUS ==='); + + return availableColors; + }, + + formatTime: (ms) => { + const seconds = Math.floor((ms / 1000) % 60); + const minutes = Math.floor((ms / (1000 * 60)) % 60); + const hours = Math.floor((ms / (1000 * 60 * 60)) % 24); + const days = Math.floor(ms / (1000 * 60 * 60 * 24)); + + let result = ''; + if (days > 0) result += `${days}d `; + if (hours > 0 || days > 0) result += `${hours}h `; + if (minutes > 0 || hours > 0 || days > 0) result += `${minutes}m `; + result += `${seconds}s`; + + return result; + }, + + calculateEstimatedTime: (remainingPixels, charges, cooldown) => { + if (remainingPixels <= 0) return 0; + + const paintingSpeedDelay = state.paintingSpeed > 0 ? 1000 / state.paintingSpeed : 1000; + const timeFromSpeed = remainingPixels * paintingSpeedDelay; + + const cyclesNeeded = Math.ceil(remainingPixels / Math.max(charges, 1)); + const timeFromCharges = cyclesNeeded * cooldown; + + return timeFromSpeed + timeFromCharges; // combine instead of taking max + }, + + // --- Painted pixel tracking helpers --- + initializePaintedMap: (width, height) => { + if (!state.paintedMap || state.paintedMap.length !== height) { + state.paintedMap = Array(height) + .fill() + .map(() => Array(width).fill(false)); + console.log(`📋 Initialized painted map: ${width}x${height}`); } - - .wplace-content { - padding: ${CONFIG.currentTheme === "Neon Retro" ? "15px" : "12px"}; - display: block; - position: relative; - z-index: 2; + }, + + markPixelPainted: (x, y, regionX = 0, regionY = 0) => { + const actualX = x + regionX; + const actualY = y + regionY; + + if ( + state.paintedMap && + state.paintedMap[actualY] && + actualX >= 0 && + actualX < state.paintedMap[actualY].length + ) { + state.paintedMap[actualY][actualX] = true; } - .wplace-content.wplace-hidden { - display: none; + }, + + isPixelPainted: (x, y, regionX = 0, regionY = 0) => { + const actualX = x + regionX; + const actualY = y + regionY; + + if ( + state.paintedMap && + state.paintedMap[actualY] && + actualX >= 0 && + actualX < state.paintedMap[actualY].length + ) { + return state.paintedMap[actualY][actualX]; } - - .wplace-status-section { - margin-bottom: 12px; - padding: 8px; - background: rgba(255,255,255,0.03); - border-radius: ${theme.borderRadius}; - border: 1px solid rgba(255,255,255,0.1); + return false; + }, + + // Smart save - only save if significant changes + shouldAutoSave: () => { + 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; + }, + + 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`); } - - .wplace-section { - margin-bottom: ${CONFIG.currentTheme === "Neon Retro" ? "15px" : "12px"}; - padding: 12px; - background: rgba(255,255,255,0.03); - border-radius: ${theme.borderRadius}; - border: 1px solid rgba(255,255,255,0.1); + + state._saveInProgress = false; + return success; + }, + + // --- Data management helpers --- + + // Base64 compression helpers for efficient storage + packPaintedMapToBase64: (paintedMap, width, height) => { + if (!paintedMap || !width || !height) return null; + const totalBits = width * height; + const byteLen = Math.ceil(totalBits / 8); + const bytes = new Uint8Array(byteLen); + let bitIndex = 0; + for (let y = 0; y < height; y++) { + const row = paintedMap[y]; + for (let x = 0; x < width; x++) { + const bit = row && row[x] ? 1 : 0; + const b = bitIndex >> 3; // byte index + const o = bitIndex & 7; // bit offset + if (bit) bytes[b] |= 1 << o; + bitIndex++; + } } - - .wplace-section-title { - font-size: 11px; - font-weight: 600; - margin-bottom: 8px; - color: ${theme.highlight}; - display: flex; - align-items: center; - gap: 6px; - text-transform: uppercase; - letter-spacing: 0.5px; + let binary = ''; + const chunk = 0x8000; + for (let i = 0; i < bytes.length; i += chunk) { + binary += String.fromCharCode.apply( + null, + bytes.subarray(i, Math.min(i + chunk, bytes.length)) + ); } - - .wplace-controls { - display: flex; - flex-direction: column; - gap: 8px; + return btoa(binary); + }, + + unpackPaintedMapFromBase64: (base64, width, height) => { + if (!base64 || !width || !height) return null; + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); + const map = Array(height) + .fill() + .map(() => Array(width).fill(false)); + let bitIndex = 0; + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const b = bitIndex >> 3; + const o = bitIndex & 7; + map[y][x] = ((bytes[b] >> o) & 1) === 1; + bitIndex++; + } } - .wplace-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 8px; + return map; + }, + + // Migration helpers for backward compatibility + migrateProgressToV2: (saved) => { + if (!saved) return saved; + const isV1 = + !saved.version || + saved.version === '1' || + saved.version === '1.0' || + saved.version === '1.1'; + if (!isV1) return saved; + + try { + const migrated = { ...saved }; + const width = migrated.imageData?.width; + const height = migrated.imageData?.height; + if (migrated.paintedMap && width && height) { + const data = Utils.packPaintedMapToBase64(migrated.paintedMap, width, height); + migrated.paintedMapPacked = { width, height, data }; + } + delete migrated.paintedMap; + migrated.version = '2'; + return migrated; + } catch (e) { + console.warn('Migration to v2 failed, using original data:', e); + return saved; } - .wplace-row.single { - grid-template-columns: 1fr; + }, + + migrateProgressToV21: (saved) => { + if (!saved) return saved; + if (saved.version === '2.1') return saved; + const isV2 = saved.version === '2' || saved.version === '2.0'; + const isV1 = + !saved.version || + saved.version === '1' || + saved.version === '1.0' || + saved.version === '1.1'; + if (!isV2 && !isV1) return saved; // save this for future + try { + const migrated = { ...saved }; + // First migrate to v2 if needed + if (isV1) { + const width = migrated.imageData?.width; + const height = migrated.imageData?.height; + if (migrated.paintedMap && width && height) { + const data = Utils.packPaintedMapToBase64(migrated.paintedMap, width, height); + migrated.paintedMapPacked = { width, height, data }; + } + delete migrated.paintedMap; + } + migrated.version = '2.1'; + return migrated; + } catch (e) { + console.warn('Migration to v2.1 failed, using original data:', e); + return saved; } - - .wplace-btn { - padding: ${CONFIG.currentTheme === "Neon Retro" ? "12px 8px" : "8px 12px"}; - border: ${CONFIG.currentTheme === "Neon Retro" ? "2px solid" : "none"}; - border-radius: ${theme.borderRadius}; - font-weight: ${CONFIG.currentTheme === "Neon Retro" ? "normal" : "500"}; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - gap: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "6px"}; - font-size: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "11px"}; - transition: all 0.3s ease; - position: relative; - overflow: hidden; - font-family: ${theme.fontFamily}; - ${CONFIG.currentTheme === "Neon Retro" ? "text-transform: uppercase; letter-spacing: 1px; image-rendering: pixelated;" : ""} - background: ${ - CONFIG.currentTheme === "Classic Autobot" - ? `linear-gradient(135deg, ${theme.accent} 0%, #4a4a4a 100%)` - : theme.accent - }; - ${CONFIG.currentTheme === "Classic Autobot" ? "border: 1px solid rgba(255,255,255,0.1);" : ""} + }, + + migrateProgressToV22: (data) => { + try { + const migrated = { ...data }; + migrated.version = '2.2'; + + // Add new fields with default values + if (!migrated.state.coordinateMode) { + migrated.state.coordinateMode = CONFIG.COORDINATE_MODE; + } + if (!migrated.state.coordinateDirection) { + migrated.state.coordinateDirection = CONFIG.COORDINATE_DIRECTION; + } + if (!migrated.state.coordinateSnake) { + migrated.state.coordinateSnake = CONFIG.COORDINATE_SNAKE; + } + if (!migrated.state.blockWidth) { + migrated.state.blockWidth = CONFIG.COORDINATE_BLOCK_WIDTH; + } + if (!migrated.state.blockHeight) { + migrated.state.blockHeight = CONFIG.COORDINATE_BLOCK_HEIGHT; + } + + return migrated; + } catch (e) { + console.warn('Migration to v2.2 failed, using original data:', e); + return data; } - - ${ - CONFIG.currentTheme === "Classic Autobot" - ? ` - .wplace-btn::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent); - transition: left 0.5s ease; - } - .wplace-btn:hover:not(:disabled)::before { - left: 100%; - }` - : ` - .wplace-btn::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); - transition: left 0.5s; - } - .wplace-btn:hover::before { - left: 100%; - }` + }, + + buildPaintedMapPacked() { + if (state.paintedMap && state.imageData) { + const data = Utils.packPaintedMapToBase64( + state.paintedMap, + state.imageData.width, + state.imageData.height + ); + if (data) { + return { + width: state.imageData.width, + height: state.imageData.height, + data: data, + }; + } } - - .wplace-btn:hover:not(:disabled) { - transform: ${CONFIG.currentTheme === "Classic Autobot" ? "translateY(-1px)" : "none"}; - box-shadow: ${ - CONFIG.currentTheme === "Classic Autobot" ? "0 4px 12px rgba(0,0,0,0.4)" : "0 0 15px currentColor" - }; - ${theme.animations.pixelBlink ? "animation: pixelBlink 0.5s infinite;" : ""} + return null; + }, + + buildProgressData() { + return { + timestamp: Date.now(), + version: '2.2', + state: { + totalPixels: state.totalPixels, + paintedPixels: state.paintedPixels, + lastPosition: state.lastPosition, + startPosition: state.startPosition, + region: state.region, + imageLoaded: state.imageLoaded, + colorsChecked: state.colorsChecked, + coordinateMode: state.coordinateMode, + coordinateDirection: state.coordinateDirection, + coordinateSnake: state.coordinateSnake, + blockWidth: state.blockWidth, + blockHeight: state.blockHeight, + availableColors: state.availableColors, + }, + imageData: state.imageData + ? { + width: state.imageData.width, + height: state.imageData.height, + pixels: Array.from(state.imageData.pixels), + totalPixels: state.imageData.totalPixels, + } + : null, + paintedMapPacked: Utils.buildPaintedMapPacked(), + }; + }, + + migrateProgress(saved) { + if (!saved) return null; + + let data = saved; + const ver = data.version; + + // If version is missing or ≤ 1.x → first migrate to v2 + if (!ver || ver === '1' || ver === '1.0' || ver === '1.1') { + data = Utils.migrateProgressToV2(data); } - .wplace-btn:active:not(:disabled) { - transform: translateY(0); + + // If still older than v2.1 → migrate to 2.1 + if (data.version === '2' || data.version === '2.0') { + data = Utils.migrateProgressToV21(data); } - - .wplace-btn-primary { - background: ${ - CONFIG.currentTheme === "Classic Autobot" - ? `linear-gradient(135deg, ${theme.accent} 0%, #6a5acd 100%)` - : theme.accent - }; - color: ${theme.text}; - ${CONFIG.currentTheme === "Neon Retro" ? `border-color: ${theme.text};` : ""} - } - .wplace-btn-upload { - background: ${ - CONFIG.currentTheme === "Classic Autobot" - ? `linear-gradient(135deg, ${theme.secondary} 0%, #4a4a4a 100%)` - : theme.purple - }; - color: ${theme.text}; - ${ - CONFIG.currentTheme === "Classic Autobot" - ? `border: 1px dashed ${theme.highlight};` - : `border-color: ${theme.text}; border-style: dashed;` - } - } - .wplace-btn-start { - background: ${ - CONFIG.currentTheme === "Classic Autobot" - ? `linear-gradient(135deg, ${theme.success} 0%, #228b22 100%)` - : theme.success - }; - color: ${CONFIG.currentTheme === "Classic Autobot" ? "white" : theme.primary}; - ${CONFIG.currentTheme === "Neon Retro" ? `border-color: ${theme.success};` : ""} - } - .wplace-btn-stop { - background: ${ - CONFIG.currentTheme === "Classic Autobot" - ? `linear-gradient(135deg, ${theme.error} 0%, #dc143c 100%)` - : theme.error - }; - color: ${CONFIG.currentTheme === "Classic Autobot" ? "white" : theme.text}; - ${CONFIG.currentTheme === "Neon Retro" ? `border-color: ${theme.error};` : ""} - } - .wplace-btn-select { - background: ${ - CONFIG.currentTheme === "Classic Autobot" - ? `linear-gradient(135deg, ${theme.highlight} 0%, #9370db 100%)` - : theme.highlight - }; - color: ${CONFIG.currentTheme === "Classic Autobot" ? "white" : theme.primary}; - ${CONFIG.currentTheme === "Neon Retro" ? `border-color: ${theme.highlight};` : ""} - } - .wplace-btn-file { - background: ${ - CONFIG.currentTheme === "Classic Autobot" - ? "linear-gradient(135deg, #ff8c00 0%, #ff7f50 100%)" - : theme.warning - }; - color: ${CONFIG.currentTheme === "Classic Autobot" ? "white" : theme.primary}; - ${CONFIG.currentTheme === "Neon Retro" ? `border-color: ${theme.warning};` : ""} + + // If still older than v2.2 → migrate to 2.2 + if (data.version === '2.1') { + data = Utils.migrateProgressToV22(data); } - .wplace-btn:disabled { - opacity: ${CONFIG.currentTheme === "Classic Autobot" ? "0.5" : "0.3"}; - cursor: not-allowed; - transform: none !important; - ${theme.animations.pixelBlink ? "animation: none !important;" : ""} - box-shadow: none !important; + + // Now data is guaranteed to be the latest version + return data; + }, + + saveProgress: () => { + try { + const progressData = Utils.buildProgressData(state); + + localStorage.setItem('wplace-bot-progress', JSON.stringify(progressData)); + return true; + } catch (error) { + console.error('Error saving progress:', error); + return false; } - .wplace-btn:disabled::before { - display: none; + }, + + loadProgress: () => { + try { + const saved = localStorage.getItem('wplace-bot-progress'); + if (!saved) return null; + let data = JSON.parse(saved); + const migrated = Utils.migrateProgress(data); + + if (migrated && migrated !== data) { + try { + localStorage.setItem('wplace-bot-progress', JSON.stringify(migrated)); + } catch {} + } + return migrated; + } catch (error) { + console.error('Error loading progress:', error); + return null; } - - .wplace-stats { - background: ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(255,255,255,0.03)" : theme.secondary}; - padding: ${CONFIG.currentTheme === "Neon Retro" ? "12px" : "8px"}; - border: ${CONFIG.currentTheme === "Neon Retro" ? `2px solid ${theme.text}` : "1px solid rgba(255,255,255,0.1)"}; - border-radius: ${theme.borderRadius}; - margin-bottom: ${CONFIG.currentTheme === "Neon Retro" ? "15px" : "8px"}; - ${CONFIG.currentTheme === "Neon Retro" ? "box-shadow: inset 0 0 10px rgba(0, 255, 65, 0.1);" : ""} + }, + + clearProgress: () => { + try { + localStorage.removeItem('wplace-bot-progress'); + // Also clear painted map from memory + state.paintedMap = null; + state._lastSavePixelCount = 0; + state._lastSaveTime = 0; + // Reset coordinate generation settings to their default values + state.coordinateMode = CONFIG.COORDINATE_MODE; + state.coordinateDirection = CONFIG.COORDINATE_DIRECTION; + state.coordinateSnake = CONFIG.COORDINATE_SNAKE; + state.blockWidth = CONFIG.COORDINATE_BLOCK_WIDTH; + state.blockHeight = CONFIG.COORDINATE_BLOCK_HEIGHT; + console.log('📋 Progress and painted map cleared'); + return true; + } catch (error) { + console.error('Error clearing progress:', error); + return false; } - - .wplace-stat-item { - display: flex; - justify-content: space-between; - padding: ${CONFIG.currentTheme === "Neon Retro" ? "6px 0" : "4px 0"}; - font-size: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "11px"}; - border-bottom: 1px solid rgba(255,255,255,0.05); - ${CONFIG.currentTheme === "Neon Retro" ? "text-transform: uppercase; letter-spacing: 1px;" : ""} - } - .wplace-stat-item:last-child { - border-bottom: none; - } - .wplace-stat-label { - display: flex; - align-items: center; - gap: 6px; - opacity: 0.9; - font-size: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "10px"}; - } - .wplace-stat-value { - font-weight: 600; - color: ${theme.highlight}; + }, + + restoreProgress: (savedData) => { + try { + Object.assign(state, savedData.state); + + // Restore coordinate generation settings + if (savedData.state.coordinateMode) { + state.coordinateMode = savedData.state.coordinateMode; + } + if (savedData.state.coordinateDirection) { + state.coordinateDirection = savedData.state.coordinateDirection; + } + if (savedData.state.coordinateSnake !== undefined) { + state.coordinateSnake = savedData.state.coordinateSnake; + } + if (savedData.state.blockWidth) { + state.blockWidth = savedData.state.blockWidth; + } + if (savedData.state.blockHeight) { + state.blockHeight = savedData.state.blockHeight; + } + + if (savedData.imageData) { + state.imageData = { + ...savedData.imageData, + pixels: new Uint8ClampedArray(savedData.imageData.pixels), + }; + + try { + const canvas = document.createElement('canvas'); + canvas.width = state.imageData.width; + canvas.height = state.imageData.height; + const ctx = canvas.getContext('2d'); + const imageData = new ImageData( + state.imageData.pixels, + state.imageData.width, + state.imageData.height + ); + ctx.putImageData(imageData, 0, 0); + const proc = new ImageProcessor(''); + proc.img = canvas; + proc.canvas = canvas; + proc.ctx = ctx; + state.imageData.processor = proc; + } catch (e) { + console.warn('Could not rebuild processor from saved image data:', e); + } + } + + // Prefer packed form if available; fallback to legacy paintedMap array for backward compatibility + if (savedData.paintedMapPacked && savedData.paintedMapPacked.data) { + const { width, height, data } = savedData.paintedMapPacked; + state.paintedMap = Utils.unpackPaintedMapFromBase64(data, width, height); + } else if (savedData.paintedMap) { + state.paintedMap = savedData.paintedMap.map((row) => Array.from(row)); + } + + return true; + } catch (error) { + console.error('Error restoring progress:', error); + return false; } - - .wplace-progress { - width: 100%; - background: ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(0,0,0,0.3)" : theme.secondary}; - border: ${CONFIG.currentTheme === "Neon Retro" ? `2px solid ${theme.text}` : "1px solid rgba(255,255,255,0.1)"}; - border-radius: ${theme.borderRadius}; - margin: ${CONFIG.currentTheme === "Neon Retro" ? "10px 0" : "8px 0"}; - overflow: hidden; - height: ${CONFIG.currentTheme === "Neon Retro" ? "16px" : "6px"}; - position: relative; + }, + + saveProgressToFile: () => { + try { + const progressData = Utils.buildProgressData(); + const filename = `wplace-bot-progress-${new Date() + .toISOString() + .slice(0, 19) + .replace(/:/g, '-')}.json`; + Utils.createFileDownloader(JSON.stringify(progressData, null, 2), filename); + return true; + } catch (error) { + console.error('Error saving to file:', error); + return false; } - - ${ - CONFIG.currentTheme === "Neon Retro" - ? ` - .wplace-progress::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: - repeating-linear-gradient( - 45deg, - transparent, - transparent 2px, - rgba(0, 255, 65, 0.1) 2px, - rgba(0, 255, 65, 0.1) 4px - ); - pointer-events: none; - }` - : "" + }, + + loadProgressFromFile: async () => { + try { + const data = await Utils.createFileUploader(); + if (!data || !data.state) { + throw new Error('Invalid file format'); + } + const migrated = Utils.migrateProgress(data); + + const success = Utils.restoreProgress(migrated); + return success; + } catch (error) { + console.error('Error loading from file:', error); + throw error; } - - .wplace-progress-bar { - height: ${CONFIG.currentTheme === "Neon Retro" ? "100%" : "6px"}; - background: ${ - CONFIG.currentTheme === "Classic Autobot" - ? `linear-gradient(135deg, ${theme.highlight} 0%, #9370db 100%)` - : `linear-gradient(90deg, ${theme.success}, ${theme.neon})` + }, + + // Helper function to restore overlay from loaded data + restoreOverlayFromData: async () => { + if (!state.imageLoaded || !state.imageData || !state.startPosition || !state.region) { + return false; + } + + try { + // Recreate ImageBitmap from loaded pixel data + const imageData = new ImageData( + state.imageData.pixels, + state.imageData.width, + state.imageData.height + ); + + const canvas = new OffscreenCanvas(state.imageData.width, state.imageData.height); + const ctx = canvas.getContext('2d'); + ctx.putImageData(imageData, 0, 0); + const imageBitmap = await canvas.transferToImageBitmap(); + + // Set up overlay with restored data + await overlayManager.setImage(imageBitmap); + await overlayManager.setPosition(state.startPosition, state.region); + overlayManager.enable(); + + // Update overlay button state + const toggleOverlayBtn = document.getElementById('toggleOverlayBtn'); + if (toggleOverlayBtn) { + toggleOverlayBtn.disabled = false; + toggleOverlayBtn.classList.add('active'); + } + + console.log('Overlay restored from data'); + return true; + } catch (error) { + console.error('Failed to restore overlay from data:', error); + return false; + } + }, + + updateCoordinateUI({ mode, directionControls, snakeControls, blockControls }) { + const isLinear = mode === 'rows' || mode === 'columns'; + const isBlock = mode === 'blocks' || mode === 'shuffle-blocks'; + + if (directionControls) directionControls.style.display = isLinear ? 'block' : 'none'; + if (snakeControls) snakeControls.style.display = isLinear ? 'block' : 'none'; + if (blockControls) blockControls.style.display = isBlock ? 'block' : 'none'; + }, + }; + + // IMAGE PROCESSOR CLASS + class ImageProcessor { + constructor(imageSrc) { + this.imageSrc = imageSrc; + this.img = null; + this.canvas = null; + this.ctx = null; + } + + async load() { + return new Promise((resolve, reject) => { + this.img = new Image(); + this.img.crossOrigin = 'anonymous'; + this.img.onload = () => { + this.canvas = document.createElement('canvas'); + this.ctx = this.canvas.getContext('2d'); + this.canvas.width = this.img.width; + this.canvas.height = this.img.height; + this.ctx.drawImage(this.img, 0, 0); + resolve(); + }; + this.img.onerror = reject; + this.img.src = this.imageSrc; + }); + } + + getDimensions() { + return { + width: this.canvas.width, + height: this.canvas.height, + }; + } + + getPixelData() { + return this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height).data; + } + + resize(newWidth, newHeight) { + const tempCanvas = document.createElement('canvas'); + const tempCtx = tempCanvas.getContext('2d'); + + tempCanvas.width = newWidth; + tempCanvas.height = newHeight; + + tempCtx.imageSmoothingEnabled = false; + tempCtx.drawImage(this.canvas, 0, 0, newWidth, newHeight); + + this.canvas.width = newWidth; + this.canvas.height = newHeight; + this.ctx.imageSmoothingEnabled = false; + this.ctx.drawImage(tempCanvas, 0, 0); + + return this.ctx.getImageData(0, 0, newWidth, newHeight).data; + } + + generatePreview(width, height) { + const previewCanvas = document.createElement('canvas'); + const previewCtx = previewCanvas.getContext('2d'); + + previewCanvas.width = width; + previewCanvas.height = height; + + previewCtx.imageSmoothingEnabled = false; + previewCtx.drawImage(this.img, 0, 0, width, height); + + return previewCanvas.toDataURL(); + } + } + + // WPLACE API SERVICE + const WPlaceService = { + async paintPixelInRegion(regionX, regionY, pixelX, pixelY, color) { + try { + await ensureToken(); + if (!turnstileToken) return 'token_error'; + const payload = { + coords: [pixelX, pixelY], + colors: [color], + t: turnstileToken, + }; + const res = await fetch(`https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, { + method: 'POST', + headers: { 'Content-Type': 'text/plain;charset=UTF-8' }, + credentials: 'include', + body: JSON.stringify(payload), + }); + if (res.status === 403) { + console.error('❌ 403 Forbidden. Turnstile token might be invalid or expired.'); + turnstileToken = null; + tokenPromise = new Promise((resolve) => { + _resolveToken = resolve; + }); + return 'token_error'; + } + const data = await res.json(); + return data?.painted === 1; + } catch (e) { + console.error('Paint request failed:', e); + return false; + } + }, + + async getCharges() { + const defaultResult = { + charges: 0, + max: 1, + cooldown: CONFIG.COOLDOWN_DEFAULT, + }; + + try { + const res = await fetch('https://backend.wplace.live/me', { + credentials: 'include', + }); + + if (!res.ok) { + console.error(`Failed to get charges: HTTP ${res.status}`); + return defaultResult; + } + + const data = await res.json(); + + return { + charges: data.charges?.count ?? 0, + max: data.charges?.max ?? 1, + cooldown: data.charges?.cooldownMs ?? CONFIG.COOLDOWN_DEFAULT, }; - transition: width ${CONFIG.currentTheme === "Neon Retro" ? "0.3s" : "0.5s"} ease; - position: relative; - ${CONFIG.currentTheme === "Neon Retro" ? `box-shadow: 0 0 10px ${theme.success};` : ""} - } - - ${ - CONFIG.currentTheme === "Classic Autobot" - ? ` - .wplace-progress-bar::after { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); - animation: shimmer 2s infinite; - }` - : ` - .wplace-progress-bar::after { - content: ''; - position: absolute; - top: 0; - right: 0; - width: 4px; - height: 100%; - background: ${theme.text}; - animation: pixelBlink 1s infinite; - }` - } - - .wplace-status { - padding: ${CONFIG.currentTheme === "Neon Retro" ? "10px" : "6px"}; - border: ${CONFIG.currentTheme === "Neon Retro" ? "2px solid" : "1px solid"}; - border-radius: ${theme.borderRadius}; - text-align: center; - font-size: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "11px"}; - ${CONFIG.currentTheme === "Neon Retro" ? "text-transform: uppercase; letter-spacing: 1px;" : ""} - position: relative; - overflow: hidden; + } catch (e) { + console.error('Failed to get charges:', e); + return defaultResult; } - - .status-default { - background: ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(255,255,255,0.1)" : theme.accent}; - border-color: ${theme.text}; - color: ${theme.text}; - } - .status-success { - background: ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(0, 255, 0, 0.1)" : theme.success}; - border-color: ${theme.success}; - color: ${CONFIG.currentTheme === "Classic Autobot" ? theme.success : theme.primary}; - box-shadow: 0 0 15px ${theme.success}; - } - .status-error { - background: ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(255, 0, 0, 0.1)" : theme.error}; - border-color: ${theme.error}; - color: ${CONFIG.currentTheme === "Classic Autobot" ? theme.error : theme.text}; - box-shadow: 0 0 15px ${theme.error}; - ${theme.animations.pixelBlink ? "animation: pixelBlink 0.5s infinite;" : ""} - } - .status-warning { - background: ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(255, 165, 0, 0.1)" : theme.warning}; - border-color: ${theme.warning}; - color: ${CONFIG.currentTheme === "Classic Autobot" ? "orange" : theme.primary}; - box-shadow: 0 0 15px ${theme.warning}; + }, + }; + + // 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(Utils.t('notificationsNotSupported'), 'warning'); + return 'denied'; } - - .resize-container { - display: none; - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: ${theme.primary}; - padding: 20px; - border: ${theme.borderWidth} ${theme.borderStyle} ${theme.text}; - border-radius: ${theme.borderRadius}; - z-index: 10000; - box-shadow: ${ - CONFIG.currentTheme === "Classic Autobot" ? "0 0 20px rgba(0,0,0,0.5)" : "0 0 30px rgba(0, 255, 65, 0.5)" - }; - max-width: 90%; - max-height: 90%; - overflow: auto; - font-family: ${theme.fontFamily}; + if (Notification.permission === 'granted') return 'granted'; + try { + const perm = await Notification.requestPermission(); + return perm; + } catch { + return Notification.permission; } - - .resize-preview { - max-width: 100%; - max-height: 300px; - margin: 10px 0; - border: ${ - CONFIG.currentTheme === "Classic Autobot" ? `1px solid ${theme.accent}` : `2px solid ${theme.accent}` - }; - ${CONFIG.currentTheme === "Neon Retro" ? "image-rendering: pixelated;" : ""} + }, + 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; } - - .resize-controls { - display: flex; - flex-direction: column; - gap: ${CONFIG.currentTheme === "Neon Retro" ? "15px" : "10px"}; - margin-top: 15px; + }, + resetEdgeTracking() { + state._lastChargesBelow = state.displayCharges < state.cooldownChargeThreshold; + state._lastChargesNotifyAt = 0; + }, + maybeNotifyChargesReached(force = false) { + if (!state.notificationsEnabled || !state.notifyOnChargesReached) return; + const reached = state.displayCharges >= state.cooldownChargeThreshold; + const now = Date.now(); + const repeatMs = Math.max(1, Number(state.notificationIntervalMinutes || 5)) * 60_000; + if (reached) { + const shouldEdge = state._lastChargesBelow || force; + const shouldRepeat = now - (state._lastChargesNotifyAt || 0) >= repeatMs; + if (shouldEdge || shouldRepeat) { + const msg = Utils.t('chargesReadyMessage', { + current: state.displayCharges, + max: state.maxCharges, + threshold: state.cooldownChargeThreshold, + }); + this.notify(Utils.t('chargesReadyNotification'), msg, 'wplace-notify-charges'); + state._lastChargesNotifyAt = now; + } + state._lastChargesBelow = false; + } else { + state._lastChargesBelow = true; } - - .resize-controls label { - font-size: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "12px"}; - ${CONFIG.currentTheme === "Neon Retro" ? "text-transform: uppercase; letter-spacing: 1px;" : ""} - color: ${theme.text}; + }, + 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.displayCharges = 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; } - - .resize-slider { - width: 100%; - height: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "4px"}; - background: ${CONFIG.currentTheme === "Classic Autobot" ? "#ccc" : theme.secondary}; - border: ${CONFIG.currentTheme === "Neon Retro" ? `2px solid ${theme.text}` : "none"}; - border-radius: ${theme.borderRadius}; - outline: none; - -webkit-appearance: none; + }, + syncFromState() { + this.resetEdgeTracking(); + if (state.notificationsEnabled && state.notifyOnChargesReached) this.startPolling(); + else this.stopPolling(); + }, + }; + + // COLOR MATCHING FUNCTION - Optimized with caching + const colorCache = new Map(); + + // UI UPDATE FUNCTIONS (declared early to avoid reference errors) + let updateUI = () => {}; + let updateStats = (isManualRefresh) => {}; + let updateDataButtons = () => {}; + + function updateActiveColorPalette() { + state.activeColorPalette = []; + const activeSwatches = document.querySelectorAll('.wplace-color-swatch.active'); + if (activeSwatches) { + activeSwatches.forEach((swatch) => { + const rgbStr = swatch.getAttribute('data-rgb'); + if (rgbStr) { + const rgb = rgbStr.split(',').map(Number); + state.activeColorPalette.push(rgb); + } + }); + } + if (document.querySelector('.resize-container')?.style.display === 'block') { + _updateResizePreview(); + } + } + + function toggleAllColors(select, showingUnavailable = false) { + const swatches = document.querySelectorAll('.wplace-color-swatch'); + if (swatches) { + swatches.forEach((swatch) => { + // Only toggle colors that are available or if we're showing unavailable colors + const isUnavailable = swatch.classList.contains('unavailable'); + if (!isUnavailable || showingUnavailable) { + // Don't try to select unavailable colors + if (!isUnavailable) { + swatch.classList.toggle('active', select); + } + } + }); + } + updateActiveColorPalette(); + } + + function unselectAllPaidColors() { + const swatches = document.querySelectorAll('.wplace-color-swatch'); + if (swatches) { + swatches.forEach((swatch) => { + const colorId = parseInt(swatch.getAttribute('data-color-id'), 10); + if (!isNaN(colorId) && colorId >= 32) { + swatch.classList.toggle('active', false); + } + }); + } + updateActiveColorPalette(); + } + + function initializeColorPalette(container) { + const colorsContainer = container.querySelector('#colors-container'); + const showAllToggle = container.querySelector('#showAllColorsToggle'); + if (!colorsContainer) return; + + // Use already captured colors from state (captured during upload) + // Don't re-fetch colors here, use what was captured when user clicked upload + if (!state.availableColors || state.availableColors.length === 0) { + // If no colors have been captured yet, show message + colorsContainer.innerHTML = `
${Utils.t( + 'uploadImageFirst' + )}
`; + return; + } + + function populateColors(showUnavailable = false) { + colorsContainer.innerHTML = ''; + let availableCount = 0; + let totalCount = 0; + + // Convert COLOR_MAP to array and filter out transparent + const allColors = Object.values(CONFIG.COLOR_MAP).filter((color) => color.rgb !== null); + + allColors.forEach((colorData) => { + const { id, name, rgb } = colorData; + const rgbKey = `${rgb.r},${rgb.g},${rgb.b}`; + totalCount++; + + // Check if this color is available in the captured colors + const isAvailable = state.availableColors.some( + (c) => c.rgb[0] === rgb.r && c.rgb[1] === rgb.g && c.rgb[2] === rgb.b + ); + + // If not showing all colors and this color is not available, skip it + if (!showUnavailable && !isAvailable) { + return; + } + + if (isAvailable) availableCount++; + + const colorItem = Utils.createElement('div', { + className: 'wplace-color-item', + }); + const swatch = Utils.createElement('button', { + className: `wplace-color-swatch ${!isAvailable ? 'unavailable' : ''}`, + title: `${name} (ID: ${id})${!isAvailable ? ' (Unavailable)' : ''}`, + 'data-rgb': rgbKey, + 'data-color-id': id, + }); + swatch.style.backgroundColor = `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`; + + // Make unavailable colors visually distinct + if (!isAvailable) { + swatch.style.opacity = '0.4'; + swatch.style.filter = 'grayscale(50%)'; + swatch.disabled = true; + } else { + // Select available colors by default + swatch.classList.add('active'); + } + + const nameLabel = Utils.createElement( + 'span', + { + className: 'wplace-color-item-name', + style: !isAvailable ? 'color: #888; font-style: italic;' : '', + }, + name + (!isAvailable ? ' (N/A)' : '') + ); + + // Only add click listener for available colors + if (isAvailable) { + swatch.addEventListener('click', () => { + swatch.classList.toggle('active'); + updateActiveColorPalette(); + }); + } + + colorItem.appendChild(swatch); + colorItem.appendChild(nameLabel); + colorsContainer.appendChild(colorItem); + }); + + updateActiveColorPalette(); + } + + // Initialize with only available colors + populateColors(false); + + // Add toggle functionality + if (showAllToggle) { + showAllToggle.addEventListener('change', (e) => { + populateColors(e.target.checked); + }); + } + + container + .querySelector('#selectAllBtn') + ?.addEventListener('click', () => toggleAllColors(true, showAllToggle?.checked)); + container + .querySelector('#unselectAllBtn') + ?.addEventListener('click', () => toggleAllColors(false, showAllToggle?.checked)); + container + .querySelector('#unselectPaidBtn') + ?.addEventListener('click', () => unselectAllPaidColors()); + } + + async function handleCaptcha() { + const startTime = performance.now(); + + // Check user's token source preference + if (state.tokenSource === 'manual') { + console.log('🎯 Manual token source selected - using pixel placement automation'); + return await handleCaptchaFallback(); + } + + // Generator mode (pure) or Hybrid mode - try generator first + try { + // Use optimized token generation with automatic sitekey detection + const { sitekey, token: preGeneratedToken } = await Utils.obtainSitekeyAndToken(); + + if (!sitekey) { + throw new Error('No valid sitekey found'); } - - ${ - CONFIG.currentTheme === "Neon Retro" - ? ` - .resize-slider::-webkit-slider-thumb { - -webkit-appearance: none; - width: 16px; - height: 16px; - background: ${theme.highlight}; - border: 2px solid ${theme.text}; - border-radius: 0; - cursor: pointer; - box-shadow: 0 0 5px ${theme.highlight}; + + console.log('🔑 Generating Turnstile token for sitekey:', sitekey); + console.log( + '🧭 UA:', + navigator.userAgent.substring(0, 50) + '...', + 'Platform:', + navigator.platform + ); + + // Add additional checks before token generation + if (!window.turnstile) { + await Utils.loadTurnstile(); } - - .resize-slider::-moz-range-thumb { - width: 16px; - height: 16px; - background: ${theme.highlight}; - border: 2px solid ${theme.text}; - border-radius: 0; - cursor: pointer; - box-shadow: 0 0 5px ${theme.highlight}; - }` - : "" + + let token = null; + + // ✅ Reuse pre-generated token if available and valid + if ( + preGeneratedToken && + typeof preGeneratedToken === 'string' && + preGeneratedToken.length > 20 + ) { + console.log('♻️ Reusing pre-generated token from sitekey detection phase'); + token = preGeneratedToken; } - - .resize-buttons { - display: flex; - gap: 10px; + // ✅ Or use globally cached token if still valid + else if (isTokenValid()) { + console.log('♻️ Using existing cached token (from previous operation)'); + token = turnstileToken; } - - .resize-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.8); - z-index: 9999; - display: none; + // ✅ Otherwise generate a new one + else { + console.log('🔐 No valid pre-generated or cached token, creating new one...'); + token = await Utils.executeTurnstile(sitekey, 'paint'); + if (token) { + setTurnstileToken(token); + } } - - ${ - CONFIG.currentTheme === "Neon Retro" - ? ` - /* Retro checkbox styling */ - input[type="checkbox"] { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - width: 16px; - height: 16px; - border: 2px solid ${theme.text}; - background: ${theme.secondary}; - margin-right: 8px; - position: relative; - cursor: pointer; + + // 📊 Debug log + console.log( + `🔍 Token received - Type: ${typeof token}, Value: ${ + token + ? typeof token === 'string' + ? token.length > 50 + ? token.substring(0, 50) + '...' + : token + : JSON.stringify(token) + : 'null/undefined' + }, Length: ${token?.length || 0}` + ); + + // ✅ Final validation + if (typeof token === 'string' && token.length > 20) { + const duration = Math.round(performance.now() - startTime); + console.log(`✅ Turnstile token generated successfully in ${duration}ms`); + return token; + } else { + throw new Error( + `Invalid or empty token received - Type: ${typeof token}, Value: ${JSON.stringify( + token + )}, Length: ${token?.length || 0}` + ); } - - input[type="checkbox"]:checked { - background: ${theme.success}; + } catch (error) { + const duration = Math.round(performance.now() - startTime); + console.error(`❌ Turnstile token generation failed after ${duration}ms:`, error); + + // Fallback to manual pixel placement for hybrid mode + if (state.tokenSource === 'hybrid') { + console.log( + '🔄 Hybrid mode: Generator failed, automatically switching to manual pixel placement...' + ); + const fbToken = await handleCaptchaFallback(); + return fbToken; + } else { + // Pure generator mode - don't fallback, just fail + throw error; } - - input[type="checkbox"]:checked::after { - content: '✓'; - position: absolute; - top: -2px; - left: 1px; - color: ${theme.primary}; - font-size: 12px; - font-weight: bold; + } + } + + // Keep original method as fallback + async function handleCaptchaFallback() { + return new Promise(async (resolve, reject) => { + try { + // Ensure we have a fresh promise to await for a new token capture + if (!_resolveToken) { + tokenPromise = new Promise((res) => { + _resolveToken = res; + }); + } + const timeoutPromise = Utils.sleep(20000).then(() => + reject(new Error('Auto-CAPTCHA timed out.')) + ); + + const solvePromise = (async () => { + const mainPaintBtn = await Utils.waitForSelector( + 'button.btn.btn-primary.btn-lg, button.btn-primary.sm\\:btn-xl', + 200, + 10000 + ); + if (!mainPaintBtn) throw new Error('Could not find the main paint button.'); + mainPaintBtn.click(); + await Utils.sleep(500); + + const transBtn = await Utils.waitForSelector('button#color-0', 200, 5000); + if (!transBtn) throw new Error('Could not find the transparent color button.'); + transBtn.click(); + await Utils.sleep(500); + + const canvas = await Utils.waitForSelector('canvas', 200, 5000); + if (!canvas) throw new Error('Could not find the canvas element.'); + + canvas.setAttribute('tabindex', '0'); + canvas.focus(); + const rect = canvas.getBoundingClientRect(); + const centerX = Math.round(rect.left + rect.width / 2); + const centerY = Math.round(rect.top + rect.height / 2); + + canvas.dispatchEvent( + new MouseEvent('mousemove', { + clientX: centerX, + clientY: centerY, + bubbles: true, + }) + ); + canvas.dispatchEvent( + new KeyboardEvent('keydown', { + key: ' ', + code: 'Space', + bubbles: true, + }) + ); + await Utils.sleep(50); + canvas.dispatchEvent( + new KeyboardEvent('keyup', { + key: ' ', + code: 'Space', + bubbles: true, + }) + ); + await Utils.sleep(500); + + // 800ms delay before sending confirmation + await Utils.sleep(800); + + // Keep confirming until token is captured + const confirmLoop = async () => { + while (!turnstileToken) { + let confirmBtn = await Utils.waitForSelector( + 'button.btn.btn-primary.btn-lg, button.btn.btn-primary.sm\\:btn-xl' + ); + if (!confirmBtn) { + const allPrimary = Array.from(document.querySelectorAll('button.btn-primary')); + confirmBtn = allPrimary.length ? allPrimary[allPrimary.length - 1] : null; + } + if (confirmBtn) { + confirmBtn.click(); + } + await Utils.sleep(500); // 500ms delay between confirmation attempts + } + }; + + // Start confirmation loop and wait for token + confirmLoop(); + const token = await tokenPromise; + await Utils.sleep(300); // small delay after token is captured + resolve(token); + })(); + + await Promise.race([solvePromise, timeoutPromise]); + } catch (error) { + console.error('Auto-CAPTCHA process failed:', error); + reject(error); } - - /* Icon styling for retro feel */ - .fas, .fa { - filter: drop-shadow(0 0 3px currentColor); - }` - : "" + }); + } + + async function createUI() { + await detectLanguage(); + + const existingContainer = document.getElementById('wplace-image-bot-container'); + const existingStats = document.getElementById('wplace-stats-container'); + const existingSettings = document.getElementById('wplace-settings-container'); + const existingResizeContainer = document.querySelector('.resize-container'); + const existingResizeOverlay = document.querySelector('.resize-overlay'); + + if (existingContainer) existingContainer.remove(); + if (existingStats) existingStats.remove(); + if (existingSettings) existingSettings.remove(); + if (existingResizeContainer) existingResizeContainer.remove(); + if (existingResizeOverlay) existingResizeOverlay.remove(); + + loadThemePreference(); + await initializeTranslations(); + + const theme = getCurrentTheme(); + applyTheme(); // <- new: set CSS vars and theme class before building UI + + function appendLinkOnce(href, attributes = {}) { + // Check if a link with the same href already exists in the document head + const exists = Array.from(document.head.querySelectorAll('link')).some( + (link) => link.href === href + ); + if (exists) return; + + // Create a new link element + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = href; + + // Add any additional attributes (e.g., data-* attributes) + for (const [key, value] of Object.entries(attributes)) { + link.setAttribute(key, value); } - ` - document.head.appendChild(style) - const container = document.createElement("div") - container.id = "wplace-image-bot-container" + // Append the link element to the document head + document.head.appendChild(link); + } + + appendLinkOnce('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css'); + + if (theme.fontFamily.includes('Press Start 2P')) { + appendLinkOnce('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); + } + + appendLinkOnce( + 'https://wplace-autobot.github.io/WPlace-AutoBOT/main/auto-image-styles.css', + { 'data-wplace-theme': 'true' } + ); + + const container = document.createElement('div'); + container.id = 'wplace-image-bot-container'; container.innerHTML = `
- ${Utils.t("title")} + ${Utils.t('title')}
- - - -
@@ -1497,42 +3420,33 @@
- ${Utils.t("waitingInit")} + ${Utils.t('initMessage')}
- -
-
🤖 Bot Setup
-
- -
-
-
🖼️ Image Management
-
@@ -1545,16 +3459,41 @@
+
+ +
+ +
+
⏱️ ${Utils.t('cooldownSettings')}
+
+ +
+
+ +
+
+ + + + ${Utils.t('charges')} +
+
+
+
+
💾 Data Management
@@ -1562,40 +3501,49 @@
-
-
- ` + `; // Stats Window - Separate UI - const statsContainer = document.createElement("div") - statsContainer.id = "wplace-stats-container" - statsContainer.style.display = "none" + const statsContainer = document.createElement('div'); + statsContainer.id = 'wplace-stats-container'; + statsContainer.style.display = 'none'; statsContainer.innerHTML = `
- Painting Stats + ${Utils.t('paintingStats')}
- +
@@ -1604,567 +3552,2953 @@
-
${Utils.t("initMessage")}
+
${Utils.t( + 'initMessage' + )}
+
+
+
+
+ `; + + // Modern Settings Container with Theme Support + // Use the theme variable already declared at the top of createUI function + const settingsContainer = document.createElement('div'); + settingsContainer.id = 'wplace-settings-container'; + + // Apply theme-based styling + const themeBackground = theme.primary + ? `linear-gradient(135deg, ${theme.primary} 0%, ${theme.secondary || theme.primary} 100%)` + : `linear-gradient(135deg, #667eea 0%, #764ba2 100%)`; + + settingsContainer.className = 'wplace-settings-container-base'; + // Apply theme-specific background + settingsContainer.style.background = themeBackground; + settingsContainer.style.cssText += ` + min-width: 420px; + max-width: 480px; + z-index: 99999; + color: ${theme.text || 'white'}; + font-family: ${theme.fontFamily || "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif"}; + box-shadow: ${ + theme.boxShadow || '0 20px 40px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.1)' + }; + backdrop-filter: ${theme.backdropFilter || 'blur(10px)'}; + overflow: hidden; + animation: settings-slide-in 0.4s ease-out; + ${ + theme.animations?.glow + ? ` + box-shadow: ${theme.boxShadow || '0 20px 40px rgba(0,0,0,0.3)'}, + 0 0 30px ${theme.highlight || theme.neon || '#00ffff'}; + ` + : '' + } + `; + + // noinspection CssInvalidFunction + settingsContainer.innerHTML = ` +
+
+

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

+ +
+
+ +
+ + +
+ +
+ +

+ Generator mode creates tokens automatically. Hybrid mode falls back to manual when generator fails. Manual mode only uses pixel placement. +

+
+
+ + +
+ + +
+ + +
+ +
+ +
+
+ Overlay Opacity +
${Math.round(state.overlayOpacity * 100)}%
+
+ +
+ + +
+
+ + +
+ + +
+ + + + + + +
+
+ + +
+ + + +
+ + +
+ + +
+
+ ${Utils.t('batchSize')} +
+
+
+ +
+
+
+ + + + pixels +
+
+
+
+ ${CONFIG.PAINTING_SPEED.MIN} + ${CONFIG.PAINTING_SPEED.MAX} +
+
+ + +
+
+
+ + +
+
+ + +
+
+

+ 🎲 Random batch size between min and max values +

+
+ + + +
+ + +
+ + + +
+ + +
+ + +
+ + +
+ + +
+ +
+ + +
+
+
+ + +
+
+ + +
+
+

+ 🧱 Block dimensions for block-based generation modes +

+
+
+ + +
+ +
+ + + +
+ ${Utils.t('repeatEvery')} + + ${Utils.t('minutesPl')} +
+
+ + +
+
+
+ + +
+ +
+ +
+
+ + +
+ +
+ +
+
+
+ + + + + `; + + const resizeContainer = document.createElement('div'); + resizeContainer.className = 'resize-container'; + resizeContainer.innerHTML = ` +

${Utils.t('resizeImage')}

+
+ + + + + +
+ + + + + + + 100% +
+ Drag to pan • Pinch to zoom • Double‑tap to zoom +
+
+
+ +
+
+
+ + +
+
+
+
+
+
+
+ +
+ + 1 +
+
+
+ +
+ + 1 +
+
+
+
+ +
+ + + +
+
+ + + Shift = Row • Alt = Column +
+
+ +
+
+  Color Palette +
+
+
+ +
+
+ + + +
+
+
+
+ +
+
+  Advanced Color Matching +
+
+ + +
+
+ ${Utils.t('chromaWeight')} + ${state.chromaPenaltyWeight} +
+ +
+ +
+ +
+
- ` - const resizeContainer = document.createElement("div") - resizeContainer.className = "resize-container" - resizeContainer.innerHTML = ` -

${Utils.t("resizeImage")}

-
- - - - -
- - -
+
+ + +
- ` - - const resizeOverlay = document.createElement("div") - resizeOverlay.className = "resize-overlay" - - document.body.appendChild(container) - document.body.appendChild(resizeOverlay) - document.body.appendChild(resizeContainer) - document.body.appendChild(statsContainer) - - // Query all UI elements after appending to DOM - const initBotBtn = container.querySelector("#initBotBtn") - const uploadBtn = container.querySelector("#uploadBtn") - const resizeBtn = container.querySelector("#resizeBtn") - const selectPosBtn = container.querySelector("#selectPosBtn") - const startBtn = container.querySelector("#startBtn") - const stopBtn = container.querySelector("#stopBtn") - const saveBtn = container.querySelector("#saveBtn") - const loadBtn = container.querySelector("#loadBtn") - const saveToFileBtn = container.querySelector("#saveToFileBtn") - const loadFromFileBtn = container.querySelector("#loadFromFileBtn") - const minimizeBtn = container.querySelector("#minimizeBtn") - const compactBtn = container.querySelector("#compactBtn") - const statsBtn = container.querySelector("#statsBtn") - const statusText = container.querySelector("#statusText") - const progressBar = container.querySelector("#progressBar") - const statsArea = statsContainer.querySelector("#statsArea") - const content = container.querySelector(".wplace-content") - const closeStatsBtn = statsContainer.querySelector("#closeStatsBtn") - - // Check if all elements are found - if (!initBotBtn || !uploadBtn || !selectPosBtn || !startBtn || !stopBtn) { - console.error("Some UI elements not found:", { - initBotBtn: !!initBotBtn, + `; + + const resizeOverlay = document.createElement('div'); + resizeOverlay.className = 'resize-overlay'; + + document.body.appendChild(container); + document.body.appendChild(resizeOverlay); + document.body.appendChild(resizeContainer); + document.body.appendChild(statsContainer); + document.body.appendChild(settingsContainer); + + // Show the main container after all elements are appended + container.style.display = 'block'; + + const uploadBtn = container.querySelector('#uploadBtn'); + const resizeBtn = container.querySelector('#resizeBtn'); + const selectPosBtn = container.querySelector('#selectPosBtn'); + const startBtn = container.querySelector('#startBtn'); + const stopBtn = container.querySelector('#stopBtn'); + const saveBtn = container.querySelector('#saveBtn'); + const loadBtn = container.querySelector('#loadBtn'); + const saveToFileBtn = container.querySelector('#saveToFileBtn'); + const loadFromFileBtn = container.querySelector('#loadFromFileBtn'); + + container.querySelectorAll('.wplace-section-title').forEach((title) => { + // Add a right-side arrow if it doesn't exist + if (!title.querySelector('i.arrow')) { + const arrow = document.createElement('i'); + arrow.className = 'fas fa-chevron-down arrow'; // FontAwesome down arrow + title.appendChild(arrow); + } + + // Click event to toggle collapse/expand of the section + title.addEventListener('click', () => { + const section = title.parentElement; + section.classList.toggle('collapsed'); + }); + }); + + // Disable load/upload buttons until initial setup is complete (startup only) + if (loadBtn) { + loadBtn.disabled = !state.initialSetupComplete; + loadBtn.title = state.initialSetupComplete + ? '' + : '🔄 Waiting for initial setup to complete...'; + } + if (loadFromFileBtn) { + loadFromFileBtn.disabled = !state.initialSetupComplete; + loadFromFileBtn.title = state.initialSetupComplete + ? '' + : '🔄 Waiting for initial setup to complete...'; + } + if (uploadBtn) { + uploadBtn.disabled = !state.initialSetupComplete; + uploadBtn.title = state.initialSetupComplete + ? '' + : '🔄 Waiting for initial setup to complete...'; + } + + const minimizeBtn = container.querySelector('#minimizeBtn'); + const compactBtn = container.querySelector('#compactBtn'); + const statsBtn = container.querySelector('#statsBtn'); + const toggleOverlayBtn = container.querySelector('#toggleOverlayBtn'); + const statusText = container.querySelector('#statusText'); + const progressBar = container.querySelector('#progressBar'); + const statsArea = statsContainer.querySelector('#statsArea'); + const content = container.querySelector('.wplace-content'); + const closeStatsBtn = statsContainer.querySelector('#closeStatsBtn'); + const refreshChargesBtn = statsContainer.querySelector('#refreshChargesBtn'); + const cooldownSlider = container.querySelector('#cooldownSlider'); + const cooldownInput = container.querySelector('#cooldownInput'); + const cooldownDecrease = container.querySelector('#cooldownDecrease'); + const cooldownIncrease = container.querySelector('#cooldownIncrease'); + const cooldownValue = container.querySelector('#cooldownValue'); + + if (!uploadBtn || !selectPosBtn || !startBtn || !stopBtn) { + console.error('Some UI elements not found:', { uploadBtn: !!uploadBtn, selectPosBtn: !!selectPosBtn, startBtn: !!startBtn, stopBtn: !!stopBtn, - }) + }); } if (!statsContainer || !statsArea || !closeStatsBtn) { - console.error("Stats UI elements not found:", { - statsContainer: !!statsContainer, - statsArea: !!statsArea, - closeStatsBtn: !!closeStatsBtn, - }) + // Note: base CSS now aligns with this layout: main panel at left:20px (width 280), stats at left:330px. } - const header = container.querySelector(".wplace-header") - let pos1 = 0, - pos2 = 0, - pos3 = 0, - pos4 = 0 + const header = container.querySelector('.wplace-header'); + + makeDraggable(container); + + function makeDraggable(element) { + let pos1 = 0, + pos2 = 0, + pos3 = 0, + pos4 = 0; + let isDragging = false; + const header = + element.querySelector('.wplace-header') || element.querySelector('.wplace-settings-header'); + + if (!header) { + console.warn('No draggable header found for element:', element); + return; + } + + header.onmousedown = dragMouseDown; + + function dragMouseDown(e) { + if (e.target.closest('.wplace-header-btn') || e.target.closest('button')) return; - header.onmousedown = dragMouseDown + e.preventDefault(); + isDragging = true; - function dragMouseDown(e) { - if (e.target.closest(".wplace-header-btn")) return + const rect = element.getBoundingClientRect(); - e.preventDefault() - pos3 = e.clientX - pos4 = e.clientY - container.classList.add("wplace-dragging") - document.onmouseup = closeDragElement - document.onmousemove = elementDrag + element.style.transform = 'none'; + element.style.top = rect.top + 'px'; + element.style.left = rect.left + 'px'; - // Prevent text selection during drag - document.body.style.userSelect = "none" - } + pos3 = e.clientX; + pos4 = e.clientY; + element.classList.add('wplace-dragging'); + document.onmouseup = closeDragElement; + document.onmousemove = elementDrag; - function elementDrag(e) { - e.preventDefault() - pos1 = pos3 - e.clientX - pos2 = pos4 - e.clientY - pos3 = e.clientX - pos4 = e.clientY + document.body.style.userSelect = 'none'; + } + + function elementDrag(e) { + if (!isDragging) return; + + e.preventDefault(); + pos1 = pos3 - e.clientX; + pos2 = pos4 - e.clientY; + pos3 = e.clientX; + pos4 = e.clientY; - let newTop = container.offsetTop - pos2 - let newLeft = container.offsetLeft - pos1 + let newTop = element.offsetTop - pos2; + let newLeft = element.offsetLeft - pos1; - // Boundary checking to keep UI within viewport - const rect = container.getBoundingClientRect() - const maxTop = window.innerHeight - rect.height - const maxLeft = window.innerWidth - rect.width + const rect = element.getBoundingClientRect(); + const maxTop = window.innerHeight - rect.height; + const maxLeft = window.innerWidth - rect.width; - newTop = Math.max(0, Math.min(newTop, maxTop)) - newLeft = Math.max(0, Math.min(newLeft, maxLeft)) + newTop = Math.max(0, Math.min(newTop, maxTop)); + newLeft = Math.max(0, Math.min(newLeft, maxLeft)); - container.style.top = newTop + "px" - container.style.left = newLeft + "px" + element.style.top = newTop + 'px'; + element.style.left = newLeft + 'px'; + } + + function closeDragElement() { + isDragging = false; + element.classList.remove('wplace-dragging'); + document.onmouseup = null; + document.onmousemove = null; + document.body.style.userSelect = ''; + } } - function closeDragElement() { - container.classList.remove("wplace-dragging") - document.onmouseup = null - document.onmousemove = null - document.body.style.userSelect = "" + makeDraggable(statsContainer); + makeDraggable(container); + + if (statsBtn && closeStatsBtn) { + statsBtn.addEventListener('click', () => { + const isVisible = statsContainer.style.display !== 'none'; + if (isVisible) { + statsContainer.style.display = 'none'; + statsBtn.innerHTML = ''; + statsBtn.title = Utils.t('showStats'); + } else { + statsContainer.style.display = 'block'; + statsBtn.innerHTML = ''; + statsBtn.title = Utils.t('hideStats'); + } + }); + + closeStatsBtn.addEventListener('click', () => { + statsContainer.style.display = 'none'; + statsBtn.innerHTML = ''; + statsBtn.title = Utils.t('showStats'); + }); + + if (refreshChargesBtn) { + refreshChargesBtn.addEventListener('click', async () => { + refreshChargesBtn.innerHTML = ''; + refreshChargesBtn.disabled = true; + + try { + await updateStats(true); + } catch (error) { + console.error('Error refreshing charges:', error); + } finally { + refreshChargesBtn.innerHTML = ''; + refreshChargesBtn.disabled = false; + } + }); + } + } + if (statsContainer && statsBtn) { + // Stats container starts hidden - user clicks button to show + statsBtn.innerHTML = ''; + statsBtn.title = Utils.t('showStats'); } - function makeDraggable(element) { - let pos1 = 0, - pos2 = 0, - pos3 = 0, - pos4 = 0 - const header = element.querySelector(".wplace-header") - header.onmousedown = dragMouseDown + const settingsBtn = container.querySelector('#settingsBtn'); + const closeSettingsBtn = settingsContainer.querySelector('#closeSettingsBtn'); + const applySettingsBtn = settingsContainer.querySelector('#applySettingsBtn'); - function dragMouseDown(e) { - if (e.target.closest(".wplace-header-btn")) return + if (settingsBtn && closeSettingsBtn && applySettingsBtn) { + settingsBtn.addEventListener('click', () => { + const isVisible = settingsContainer.classList.contains('show'); + if (isVisible) { + settingsContainer.style.animation = 'settings-fade-out 0.3s ease-out forwards'; + settingsContainer.classList.remove('show'); + setTimeout(() => { + settingsContainer.style.animation = ''; + }, 300); + } else { + settingsContainer.style.top = '50%'; + settingsContainer.style.left = '50%'; + settingsContainer.style.transform = 'translate(-50%, -50%)'; + settingsContainer.classList.add('show'); + settingsContainer.style.animation = 'settings-slide-in 0.4s ease-out'; + } + }); - e.preventDefault() - pos3 = e.clientX - pos4 = e.clientY - element.classList.add("wplace-dragging") - document.onmouseup = closeDragElement - document.onmousemove = elementDrag + closeSettingsBtn.addEventListener('click', () => { + settingsContainer.style.animation = 'settings-fade-out 0.3s ease-out forwards'; + settingsContainer.classList.remove('show'); + setTimeout(() => { + settingsContainer.style.animation = ''; + settingsContainer.style.top = '50%'; + settingsContainer.style.left = '50%'; + settingsContainer.style.transform = 'translate(-50%, -50%)'; + }, 300); + }); + + applySettingsBtn.addEventListener('click', () => { + // Sync advanced settings before save + const colorAlgorithmSelect = document.getElementById('colorAlgorithmSelect'); + if (colorAlgorithmSelect) state.colorMatchingAlgorithm = colorAlgorithmSelect.value; + const enableChromaPenaltyToggle = document.getElementById('enableChromaPenaltyToggle'); + if (enableChromaPenaltyToggle) + state.enableChromaPenalty = enableChromaPenaltyToggle.checked; + const chromaPenaltyWeightSlider = document.getElementById('chromaPenaltyWeightSlider'); + if (chromaPenaltyWeightSlider) + state.chromaPenaltyWeight = parseFloat(chromaPenaltyWeightSlider.value) || 0.15; + const transparencyThresholdInput = document.getElementById('transparencyThresholdInput'); + if (transparencyThresholdInput) { + const v = parseInt(transparencyThresholdInput.value, 10); + if (!isNaN(v) && v >= 0 && v <= 255) state.customTransparencyThreshold = v; + } + const whiteThresholdInput = document.getElementById('whiteThresholdInput'); + if (whiteThresholdInput) { + const v = parseInt(whiteThresholdInput.value, 10); + if (!isNaN(v) && v >= 200 && v <= 255) state.customWhiteThreshold = v; + } + // Update functional thresholds + CONFIG.TRANSPARENCY_THRESHOLD = state.customTransparencyThreshold; + CONFIG.WHITE_THRESHOLD = state.customWhiteThreshold; + // Notifications + const notifEnabledToggle = document.getElementById('notifEnabledToggle'); + const notifOnChargesToggle = document.getElementById('notifOnChargesToggle'); + const notifOnlyUnfocusedToggle = document.getElementById('notifOnlyUnfocusedToggle'); + const notifIntervalInput = document.getElementById('notifIntervalInput'); + if (notifEnabledToggle) state.notificationsEnabled = !!notifEnabledToggle.checked; + if (notifOnChargesToggle) state.notifyOnChargesReached = !!notifOnChargesToggle.checked; + if (notifOnlyUnfocusedToggle) + state.notifyOnlyWhenUnfocused = !!notifOnlyUnfocusedToggle.checked; + if (notifIntervalInput) { + const v = parseInt(notifIntervalInput.value, 10); + if (!isNaN(v) && v >= 1 && v <= 60) state.notificationIntervalMinutes = v; + } + saveBotSettings(); + Utils.showAlert(Utils.t('settingsSaved'), 'success'); + closeSettingsBtn.click(); + NotificationManager.syncFromState(); + }); + + makeDraggable(settingsContainer); + + const tokenSourceSelect = settingsContainer.querySelector('#tokenSourceSelect'); + if (tokenSourceSelect) { + tokenSourceSelect.addEventListener('change', (e) => { + state.tokenSource = e.target.value; + saveBotSettings(); + console.log(`🔑 Token source changed to: ${state.tokenSource}`); + const sourceNames = { + generator: 'Automatic Generator', + hybrid: 'Generator + Auto Fallback', + manual: 'Manual Pixel Placement', + }; + Utils.showAlert( + Utils.t('tokenSourceSet', { source: sourceNames[state.tokenSource] }), + 'success' + ); + }); + } + + // Batch mode controls + const batchModeSelect = settingsContainer.querySelector('#batchModeSelect'); + const normalBatchControls = settingsContainer.querySelector('#normalBatchControls'); + const randomBatchControls = settingsContainer.querySelector('#randomBatchControls'); + const randomBatchMin = settingsContainer.querySelector('#randomBatchMin'); + const randomBatchMax = settingsContainer.querySelector('#randomBatchMax'); + + if (batchModeSelect) { + batchModeSelect.addEventListener('change', (e) => { + state.batchMode = e.target.value; + + // Switch between normal and random controls + if (normalBatchControls && randomBatchControls) { + if (e.target.value === 'random') { + normalBatchControls.style.display = 'none'; + randomBatchControls.style.display = 'block'; + } else { + normalBatchControls.style.display = 'block'; + randomBatchControls.style.display = 'none'; + } + } - // Prevent text selection during drag - document.body.style.userSelect = "none" + saveBotSettings(); + console.log(`📦 Batch mode changed to: ${state.batchMode}`); + Utils.showAlert( + Utils.t('batchModeSet', { + mode: + state.batchMode === 'random' ? Utils.t('randomRange') : Utils.t('normalFixedSize'), + }), + 'success' + ); + }); } - function elementDrag(e) { - e.preventDefault() - pos1 = pos3 - e.clientX - pos2 = pos4 - e.clientY - pos3 = e.clientX - pos4 = e.clientY + if (randomBatchMin) { + randomBatchMin.addEventListener('input', (e) => { + const min = parseInt(e.target.value); + if (min >= 1 && min <= 1000) { + state.randomBatchMin = min; + // Ensure min doesn't exceed max + if (randomBatchMax && min > state.randomBatchMax) { + state.randomBatchMax = min; + randomBatchMax.value = min; + } + saveBotSettings(); + } + }); + } + + if (randomBatchMax) { + randomBatchMax.addEventListener('input', (e) => { + const max = parseInt(e.target.value); + if (max >= 1 && max <= 1000) { + state.randomBatchMax = max; + // Ensure max doesn't go below min + if (randomBatchMin && max < state.randomBatchMin) { + state.randomBatchMin = max; + randomBatchMin.value = max; + } + saveBotSettings(); + } + }); + } + + const languageSelect = settingsContainer.querySelector('#languageSelect'); + if (languageSelect) { + languageSelect.addEventListener('change', async (e) => { + const newLanguage = e.target.value; + state.language = newLanguage; + localStorage.setItem('wplace_language', newLanguage); + + // Load the new language translations + await loadTranslations(newLanguage); + + setTimeout(() => { + settingsContainer.style.display = 'none'; + createUI(); + }, 100); + }); + } - let newTop = element.offsetTop - pos2 - let newLeft = element.offsetLeft - pos1 + const themeSelect = settingsContainer.querySelector('#themeSelect'); + if (themeSelect) { + themeSelect.addEventListener('change', (e) => { + const newTheme = e.target.value; + switchTheme(newTheme); + }); + } - // Boundary checking to keep UI within viewport - const rect = element.getBoundingClientRect() - const maxTop = window.innerHeight - rect.height - const maxLeft = window.innerWidth - rect.width + const overlayOpacitySlider = settingsContainer.querySelector('#overlayOpacitySlider'); + const overlayOpacityValue = settingsContainer.querySelector('#overlayOpacityValue'); + const enableBlueMarbleToggle = settingsContainer.querySelector('#enableBlueMarbleToggle'); + const settingsPaintWhiteToggle = settingsContainer.querySelector('#settingsPaintWhiteToggle'); + const settingsPaintTransparentToggle = settingsContainer.querySelector( + '#settingsPaintTransparentToggle' + ); + + if (overlayOpacitySlider && overlayOpacityValue) { + const updateOpacity = (newValue) => { + const opacity = parseFloat(newValue); + state.overlayOpacity = opacity; + overlayOpacitySlider.value = opacity; + overlayOpacityValue.textContent = `${Math.round(opacity * 100)}%`; + }; - newTop = Math.max(0, Math.min(newTop, maxTop)) - newLeft = Math.max(0, Math.min(newLeft, maxLeft)) + overlayOpacitySlider.addEventListener('input', (e) => { + updateOpacity(e.target.value); + }); - element.style.top = newTop + "px" - element.style.left = newLeft + "px" + // Add scroll-to-adjust for overlay opacity slider + Utils.createScrollToAdjust(overlayOpacitySlider, updateOpacity, 0, 1, 0.05); } - function closeDragElement() { - element.classList.remove("wplace-dragging") - document.onmouseup = null - document.onmousemove = null - document.body.style.userSelect = "" + if (settingsPaintWhiteToggle) { + settingsPaintWhiteToggle.checked = state.paintWhitePixels; + settingsPaintWhiteToggle.addEventListener('change', (e) => { + state.paintWhitePixels = e.target.checked; + saveBotSettings(); + console.log(`🎨 Paint white pixels: ${state.paintWhitePixels ? 'ON' : 'OFF'}`); + const statusText = state.paintWhitePixels + ? 'White pixels in the template will be painted' + : 'White pixels will be skipped'; + Utils.showAlert(statusText, 'success'); + }); + } + + if (settingsPaintTransparentToggle) { + settingsPaintTransparentToggle.checked = state.paintTransparentPixels; + settingsPaintTransparentToggle.addEventListener('change', (e) => { + state.paintTransparentPixels = e.target.checked; + saveBotSettings(); + console.log( + `🎨 Paint transparent pixels: ${state.paintTransparentPixels ? 'ON' : 'OFF'}` + ); + const statusText = state.paintTransparentPixels + ? 'Transparent pixels in the template will be painted with the closest available color' + : 'Transparent pixels will be skipped'; + Utils.showAlert(statusText, 'success'); + }); + } + + // Speed controls - both slider and input + const speedSlider = settingsContainer.querySelector('#speedSlider'); + const speedInput = settingsContainer.querySelector('#speedInput'); + const speedDecrease = settingsContainer.querySelector('#speedDecrease'); + const speedIncrease = settingsContainer.querySelector('#speedIncrease'); + const speedValue = settingsContainer.querySelector('#speedValue'); + + if (speedSlider && speedInput && speedValue && speedDecrease && speedIncrease) { + const updateSpeed = (newValue) => { + const speed = Math.max(CONFIG.PAINTING_SPEED.MIN, Math.min(CONFIG.PAINTING_SPEED.MAX, parseInt(newValue))); + state.paintingSpeed = speed; + + // Update both controls (value shows in input, label shows unit only) + speedSlider.value = speed; + speedInput.value = speed; + speedValue.textContent = `pixels`; + + saveBotSettings(); + }; + + // Slider event listener + speedSlider.addEventListener('input', (e) => { + updateSpeed(e.target.value); + }); + + // Number input event listener + speedInput.addEventListener('input', (e) => { + updateSpeed(e.target.value); + }); + + // Decrease button + speedDecrease.addEventListener('click', () => { + updateSpeed(parseInt(speedInput.value) - 1); + }); + + // Increase button + speedIncrease.addEventListener('click', () => { + updateSpeed(parseInt(speedInput.value) + 1); + }); + + // Add scroll-to-adjust for speed slider + Utils.createScrollToAdjust(speedSlider, updateSpeed, CONFIG.PAINTING_SPEED.MIN, CONFIG.PAINTING_SPEED.MAX, 1); + } + + if (enableBlueMarbleToggle) { + enableBlueMarbleToggle.addEventListener('click', async () => { + state.blueMarbleEnabled = enableBlueMarbleToggle.checked; + if (state.imageLoaded && overlayManager.imageBitmap) { + Utils.showAlert(Utils.t('reprocessingOverlay'), 'info'); + await overlayManager.processImageIntoChunks(); + Utils.showAlert(Utils.t('overlayUpdated'), 'success'); + } + }); + } + + // (Advanced color listeners moved outside to work with resize dialog) + // (Advanced color listeners moved outside to work with resize dialog) + // Notifications listeners + const notifPermBtn = settingsContainer.querySelector('#notifRequestPermBtn'); + const notifTestBtn = settingsContainer.querySelector('#notifTestBtn'); + if (notifPermBtn) { + notifPermBtn.addEventListener('click', async () => { + const perm = await NotificationManager.requestPermission(); + if (perm === 'granted') Utils.showAlert(Utils.t('notificationsEnabled'), 'success'); + else Utils.showAlert(Utils.t('notificationsPermissionDenied'), 'warning'); + }); + } + if (notifTestBtn) { + notifTestBtn.addEventListener('click', () => { + NotificationManager.notify( + Utils.t('testNotificationTitle'), + Utils.t('testNotificationMessage'), + 'wplace-notify-test', + true + ); + }); } } - // Make stats container draggable - makeDraggable(statsContainer) + const widthSlider = resizeContainer.querySelector('#widthSlider'); + const heightSlider = resizeContainer.querySelector('#heightSlider'); + const widthValue = resizeContainer.querySelector('#widthValue'); + const heightValue = resizeContainer.querySelector('#heightValue'); + const keepAspect = resizeContainer.querySelector('#keepAspect'); + const paintWhiteToggle = resizeContainer.querySelector('#paintWhiteToggle'); + const paintTransparentToggle = resizeContainer.querySelector('#paintTransparentToggle'); + const zoomSlider = resizeContainer.querySelector('#zoomSlider'); + const zoomValue = resizeContainer.querySelector('#zoomValue'); + const zoomInBtn = resizeContainer.querySelector('#zoomInBtn'); + const zoomOutBtn = resizeContainer.querySelector('#zoomOutBtn'); + const zoomFitBtn = resizeContainer.querySelector('#zoomFitBtn'); + const zoomActualBtn = resizeContainer.querySelector('#zoomActualBtn'); + const panModeBtn = resizeContainer.querySelector('#panModeBtn'); + const panStage = resizeContainer.querySelector('#resizePanStage'); + const canvasStack = resizeContainer.querySelector('#resizeCanvasStack'); + const baseCanvas = resizeContainer.querySelector('#resizeCanvas'); + const maskCanvas = resizeContainer.querySelector('#maskCanvas'); + const baseCtx = baseCanvas.getContext('2d'); + const maskCtx = maskCanvas.getContext('2d'); + const confirmResize = resizeContainer.querySelector('#confirmResize'); + const cancelResize = resizeContainer.querySelector('#cancelResize'); + const downloadPreviewBtn = resizeContainer.querySelector('#downloadPreviewBtn'); + const clearIgnoredBtn = resizeContainer.querySelector('#clearIgnoredBtn'); + + // Coordinate generation controls with smart visibility + const coordinateModeSelect = settingsContainer.querySelector('#coordinateModeSelect'); + const coordinateDirectionSelect = settingsContainer.querySelector('#coordinateDirectionSelect'); + const coordinateSnakeToggle = settingsContainer.querySelector('#coordinateSnakeToggle'); + const directionControls = settingsContainer.querySelector('#directionControls'); + const snakeControls = settingsContainer.querySelector('#snakeControls'); + const blockControls = settingsContainer.querySelector('#blockControls'); + const blockWidthInput = settingsContainer.querySelector('#blockWidthInput'); + const blockHeightInput = settingsContainer.querySelector('#blockHeightInput'); + const paintUnavailablePixelsToggle = settingsContainer.querySelector( + '#paintUnavailablePixelsToggle' + ); + + if (paintUnavailablePixelsToggle) { + paintUnavailablePixelsToggle.checked = state.paintUnavailablePixels; + paintUnavailablePixelsToggle.addEventListener('change', (e) => { + state.paintUnavailablePixels = e.target.checked; + saveBotSettings(); + console.log(`🎨 Paint unavailable colors: ${state.paintUnavailablePixels ? 'ON' : 'OFF'}`); + const statusText = state.paintUnavailablePixels + ? 'Unavailable template colors will be painted with the closest available color' + : 'Unavailable template colors will be skipped'; + Utils.showAlert(statusText, 'success'); + }); + } + if (coordinateModeSelect) { + coordinateModeSelect.value = state.coordinateMode; + coordinateModeSelect.addEventListener('change', (e) => { + state.coordinateMode = e.target.value; + Utils.updateCoordinateUI({ + mode: state.coordinateMode, + directionControls, + snakeControls, + blockControls, + }); + saveBotSettings(); + console.log(`🔄 Coordinate mode changed to: ${state.coordinateMode}`); + Utils.showAlert(`Coordinate mode set to: ${state.coordinateMode}`, 'success'); + }); + } - // Make main container draggable - makeDraggable(container) + if (coordinateDirectionSelect) { + coordinateDirectionSelect.value = state.coordinateDirection; + coordinateDirectionSelect.addEventListener('change', (e) => { + state.coordinateDirection = e.target.value; + saveBotSettings(); + console.log(`🧭 Coordinate direction changed to: ${state.coordinateDirection}`); + Utils.showAlert(`Coordinate direction set to: ${state.coordinateDirection}`, 'success'); + }); + } - // Stats window functionality - if (statsBtn && closeStatsBtn) { - statsBtn.addEventListener("click", () => { - const isVisible = statsContainer.style.display !== "none" - if (isVisible) { - statsContainer.style.display = "none" - statsBtn.innerHTML = '' - statsBtn.title = "Show Stats" - } else { - statsContainer.style.display = "block" - statsBtn.innerHTML = '' - statsBtn.title = "Hide Stats" - } - }) + if (coordinateSnakeToggle) { + coordinateSnakeToggle.checked = state.coordinateSnake; + coordinateSnakeToggle.addEventListener('change', (e) => { + state.coordinateSnake = e.target.checked; + saveBotSettings(); + console.log(`🐍 Snake pattern ${state.coordinateSnake ? 'enabled' : 'disabled'}`); + Utils.showAlert( + `Snake pattern ${state.coordinateSnake ? 'enabled' : 'disabled'}`, + 'success' + ); + }); + } - closeStatsBtn.addEventListener("click", () => { - statsContainer.style.display = "none" - statsBtn.innerHTML = '' - statsBtn.title = "Show Stats" - }) + if (blockWidthInput) { + blockWidthInput.value = state.blockWidth; + blockWidthInput.addEventListener('input', (e) => { + const width = parseInt(e.target.value); + if (width >= 1 && width <= 50) { + state.blockWidth = width; + saveBotSettings(); + } + }); } - const widthSlider = resizeContainer.querySelector("#widthSlider") - const heightSlider = resizeContainer.querySelector("#heightSlider") - const widthValue = resizeContainer.querySelector("#widthValue") - const heightValue = resizeContainer.querySelector("#heightValue") - const keepAspect = resizeContainer.querySelector("#keepAspect") - const resizePreview = resizeContainer.querySelector("#resizePreview") - const confirmResize = resizeContainer.querySelector("#confirmResize") - const cancelResize = resizeContainer.querySelector("#cancelResize") + if (blockHeightInput) { + blockHeightInput.value = state.blockHeight; + blockHeightInput.addEventListener('change', (e) => { + const height = parseInt(e.target.value); + if (height >= 1 && height <= 50) { + state.blockHeight = height; + saveBotSettings(); + } + }); + } - // Compact mode functionality if (compactBtn) { - compactBtn.addEventListener("click", () => { - container.classList.toggle("wplace-compact") - const isCompact = container.classList.contains("wplace-compact") + compactBtn.addEventListener('click', () => { + container.classList.toggle('wplace-compact'); + const isCompact = container.classList.contains('wplace-compact'); if (isCompact) { - compactBtn.innerHTML = '' - compactBtn.title = "Expand Mode" + compactBtn.innerHTML = ''; + compactBtn.title = Utils.t('expandMode'); } else { - compactBtn.innerHTML = '' - compactBtn.title = "Compact Mode" + compactBtn.innerHTML = ''; + compactBtn.title = Utils.t('compactMode'); } - }) + }); } - // Minimize functionality if (minimizeBtn) { - minimizeBtn.addEventListener("click", () => { - state.minimized = !state.minimized + minimizeBtn.addEventListener('click', () => { + state.minimized = !state.minimized; if (state.minimized) { - container.classList.add("wplace-minimized") - content.classList.add("wplace-hidden") - minimizeBtn.innerHTML = '' - minimizeBtn.title = "Restore" + container.classList.add('wplace-minimized'); + content.classList.add('wplace-hidden'); + minimizeBtn.innerHTML = ''; + minimizeBtn.title = Utils.t('restore'); } else { - container.classList.remove("wplace-minimized") - content.classList.remove("wplace-hidden") - minimizeBtn.innerHTML = '' - minimizeBtn.title = "Minimize" + container.classList.remove('wplace-minimized'); + content.classList.remove('wplace-hidden'); + minimizeBtn.innerHTML = ''; + minimizeBtn.title = Utils.t('minimize'); } - }) + saveBotSettings(); + }); + } + + if (toggleOverlayBtn) { + toggleOverlayBtn.addEventListener('click', () => { + const isEnabled = overlayManager.toggle(); + toggleOverlayBtn.classList.toggle('active', isEnabled); + toggleOverlayBtn.setAttribute('aria-pressed', isEnabled ? 'true' : 'false'); + Utils.showAlert(isEnabled ? Utils.t('overlayEnabled') : Utils.t('overlayDisabled'), 'info'); + }); + } + + if (state.minimized) { + container.classList.add('wplace-minimized'); + content.classList.add('wplace-hidden'); + if (minimizeBtn) { + minimizeBtn.innerHTML = ''; + minimizeBtn.title = Utils.t('restore'); + } + } else { + container.classList.remove('wplace-minimized'); + content.classList.remove('wplace-hidden'); + if (minimizeBtn) { + minimizeBtn.innerHTML = ''; + minimizeBtn.title = Utils.t('minimize'); + } } - // Save progress functionality if (saveBtn) { - saveBtn.addEventListener("click", () => { + saveBtn.addEventListener('click', () => { if (!state.imageLoaded) { - Utils.showAlert(Utils.t("missingRequirements"), "error") - return + Utils.showAlert(Utils.t('missingRequirements'), 'error'); + return; } - const success = Utils.saveProgress() + const success = Utils.saveProgress(); if (success) { - updateUI("autoSaved", "success") - Utils.showAlert(Utils.t("autoSaved"), "success") + updateUI('autoSaved', 'success'); + Utils.showAlert(Utils.t('autoSaved'), 'success'); } else { - Utils.showAlert("❌ Erro ao salvar progresso", "error") + Utils.showAlert(Utils.t('errorSavingProgress'), 'error'); } - }) + }); } - // Load progress functionality if (loadBtn) { - loadBtn.addEventListener("click", () => { - const savedData = Utils.loadProgress() + loadBtn.addEventListener('click', () => { + // Check if initial setup is complete + if (!state.initialSetupComplete) { + Utils.showAlert(Utils.t('pleaseWaitInitialSetup'), 'warning'); + return; + } + + const savedData = Utils.loadProgress(); if (!savedData) { - updateUI("noSavedData", "warning") - Utils.showAlert(Utils.t("noSavedData"), "warning") - return + updateUI('noSavedData', 'warning'); + Utils.showAlert(Utils.t('noSavedData'), 'warning'); + return; } - // Show confirmation dialog const confirmLoad = confirm( - `${Utils.t("savedDataFound")}\n\n` + + `${Utils.t('savedDataFound')}\n\n` + `Saved: ${new Date(savedData.timestamp).toLocaleString()}\n` + - `Progress: ${savedData.state.paintedPixels}/${savedData.state.totalPixels} pixels`, - ) + `Progress: ${savedData.state.paintedPixels}/${savedData.state.totalPixels} pixels` + ); if (confirmLoad) { - const success = Utils.restoreProgress(savedData) + const success = Utils.restoreProgress(savedData); if (success) { - updateUI("dataLoaded", "success") - Utils.showAlert(Utils.t("dataLoaded"), "success") - updateDataButtons() + updateUI('dataLoaded', 'success'); + Utils.showAlert(Utils.t('dataLoaded'), 'success'); + updateDataButtons(); + + updateStats(); + + // Restore overlay if image data was loaded from localStorage + Utils.restoreOverlayFromData().catch((error) => { + console.error('Failed to restore overlay from localStorage:', error); + }); if (!state.colorsChecked) { - initBotBtn.style.display = "block" + uploadBtn.disabled = false; + } else { + uploadBtn.disabled = false; + selectPosBtn.disabled = false; } if (state.imageLoaded && state.startPosition && state.region && state.colorsChecked) { - startBtn.disabled = false + startBtn.disabled = false; } } else { - Utils.showAlert("❌ Erro ao carregar progresso", "error") + Utils.showAlert(Utils.t('errorLoadingProgress'), 'error'); } } - }) + }); } - // Save to file functionality if (saveToFileBtn) { - saveToFileBtn.addEventListener("click", () => { - const success = Utils.saveProgressToFile() + saveToFileBtn.addEventListener('click', () => { + const success = Utils.saveProgressToFile(); if (success) { - updateUI("fileSaved", "success") - Utils.showAlert(Utils.t("fileSaved"), "success") + updateUI('fileSaved', 'success'); + Utils.showAlert(Utils.t('fileSaved'), 'success'); } else { - Utils.showAlert(Utils.t("fileError"), "error") + Utils.showAlert(Utils.t('fileError'), 'error'); } - }) + }); } - // Load from file functionality if (loadFromFileBtn) { - loadFromFileBtn.addEventListener("click", async () => { + loadFromFileBtn.addEventListener('click', async () => { + // Check if initial setup is complete + if (!state.initialSetupComplete) { + Utils.showAlert(Utils.t('pleaseWaitFileSetup'), 'warning'); + return; + } + try { - const success = await Utils.loadProgressFromFile() + const success = await Utils.loadProgressFromFile(); if (success) { - updateUI("fileLoaded", "success") - Utils.showAlert(Utils.t("fileLoaded"), "success") - updateDataButtons() + updateUI('fileLoaded', 'success'); + Utils.showAlert(Utils.t('fileLoaded'), 'success'); + updateDataButtons(); + + await updateStats(); + + // Restore overlay if image data was loaded from file + await Utils.restoreOverlayFromData().catch((error) => { + console.error('Failed to restore overlay from file:', error); + }); - // Auto-enable buttons after loading from file if (state.colorsChecked) { - uploadBtn.disabled = false - selectPosBtn.disabled = false - resizeBtn.disabled = false - initBotBtn.style.display = "none" + uploadBtn.disabled = false; + selectPosBtn.disabled = false; + resizeBtn.disabled = false; } else { - initBotBtn.style.display = "block" + uploadBtn.disabled = false; } if (state.imageLoaded && state.startPosition && state.region && state.colorsChecked) { - startBtn.disabled = false + startBtn.disabled = false; } } } catch (error) { - if (error.message === "Invalid JSON file") { - Utils.showAlert(Utils.t("invalidFileFormat"), "error") + if (error.message === 'Invalid JSON file') { + Utils.showAlert(Utils.t('invalidFileFormat'), 'error'); } else { - Utils.showAlert(Utils.t("fileError"), "error") + Utils.showAlert(Utils.t('fileError'), 'error'); } } - }) + }); } - updateUI = (messageKey, type = "default", params = {}) => { - const message = Utils.t(messageKey, params) - statusText.textContent = message - statusText.className = `wplace-status status-${type}` - statusText.style.animation = "none" - void statusText.offsetWidth - statusText.style.animation = "slideIn 0.3s ease-out" + updateUI = (messageKey, type = 'default', params = {}, silent = false) => { + const message = Utils.t(messageKey, params); + statusText.textContent = message; + statusText.className = `wplace-status status-${type}`; + + if (!silent) { + // Trigger animation only when silent = false + statusText.style.animation = 'none'; + void statusText.offsetWidth; // trick to restart the animation + statusText.style.animation = 'slide-in 0.3s ease-out'; + } + }; + + function updateChargeStatsDisplay(intervalMs) { + const currentChargesEl = document.getElementById('wplace-stat-charges-value'); + const fullChargeEl = document.getElementById('wplace-stat-fullcharge-value'); + if (!fullChargeEl && !currentChargesEl) return; + if (!state.fullChargeData) { + fullChargeEl.textContent = '--:--:--'; + return; + } + + const { current, max, cooldownMs, startTime, spentSinceShot } = state.fullChargeData; + const elapsed = Date.now() - startTime; + + // total charges including elapsed time and spent during painting since snapshot + const chargesGained = elapsed / cooldownMs; + const rawCharges = current + chargesGained - spentSinceShot; + const cappedCharges = Math.min(rawCharges, max); + + // rounding with 0.95 threshold + let displayCharges; + const fraction = cappedCharges - Math.floor(cappedCharges); + if (fraction >= 0.95) { + displayCharges = Math.ceil(cappedCharges); + } else { + displayCharges = Math.floor(cappedCharges); + } + + state.displayCharges = Math.max(0, displayCharges); + state.preciseCurrentCharges = cappedCharges; + + const remainingMs = getMsToTargetCharges(cappedCharges, max, state.cooldown, intervalMs); + const timeText = Utils.msToTimeText(remainingMs); + + if (currentChargesEl) { + currentChargesEl.innerHTML = `${state.displayCharges} / ${state.maxCharges}`; + } + + if ( + state.displayCharges < state.cooldownChargeThreshold && + !state.stopFlag && + state.running + ) { + updateChargesThresholdUI(intervalMs); + } + + if (fullChargeEl) { + if (state.displayCharges >= max) { + fullChargeEl.innerHTML = `FULL`; + } else { + fullChargeEl.innerHTML = ` + ${timeText} + `; + } + } } - updateStats = async () => { - if (!state.colorsChecked || !state.imageLoaded) return + updateStats = async (isManualRefresh = false) => { + const isForcedRefresh = isManualRefresh; + const isFirstCheck = !state.fullChargeData?.startTime; + + const minUpdateInterval = 60_000; + const maxUpdateInterval = 90_000; + const randomUpdateThreshold = + minUpdateInterval + Math.random() * (maxUpdateInterval - minUpdateInterval); + const timeSinceLastUpdate = Date.now() - (state.fullChargeData?.startTime || 0); + const isTimeToUpdate = timeSinceLastUpdate >= randomUpdateThreshold; + + const shouldCallApi = isForcedRefresh || isFirstCheck || isTimeToUpdate; + + if (shouldCallApi) { + const { charges, max, cooldown } = await WPlaceService.getCharges(); + state.displayCharges = Math.floor(charges); + state.preciseCurrentCharges = charges; + state.cooldown = cooldown; + state.maxCharges = Math.floor(max) > 1 ? Math.floor(max) : state.maxCharges; + + state.fullChargeData = { + current: charges, + max: max, + cooldownMs: cooldown, + startTime: Date.now(), + spentSinceShot: 0, + }; + // Evaluate notifications every time we refresh server-side charges + NotificationManager.maybeNotifyChargesReached(); + } + + if (state.fullChargeInterval) { + clearInterval(state.fullChargeInterval); + state.fullChargeInterval = null; + } + const intervalMs = 1000; + state.fullChargeInterval = setInterval( + () => updateChargeStatsDisplay(intervalMs), + intervalMs + ); + + if (cooldownSlider && cooldownSlider.max !== state.maxCharges) { + cooldownSlider.max = state.maxCharges; + } + if (cooldownInput && cooldownInput.max !== state.maxCharges) { + cooldownInput.max = state.maxCharges; + } + + let imageStatsHTML = ''; + if (state.imageLoaded) { + const progress = + state.totalPixels > 0 ? Math.round((state.paintedPixels / state.totalPixels) * 100) : 0; + const remainingPixels = state.totalPixels - state.paintedPixels; + state.estimatedTime = Utils.calculateEstimatedTime( + remainingPixels, + state.displayCharges, + state.cooldown + ); + progressBar.style.width = `${progress}%`; + + imageStatsHTML = ` +
+
${Utils.t('progress')}
+
${progress}%
+
+
+
${Utils.t( + 'pixels' + )}
+
${state.paintedPixels}/${state.totalPixels}
+
+
+
${Utils.t( + 'estimatedTime' + )}
+
${Utils.formatTime(state.estimatedTime)}
+
+ `; + } - const { charges, cooldown } = await WPlaceService.getCharges() - state.currentCharges = Math.floor(charges) - state.cooldown = cooldown + let colorSwatchesHTML = ''; + state.availableColors = state.availableColors.filter( + (c) => c.name !== 'Unknown CoIor NaN' && c.id !== null + ); - const progress = state.totalPixels > 0 ? Math.round((state.paintedPixels / state.totalPixels) * 100) : 0 - const remainingPixels = state.totalPixels - state.paintedPixels + const availableColors = Utils.extractAvailableColors(); + const newCount = Array.isArray(availableColors) ? availableColors.length : 0; - state.estimatedTime = Utils.calculateEstimatedTime(remainingPixels, state.currentCharges, state.cooldown) + if (newCount === 0 && isManualRefresh) { + Utils.showAlert(Utils.t('noColorsFound'), 'warning'); + } else if (newCount > 0 && state.availableColors.length < newCount) { + const oldCount = state.availableColors.length; - progressBar.style.width = `${progress}%` + Utils.showAlert( + Utils.t('colorsUpdated', { + oldCount, + newCount: newCount, + diffCount: newCount - oldCount, + }), + 'success' + ); + state.availableColors = availableColors; + } + if (state.colorsChecked) { + colorSwatchesHTML = state.availableColors + .map((color) => { + const rgbString = `rgb(${color.rgb.join(',')})`; + return `
`; + }) + .join(''); + } statsArea.innerHTML = ` -
-
${Utils.t("progress")}
-
${progress}%
-
-
-
${Utils.t("pixels")}
-
${state.paintedPixels}/${state.totalPixels}
-
-
-
${Utils.t("charges")}
-
${Math.floor(state.currentCharges)}
-
- ${ - state.imageLoaded - ? ` -
-
${Utils.t("estimatedTime")}
-
${Utils.formatTime(state.estimatedTime)}
-
- ` - : "" + ${imageStatsHTML} +
+
+ ${Utils.t('charges')} +
+
+ ${state.displayCharges} / ${state.maxCharges} +
+
+
+
+ ${Utils.t('fullChargeIn')} +
+
--:--:--
+
+ ${ + state.colorsChecked + ? ` +
+
${Utils.t( + 'availableColors', + { count: state.availableColors.length } + )}
+
+ ${colorSwatchesHTML} +
+
+ ` + : '' + } + `; + + // should be after statsArea.innerHTML = '...'. todo make full stats ui update partial + updateChargeStatsDisplay(intervalMs); + }; + + updateDataButtons = () => { + const hasImageData = state.imageLoaded && state.imageData; + saveBtn.disabled = !hasImageData; + saveToFileBtn.disabled = !hasImageData; + }; + + updateDataButtons(); + + function showResizeDialog(processor) { + let baseProcessor = processor; + let width, height; + if (state.originalImage?.dataUrl) { + baseProcessor = new ImageProcessor(state.originalImage.dataUrl); + width = state.originalImage.width; + height = state.originalImage.height; + } else { + const dims = processor.getDimensions(); + width = dims.width; + height = dims.height; + } + const aspectRatio = width / height; + + const rs = state.resizeSettings; + widthSlider.max = width * 2; + heightSlider.max = height * 2; + let initialW = width; + let initialH = height; + if ( + rs && + Number.isFinite(rs.width) && + Number.isFinite(rs.height) && + rs.width > 0 && + rs.height > 0 + ) { + initialW = rs.width; + initialH = rs.height; + } + // Clamp to slider ranges + initialW = Math.max( + parseInt(widthSlider.min, 10) || 10, + Math.min(initialW, parseInt(widthSlider.max, 10)) + ); + initialH = Math.max( + parseInt(heightSlider.min, 10) || 10, + Math.min(initialH, parseInt(heightSlider.max, 10)) + ); + widthSlider.value = initialW; + heightSlider.value = initialH; + widthValue.textContent = initialW; + heightValue.textContent = initialH; + zoomSlider.value = 1; + if (zoomValue) zoomValue.textContent = '100%'; + paintWhiteToggle.checked = state.paintWhitePixels; + paintTransparentToggle.checked = state.paintTransparentPixels; + + let _previewTimer = null; + let _previewJobId = 0; + let _isDraggingSize = false; + let _zoomLevel = 1; + let _ditherWorkBuf = null; + let _ditherEligibleBuf = null; + const ensureDitherBuffers = (n) => { + if (!_ditherWorkBuf || _ditherWorkBuf.length !== n * 3) + _ditherWorkBuf = new Float32Array(n * 3); + if (!_ditherEligibleBuf || _ditherEligibleBuf.length !== n) + _ditherEligibleBuf = new Uint8Array(n); + return { work: _ditherWorkBuf, eligible: _ditherEligibleBuf }; + }; + let _maskImageData = null; + let _maskData = null; + let _dirty = null; + const _resetDirty = () => { + _dirty = { minX: Infinity, minY: Infinity, maxX: -1, maxY: -1 }; + }; + const _markDirty = (x, y) => { + if (!_dirty) _resetDirty(); + if (x < _dirty.minX) _dirty.minX = x; + if (y < _dirty.minY) _dirty.minY = y; + if (x > _dirty.maxX) _dirty.maxX = x; + if (y > _dirty.maxY) _dirty.maxY = y; + }; + const _flushDirty = () => { + if (!_dirty || _dirty.maxX < _dirty.minX || _dirty.maxY < _dirty.minY) return; + const x = Math.max(0, _dirty.minX); + const y = Math.max(0, _dirty.minY); + const w = Math.min(maskCanvas.width - x, _dirty.maxX - x + 1); + const h = Math.min(maskCanvas.height - y, _dirty.maxY - y + 1); + if (w > 0 && h > 0) maskCtx.putImageData(_maskImageData, 0, 0, x, y, w, h); + _resetDirty(); + }; + const _ensureMaskOverlayBuffers = (w, h, rebuildFromMask = false) => { + if (!_maskImageData || _maskImageData.width !== w || _maskImageData.height !== h) { + _maskImageData = maskCtx.createImageData(w, h); + _maskData = _maskImageData.data; + rebuildFromMask = true; + } + if (rebuildFromMask) { + const m = state.resizeIgnoreMask; + const md = _maskData; + md.fill(0); + if (m) { + for (let i = 0; i < m.length; i++) + if (m[i]) { + const p = i * 4; + md[p] = 255; + md[p + 1] = 0; + md[p + 2] = 0; + md[p + 3] = 150; + } + } + maskCtx.putImageData(_maskImageData, 0, 0); + _resetDirty(); + } + }; + const ensureMaskSize = (w, h) => { + if (!state.resizeIgnoreMask || state.resizeIgnoreMask.length !== w * h) { + state.resizeIgnoreMask = new Uint8Array(w * h); + } + baseCanvas.width = w; + baseCanvas.height = h; + maskCanvas.width = w; + maskCanvas.height = h; + maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); + // Ensure overlay buffers exist and rebuild from mask when dimensions change + _ensureMaskOverlayBuffers(w, h, true); + }; + _updateResizePreview = async () => { + const jobId = ++_previewJobId; + const newWidth = parseInt(widthSlider.value, 10); + const newHeight = parseInt(heightSlider.value, 10); + _zoomLevel = parseFloat(zoomSlider.value); + + widthValue.textContent = newWidth; + heightValue.textContent = newHeight; + + ensureMaskSize(newWidth, newHeight); + canvasStack.style.width = newWidth + 'px'; + canvasStack.style.height = newHeight + 'px'; + baseCtx.imageSmoothingEnabled = false; + if (!state.availableColors || state.availableColors.length === 0) { + if (baseProcessor !== processor && (!baseProcessor.img || !baseProcessor.canvas)) { + await baseProcessor.load(); + } + baseCtx.clearRect(0, 0, newWidth, newHeight); + baseCtx.drawImage(baseProcessor.img, 0, 0, newWidth, newHeight); + // Draw existing mask overlay buffer + maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); + if (_maskImageData) maskCtx.putImageData(_maskImageData, 0, 0); + updateZoomLayout(); + return; + } + if (baseProcessor !== processor && (!baseProcessor.img || !baseProcessor.canvas)) { + await baseProcessor.load(); + } + baseCtx.clearRect(0, 0, newWidth, newHeight); + baseCtx.drawImage(baseProcessor.img, 0, 0, newWidth, newHeight); + const imgData = baseCtx.getImageData(0, 0, newWidth, newHeight); + const data = imgData.data; + + const tThresh = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; + + const applyFSDither = () => { + const w = newWidth, + h = newHeight; + const n = w * h; + const { work, eligible } = ensureDitherBuffers(n); + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const idx = y * w + x; + const i4 = idx * 4; + const r = data[i4], + g = data[i4 + 1], + b = data[i4 + 2], + a = data[i4 + 3]; + const isEligible = + (state.paintTransparentPixels || a >= tThresh) && + (state.paintWhitePixels || !Utils.isWhitePixel(r, g, b)); + eligible[idx] = isEligible ? 1 : 0; + work[idx * 3] = r; + work[idx * 3 + 1] = g; + work[idx * 3 + 2] = b; + if (!isEligible) { + data[i4 + 3] = 0; // transparent in preview overlay + } + } + } + + const diffuse = (nx, ny, er, eg, eb, factor) => { + if (nx < 0 || nx >= w || ny < 0 || ny >= h) return; + const nidx = ny * w + nx; + if (!eligible[nidx]) return; + const base = nidx * 3; + work[base] = Math.min(255, Math.max(0, work[base] + er * factor)); + work[base + 1] = Math.min(255, Math.max(0, work[base + 1] + eg * factor)); + work[base + 2] = Math.min(255, Math.max(0, work[base + 2] + eb * factor)); + }; + + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const idx = y * w + x; + if (!eligible[idx]) continue; + const base = idx * 3; + const r0 = work[base], + g0 = work[base + 1], + b0 = work[base + 2]; + const [nr, ng, nb] = Utils.findClosestPaletteColor( + r0, + g0, + b0, + state.activeColorPalette + ); + const i4 = idx * 4; + data[i4] = nr; + data[i4 + 1] = ng; + data[i4 + 2] = nb; + data[i4 + 3] = 255; + + const er = r0 - nr; + const eg = g0 - ng; + const eb = b0 - nb; + + diffuse(x + 1, y, er, eg, eb, 7 / 16); + diffuse(x - 1, y + 1, er, eg, eb, 3 / 16); + diffuse(x, y + 1, er, eg, eb, 5 / 16); + diffuse(x + 1, y + 1, er, eg, eb, 1 / 16); + } + } + }; + + // Skip expensive dithering while user is dragging sliders + if (state.ditheringEnabled && !_isDraggingSize) { + applyFSDither(); + } else { + for (let i = 0; i < data.length; i += 4) { + const r = data[i], + g = data[i + 1], + b = data[i + 2], + a = data[i + 3]; + if ( + (!state.paintTransparentPixels && a < tThresh) || + (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b)) + ) { + data[i + 3] = 0; + continue; + } + const [nr, ng, nb] = Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette); + data[i] = nr; + data[i + 1] = ng; + data[i + 2] = nb; + data[i + 3] = 255; + } + } + + if (jobId !== _previewJobId) return; + baseCtx.putImageData(imgData, 0, 0); + maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); + if (_maskImageData) maskCtx.putImageData(_maskImageData, 0, 0); + updateZoomLayout(); + }; + + const onWidthInput = () => { + if (keepAspect.checked) { + heightSlider.value = Math.round(parseInt(widthSlider.value, 10) / aspectRatio); + } + _updateResizePreview(); + const curW = parseInt(widthSlider.value, 10); + const curH = parseInt(heightSlider.value, 10); + state.resizeSettings = { + baseWidth: width, + baseHeight: height, + width: curW, + height: curH, + }; + saveBotSettings(); + // Auto-fit after size changes + const fit = typeof computeFitZoom === 'function' ? computeFitZoom() : 1; + if (!isNaN(fit) && isFinite(fit)) applyZoom(fit); + }; + + const onHeightInput = () => { + if (keepAspect.checked) { + widthSlider.value = Math.round(parseInt(heightSlider.value, 10) * aspectRatio); + } + _updateResizePreview(); + const curW = parseInt(widthSlider.value, 10); + const curH = parseInt(heightSlider.value, 10); + state.resizeSettings = { + baseWidth: width, + baseHeight: height, + width: curW, + height: curH, + }; + saveBotSettings(); + // Auto-fit after size changes + const fit = typeof computeFitZoom === 'function' ? computeFitZoom() : 1; + if (!isNaN(fit) && isFinite(fit)) applyZoom(fit); + }; + + paintWhiteToggle.onchange = (e) => { + state.paintWhitePixels = e.target.checked; + _updateResizePreview(); + saveBotSettings(); + }; + + paintTransparentToggle.onchange = (e) => { + state.paintTransparentPixels = e.target.checked; + _updateResizePreview(); + saveBotSettings(); + }; + + let panX = 0, + panY = 0; + const clampPan = () => { + const wrapRect = panStage?.getBoundingClientRect() || { + width: 0, + height: 0, + }; + const w = (baseCanvas.width || 1) * _zoomLevel; + const h = (baseCanvas.height || 1) * _zoomLevel; + if (w <= wrapRect.width) { + panX = Math.floor((wrapRect.width - w) / 2); + } else { + const minX = wrapRect.width - w; + panX = Math.min(0, Math.max(minX, panX)); + } + if (h <= wrapRect.height) { + panY = Math.floor((wrapRect.height - h) / 2); + } else { + const minY = wrapRect.height - h; + panY = Math.min(0, Math.max(minY, panY)); + } + }; + let _panRaf = 0; + const applyPan = () => { + if (_panRaf) return; + _panRaf = requestAnimationFrame(() => { + clampPan(); + canvasStack.style.transform = `translate3d(${Math.round( + panX + )}px, ${Math.round(panY)}px, 0) scale(${_zoomLevel})`; + _panRaf = 0; + }); + }; + + const updateZoomLayout = () => { + const w = baseCanvas.width || 1, + h = baseCanvas.height || 1; + baseCanvas.style.width = w + 'px'; + baseCanvas.style.height = h + 'px'; + maskCanvas.style.width = w + 'px'; + maskCanvas.style.height = h + 'px'; + canvasStack.style.width = w + 'px'; + canvasStack.style.height = h + 'px'; + applyPan(); + }; + const applyZoom = (z) => { + _zoomLevel = Math.max(0.05, Math.min(20, z || 1)); + zoomSlider.value = _zoomLevel; + updateZoomLayout(); + if (zoomValue) zoomValue.textContent = `${Math.round(_zoomLevel * 100)}%`; + }; + zoomSlider.addEventListener('input', () => { + applyZoom(parseFloat(zoomSlider.value)); + }); + if (zoomInBtn) + zoomInBtn.addEventListener('click', () => applyZoom(parseFloat(zoomSlider.value) + 0.1)); + if (zoomOutBtn) + zoomOutBtn.addEventListener('click', () => applyZoom(parseFloat(zoomSlider.value) - 0.1)); + const computeFitZoom = () => { + const wrapRect = panStage?.getBoundingClientRect(); + if (!wrapRect) return 1; + const w = baseCanvas.width || 1; + const h = baseCanvas.height || 1; + const margin = 10; + const scaleX = (wrapRect.width - margin) / w; + const scaleY = (wrapRect.height - margin) / h; + return Math.max(0.05, Math.min(20, Math.min(scaleX, scaleY))); + }; + if (zoomFitBtn) + zoomFitBtn.addEventListener('click', () => { + applyZoom(computeFitZoom()); + centerInView(); + }); + if (zoomActualBtn) + zoomActualBtn.addEventListener('click', () => { + applyZoom(1); + centerInView(); + }); + + const centerInView = () => { + if (!panStage) return; + const rect = panStage.getBoundingClientRect(); + const w = (baseCanvas.width || 1) * _zoomLevel; + const h = (baseCanvas.height || 1) * _zoomLevel; + panX = Math.floor((rect.width - w) / 2); + panY = Math.floor((rect.height - h) / 2); + applyPan(); + }; + + let isPanning = false; + let startX = 0, + startY = 0, + startPanX = 0, + startPanY = 0; + let allowPan = false; // Space key + let panMode = false; // Explicit pan mode toggle for touch/one-button mice + const isPanMouseButton = (e) => e.button === 1 || e.button === 2; + const setCursor = (val) => { + if (panStage) panStage.style.cursor = val; + }; + const isPanActive = (e) => panMode || allowPan || isPanMouseButton(e); + const updatePanModeBtn = () => { + if (!panModeBtn) return; + panModeBtn.classList.toggle('active', panMode); + panModeBtn.setAttribute('aria-pressed', panMode ? 'true' : 'false'); + }; + if (panModeBtn) { + updatePanModeBtn(); + panModeBtn.addEventListener('click', () => { + panMode = !panMode; + updatePanModeBtn(); + setCursor(panMode ? 'grab' : ''); + }); + } + if (panStage) { + panStage.addEventListener('contextmenu', (e) => { + if (allowPan) e.preventDefault(); + }); + window.addEventListener('keydown', (e) => { + if (e.code === 'Space') { + allowPan = true; + setCursor('grab'); + } + }); + window.addEventListener('keyup', (e) => { + if (e.code === 'Space') { + allowPan = false; + if (!isPanning) setCursor(''); + } + }); + panStage.addEventListener('mousedown', (e) => { + if (!isPanActive(e)) return; + e.preventDefault(); + isPanning = true; + startX = e.clientX; + startY = e.clientY; + startPanX = panX; + startPanY = panY; + setCursor('grabbing'); + }); + window.addEventListener('mousemove', (e) => { + if (!isPanning) return; + const dx = e.clientX - startX; + const dy = e.clientY - startY; + panX = startPanX + dx; + panY = startPanY + dy; + applyPan(); + }); + window.addEventListener('mouseup', () => { + if (isPanning) { + isPanning = false; + setCursor(allowPan ? 'grab' : ''); + } + }); + panStage.addEventListener( + 'wheel', + (e) => { + if (!e.ctrlKey && !e.metaKey) return; + e.preventDefault(); + const rect = panStage.getBoundingClientRect(); + const cx = e.clientX - rect.left - panX; + const cy = e.clientY - rect.top - panY; + const before = _zoomLevel; + const step = Math.max(0.05, Math.min(0.5, Math.abs(e.deltaY) > 20 ? 0.2 : 0.1)); + const next = Math.max(0.05, Math.min(20, before + (e.deltaY > 0 ? -step : step))); + if (next === before) return; + const scale = next / before; + panX = panX - cx * (scale - 1); + panY = panY - cy * (scale - 1); + applyZoom(next); + }, + { passive: false } + ); + let lastTouchDist = null; + let touchStartTime = 0; + let doubleTapTimer = null; + panStage.addEventListener( + 'touchstart', + (e) => { + if (e.touches.length === 1) { + const t = e.touches[0]; + isPanning = true; + startX = t.clientX; + startY = t.clientY; + startPanX = panX; + startPanY = panY; + setCursor('grabbing'); + const now = Date.now(); + if (now - touchStartTime < 300) { + // double tap -> toggle 100%/fit + const z = Math.abs(_zoomLevel - 1) < 0.01 ? computeFitZoom() : 1; + applyZoom(z); + centerInView(); + if (doubleTapTimer) clearTimeout(doubleTapTimer); + } else { + touchStartTime = now; + doubleTapTimer = setTimeout(() => { + doubleTapTimer = null; + }, 320); + } + } else if (e.touches.length === 2) { + // Pinch start + const [a, b] = e.touches; + lastTouchDist = Math.hypot(b.clientX - a.clientX, b.clientY - a.clientY); + } + }, + { passive: true } + ); + panStage.addEventListener( + 'touchmove', + (e) => { + if (e.touches.length === 1 && isPanning) { + const t = e.touches[0]; + const dx = t.clientX - startX; + const dy = t.clientY - startY; + panX = startPanX + dx; + panY = startPanY + dy; + applyPan(); + } else if (e.touches.length === 2 && lastTouchDist != null) { + e.preventDefault(); + const [a, b] = e.touches; + const dist = Math.hypot(b.clientX - a.clientX, b.clientY - a.clientY); + const rect = panStage.getBoundingClientRect(); + const centerX = (a.clientX + b.clientX) / 2 - rect.left - panX; + const centerY = (a.clientY + b.clientY) / 2 - rect.top - panY; + const before = _zoomLevel; + const scale = dist / (lastTouchDist || dist); + const next = Math.max(0.05, Math.min(20, before * scale)); + if (next !== before) { + panX = panX - centerX * (next / before - 1); + panY = panY - centerY * (next / before - 1); + applyZoom(next); + } + lastTouchDist = dist; + } + }, + { passive: false } + ); + panStage.addEventListener('touchend', () => { + isPanning = false; + lastTouchDist = null; + setCursor(panMode || allowPan ? 'grab' : ''); + }); + } + const schedulePreview = () => { + if (_previewTimer) clearTimeout(_previewTimer); + const run = () => { + _previewTimer = null; + _updateResizePreview(); + }; + if (window.requestIdleCallback) { + _previewTimer = setTimeout(() => requestIdleCallback(run, { timeout: 150 }), 50); + } else { + _previewTimer = setTimeout(() => requestAnimationFrame(run), 50); + } + }; + // Track dragging to reduce work and skip dithering during drag + const markDragStart = () => { + _isDraggingSize = true; + }; + const markDragEnd = () => { + _isDraggingSize = false; + schedulePreview(); + }; + widthSlider.addEventListener('pointerdown', markDragStart); + heightSlider.addEventListener('pointerdown', markDragStart); + widthSlider.addEventListener('pointerup', markDragEnd); + heightSlider.addEventListener('pointerup', markDragEnd); + widthSlider.addEventListener('input', () => { + onWidthInput(); + schedulePreview(); + }); + heightSlider.addEventListener('input', () => { + onHeightInput(); + schedulePreview(); + }); + + // Mask painting UX: brush size, modes, row/column fills, and precise coords + let draggingMask = false; + let lastPaintX = -1, + lastPaintY = -1; + let brushSize = 1; + let rowColSize = 1; + let maskMode = 'ignore'; // 'ignore' | 'unignore' | 'toggle' + const brushEl = resizeContainer.querySelector('#maskBrushSize'); + const brushValEl = resizeContainer.querySelector('#maskBrushSizeValue'); + const btnIgnore = resizeContainer.querySelector('#maskModeIgnore'); + const btnUnignore = resizeContainer.querySelector('#maskModeUnignore'); + const btnToggle = resizeContainer.querySelector('#maskModeToggle'); + const clearIgnoredBtnEl = resizeContainer.querySelector('#clearIgnoredBtn'); + const invertMaskBtn = resizeContainer.querySelector('#invertMaskBtn'); + const rowColSizeEl = resizeContainer.querySelector('#rowColSize'); + const rowColSizeValEl = resizeContainer.querySelector('#rowColSizeValue'); + + const updateModeButtons = () => { + const map = [ + [btnIgnore, 'ignore'], + [btnUnignore, 'unignore'], + [btnToggle, 'toggle'], + ]; + for (const [el, m] of map) { + if (!el) continue; + const active = maskMode === m; + el.classList.toggle('active', active); + el.setAttribute('aria-pressed', active ? 'true' : 'false'); + } + }; + const setMode = (mode) => { + maskMode = mode; + updateModeButtons(); + }; + if (brushEl && brushValEl) { + brushEl.addEventListener('input', () => { + brushSize = parseInt(brushEl.value, 10) || 1; + brushValEl.textContent = brushSize; + }); + brushValEl.textContent = brushEl.value; + brushSize = parseInt(brushEl.value, 10) || 1; + } + if (rowColSizeEl && rowColSizeValEl) { + rowColSizeEl.addEventListener('input', () => { + rowColSize = parseInt(rowColSizeEl.value, 10) || 1; + rowColSizeValEl.textContent = rowColSize; + }); + rowColSizeValEl.textContent = rowColSizeEl.value; + rowColSize = parseInt(rowColSizeEl.value, 10) || 1; + } + if (btnIgnore) btnIgnore.addEventListener('click', () => setMode('ignore')); + if (btnUnignore) btnUnignore.addEventListener('click', () => setMode('unignore')); + if (btnToggle) btnToggle.addEventListener('click', () => setMode('toggle')); + // Initialize button state (default to toggle mode) + updateModeButtons(); + + const mapClientToPixel = (clientX, clientY) => { + // Compute without rounding until final step to avoid drift at higher zoom + const rect = baseCanvas.getBoundingClientRect(); + const scaleX = rect.width / baseCanvas.width; + const scaleY = rect.height / baseCanvas.height; + const dx = (clientX - rect.left) / scaleX; + const dy = (clientY - rect.top) / scaleY; + const x = Math.floor(dx); + const y = Math.floor(dy); + return { x, y }; + }; + + const ensureMask = (w, h) => { + if (!state.resizeIgnoreMask || state.resizeIgnoreMask.length !== w * h) { + state.resizeIgnoreMask = new Uint8Array(w * h); + } + }; + + const paintCircle = (cx, cy, radius, value) => { + const w = baseCanvas.width, + h = baseCanvas.height; + ensureMask(w, h); + const r2 = radius * radius; + for (let yy = cy - radius; yy <= cy + radius; yy++) { + if (yy < 0 || yy >= h) continue; + for (let xx = cx - radius; xx <= cx + radius; xx++) { + if (xx < 0 || xx >= w) continue; + const dx = xx - cx, + dy = yy - cy; + if (dx * dx + dy * dy <= r2) { + const idx = yy * w + xx; + let val = state.resizeIgnoreMask[idx]; + if (maskMode === 'toggle') { + val = val ? 0 : 1; + } else if (maskMode === 'ignore') { + val = 1; + } else { + val = 0; + } + state.resizeIgnoreMask[idx] = val; + if (_maskData) { + const p = idx * 4; + if (val) { + _maskData[p] = 255; + _maskData[p + 1] = 0; + _maskData[p + 2] = 0; + _maskData[p + 3] = 150; + } else { + _maskData[p] = 0; + _maskData[p + 1] = 0; + _maskData[p + 2] = 0; + _maskData[p + 3] = 0; + } + _markDirty(xx, yy); + } + } + } + } + }; + + const paintRow = (y, value) => { + const w = baseCanvas.width, + h = baseCanvas.height; + ensureMask(w, h); + if (y < 0 || y >= h) return; + + // Paint multiple rows based on rowColSize + const halfSize = Math.floor(rowColSize / 2); + const startY = Math.max(0, y - halfSize); + const endY = Math.min(h - 1, y + halfSize); + + for (let rowY = startY; rowY <= endY; rowY++) { + for (let x = 0; x < w; x++) { + const idx = rowY * w + x; + let val = state.resizeIgnoreMask[idx]; + if (maskMode === 'toggle') { + val = val ? 0 : 1; + } else if (maskMode === 'ignore') { + val = 1; + } else { + val = 0; + } + state.resizeIgnoreMask[idx] = val; + if (_maskData) { + const p = idx * 4; + if (val) { + _maskData[p] = 255; + _maskData[p + 1] = 0; + _maskData[p + 2] = 0; + _maskData[p + 3] = 150; + } else { + _maskData[p] = 0; + _maskData[p + 1] = 0; + _maskData[p + 2] = 0; + _maskData[p + 3] = 0; + } + } + } + if (_maskData) { + _markDirty(0, rowY); + _markDirty(w - 1, rowY); + } + } + }; + + const paintColumn = (x, value) => { + const w = baseCanvas.width, + h = baseCanvas.height; + ensureMask(w, h); + if (x < 0 || x >= w) return; + + // Paint multiple columns based on rowColSize + const halfSize = Math.floor(rowColSize / 2); + const startX = Math.max(0, x - halfSize); + const endX = Math.min(w - 1, x + halfSize); + + for (let colX = startX; colX <= endX; colX++) { + for (let y = 0; y < h; y++) { + const idx = y * w + colX; + let val = state.resizeIgnoreMask[idx]; + if (maskMode === 'toggle') { + val = val ? 0 : 1; + } else if (maskMode === 'ignore') { + val = 1; + } else { + val = 0; + } + state.resizeIgnoreMask[idx] = val; + if (_maskData) { + const p = idx * 4; + if (val) { + _maskData[p] = 255; + _maskData[p + 1] = 0; + _maskData[p + 2] = 0; + _maskData[p + 3] = 150; + } else { + _maskData[p] = 0; + _maskData[p + 1] = 0; + _maskData[p + 2] = 0; + _maskData[p + 3] = 0; + } + } + } + if (_maskData) { + _markDirty(colX, 0); + _markDirty(colX, h - 1); + } + } + }; + + const redrawMaskOverlay = () => { + // Only flush the dirty region; full rebuild happens on size change + _flushDirty(); + }; + + const handlePaint = (e) => { + // Suppress painting while panning + if ((e.buttons & 4) === 4 || (e.buttons & 2) === 2 || allowPan) return; + const { x, y } = mapClientToPixel(e.clientX, e.clientY); + const w = baseCanvas.width, + h = baseCanvas.height; + if (x < 0 || y < 0 || x >= w || y >= h) return; + const radius = Math.max(1, Math.floor(brushSize / 2)); + if (e.shiftKey) { + paintRow(y); + } else if (e.altKey) { + paintColumn(x); + } else { + paintCircle(x, y, radius); } - ` - } - - // Helper function to update data management buttons - updateDataButtons = () => { - const hasImageData = state.imageLoaded && state.imageData - saveBtn.disabled = !hasImageData - saveToFileBtn.disabled = !hasImageData - } - - // Initialize data buttons state - updateDataButtons() - - function showResizeDialog(processor) { - const { width, height } = processor.getDimensions() - const aspectRatio = width / height - - widthSlider.value = width - heightSlider.value = height - widthValue.textContent = width - heightValue.textContent = height - resizePreview.src = processor.img.src - - resizeOverlay.style.display = "block" - resizeContainer.style.display = "block" - - const updatePreview = () => { - const newWidth = Number.parseInt(widthSlider.value) - const newHeight = Number.parseInt(heightSlider.value) - - widthValue.textContent = newWidth - heightValue.textContent = newHeight - - resizePreview.src = processor.generatePreview(newWidth, newHeight) - } - - widthSlider.addEventListener("input", () => { - if (keepAspect.checked) { - const newWidth = Number.parseInt(widthSlider.value) - const newHeight = Math.round(newWidth / aspectRatio) - heightSlider.value = newHeight + lastPaintX = x; + lastPaintY = y; + redrawMaskOverlay(); + }; + + maskCanvas.addEventListener('mousedown', (e) => { + if (e.button === 1 || e.button === 2 || allowPan) return; // let pan handler manage + draggingMask = true; + handlePaint(e); + }); + // Avoid hijacking touch gestures for panning/zooming + maskCanvas.addEventListener( + 'touchstart', + (e) => { + /* let panStage handle */ + }, + { passive: true } + ); + maskCanvas.addEventListener( + 'touchmove', + (e) => { + /* let panStage handle */ + }, + { passive: true } + ); + maskCanvas.addEventListener( + 'touchend', + (e) => { + /* let panStage handle */ + }, + { passive: true } + ); + window.addEventListener('mousemove', (e) => { + if (draggingMask) handlePaint(e); + }); + window.addEventListener('mouseup', () => { + if (draggingMask) { + draggingMask = false; + saveBotSettings(); } - updatePreview() - }) - - heightSlider.addEventListener("input", () => { - if (keepAspect.checked) { - const newHeight = Number.parseInt(heightSlider.value) - const newWidth = Math.round(newHeight * aspectRatio) - widthSlider.value = newWidth + }); + + if (clearIgnoredBtnEl) + clearIgnoredBtnEl.addEventListener('click', () => { + const w = baseCanvas.width, + h = baseCanvas.height; + if (state.resizeIgnoreMask) state.resizeIgnoreMask.fill(0); + _ensureMaskOverlayBuffers(w, h, true); + _updateResizePreview(); + saveBotSettings(); + }); + + if (invertMaskBtn) + invertMaskBtn.addEventListener('click', () => { + if (!state.resizeIgnoreMask) return; + for (let i = 0; i < state.resizeIgnoreMask.length; i++) + state.resizeIgnoreMask[i] = state.resizeIgnoreMask[i] ? 0 : 1; + const w = baseCanvas.width, + h = baseCanvas.height; + _ensureMaskOverlayBuffers(w, h, true); + _updateResizePreview(); + saveBotSettings(); + }); + + confirmResize.onclick = async () => { + const newWidth = parseInt(widthSlider.value, 10); + const newHeight = parseInt(heightSlider.value, 10); + + // Generate the final paletted image data + const tempCanvas = document.createElement('canvas'); + const tempCtx = tempCanvas.getContext('2d'); + tempCanvas.width = newWidth; + tempCanvas.height = newHeight; + tempCtx.imageSmoothingEnabled = false; + if (baseProcessor !== processor && (!baseProcessor.img || !baseProcessor.canvas)) { + await baseProcessor.load(); } - updatePreview() - }) - - confirmResize.onclick = () => { - const newWidth = Number.parseInt(widthSlider.value) - const newHeight = Number.parseInt(heightSlider.value) - - const newPixels = processor.resize(newWidth, newHeight) - - let totalValidPixels = 0 - for (let y = 0; y < newHeight; y++) { - for (let x = 0; x < newWidth; x++) { - const idx = (y * newWidth + x) * 4 - const r = newPixels[idx] - const g = newPixels[idx + 1] - const b = newPixels[idx + 2] - const alpha = newPixels[idx + 3] + tempCtx.drawImage(baseProcessor.img, 0, 0, newWidth, newHeight); + const imgData = tempCtx.getImageData(0, 0, newWidth, newHeight); + const data = imgData.data; + const tThresh2 = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; + let totalValidPixels = 0; + const mask = + state.resizeIgnoreMask && state.resizeIgnoreMask.length === newWidth * newHeight + ? state.resizeIgnoreMask + : null; + + const applyFSDitherFinal = async () => { + const w = newWidth, + h = newHeight; + const n = w * h; + const { work, eligible } = ensureDitherBuffers(n); + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const idx = y * w + x; + const i4 = idx * 4; + const r = data[i4], + g = data[i4 + 1], + b = data[i4 + 2], + a = data[i4 + 3]; + const masked = mask && mask[idx]; + const isEligible = + !masked && + (state.paintTransparentPixels || a >= tThresh2) && + (state.paintWhitePixels || !Utils.isWhitePixel(r, g, b)); + eligible[idx] = isEligible ? 1 : 0; + work[idx * 3] = r; + work[idx * 3 + 1] = g; + work[idx * 3 + 2] = b; + if (!isEligible) { + data[i4 + 3] = 0; + } + } + // Yield to keep UI responsive + if ((y & 15) === 0) await Promise.resolve(); + } - if (alpha < CONFIG.TRANSPARENCY_THRESHOLD) continue - if (Utils.isWhitePixel(r, g, b)) continue + const diffuse = (nx, ny, er, eg, eb, factor) => { + if (nx < 0 || nx >= w || ny < 0 || ny >= h) return; + const nidx = ny * w + nx; + if (!eligible[nidx]) return; + const base = nidx * 3; + work[base] = Math.min(255, Math.max(0, work[base] + er * factor)); + work[base + 1] = Math.min(255, Math.max(0, work[base + 1] + eg * factor)); + work[base + 2] = Math.min(255, Math.max(0, work[base + 2] + eb * factor)); + }; + + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const idx = y * w + x; + if (!eligible[idx]) continue; + const base = idx * 3; + const r0 = work[base], + g0 = work[base + 1], + b0 = work[base + 2]; + const [nr, ng, nb] = Utils.findClosestPaletteColor( + r0, + g0, + b0, + state.activeColorPalette + ); + const i4 = idx * 4; + data[i4] = nr; + data[i4 + 1] = ng; + data[i4 + 2] = nb; + data[i4 + 3] = 255; + totalValidPixels++; + + const er = r0 - nr; + const eg = g0 - ng; + const eb = b0 - nb; + + diffuse(x + 1, y, er, eg, eb, 7 / 16); + diffuse(x - 1, y + 1, er, eg, eb, 3 / 16); + diffuse(x, y + 1, er, eg, eb, 5 / 16); + diffuse(x + 1, y + 1, er, eg, eb, 1 / 16); + } + // Yield every row to reduce jank + await Promise.resolve(); + } + }; - totalValidPixels++ + if (state.ditheringEnabled) { + await applyFSDitherFinal(); + } else { + for (let i = 0; i < data.length; i += 4) { + const r = data[i], + g = data[i + 1], + b = data[i + 2], + a = data[i + 3]; + const masked = mask && mask[i >> 2]; + const isTransparent = (!state.paintTransparentPixels && a < tThresh2) || masked; + const isWhiteAndSkipped = !state.paintWhitePixels && Utils.isWhitePixel(r, g, b); + if (isTransparent || isWhiteAndSkipped) { + data[i + 3] = 0; // overlay transparency + continue; + } + totalValidPixels++; + const [nr, ng, nb] = Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette); + data[i] = nr; + data[i + 1] = ng; + data[i + 2] = nb; + data[i + 3] = 255; } } - - state.imageData.pixels = newPixels - state.imageData.width = newWidth - state.imageData.height = newHeight - state.imageData.totalPixels = totalValidPixels - state.totalPixels = totalValidPixels - state.paintedPixels = 0 - - updateStats() - updateUI("resizeSuccess", "success", { + tempCtx.putImageData(imgData, 0, 0); + + // Save the final pixel data for painting + // Persist the paletted (and possibly dithered) pixels so painting uses the same output seen in overlay + const palettedPixels = new Uint8ClampedArray(imgData.data); + state.imageData.pixels = palettedPixels; + state.imageData.width = newWidth; + state.imageData.height = newHeight; + state.imageData.totalPixels = totalValidPixels; + state.totalPixels = totalValidPixels; + state.paintedPixels = 0; + + state.resizeSettings = { + baseWidth: width, + baseHeight: height, width: newWidth, height: newHeight, - }) + }; + saveBotSettings(); - closeResizeDialog() - } + const finalImageBitmap = await createImageBitmap(tempCanvas); + await overlayManager.setImage(finalImageBitmap); + overlayManager.enable(); + toggleOverlayBtn.classList.add('active'); + toggleOverlayBtn.setAttribute('aria-pressed', 'true'); - cancelResize.onclick = closeResizeDialog - } + // Keep state.imageData.processor as the original-based source; painting uses paletted pixels already stored - function closeResizeDialog() { - resizeOverlay.style.display = "none" - resizeContainer.style.display = "none" - } + updateStats(); + updateUI('resizeSuccess', 'success', { + width: newWidth, + height: newHeight, + }); + closeResizeDialog(); + }; - if (initBotBtn) { - initBotBtn.addEventListener("click", async () => { + downloadPreviewBtn.onclick = () => { try { - updateUI("checkingColors", "default") + const w = baseCanvas.width, + h = baseCanvas.height; + const out = document.createElement('canvas'); + out.width = w; + out.height = h; + const octx = out.getContext('2d'); + octx.imageSmoothingEnabled = false; + octx.drawImage(baseCanvas, 0, 0); + octx.drawImage(maskCanvas, 0, 0); + const link = document.createElement('a'); + link.download = 'wplace-preview.png'; + link.href = out.toDataURL(); + link.click(); + } catch (e) { + console.warn('Failed to download preview:', e); + } + }; - state.availableColors = Utils.extractAvailableColors() + cancelResize.onclick = closeResizeDialog; - if (state.availableColors.length === 0) { - Utils.showAlert(Utils.t("noColorsFound"), "error") - updateUI("noColorsFound", "error") - return - } + resizeOverlay.style.display = 'block'; + resizeContainer.style.display = 'block'; - state.colorsChecked = true - uploadBtn.disabled = false - selectPosBtn.disabled = false - initBotBtn.style.display = "none" + // Reinitialize color palette with current available colors + initializeColorPalette(resizeContainer); - updateUI("colorsFound", "success", { - count: state.availableColors.length, - }) - updateStats() - } catch { - updateUI("imageError", "error") + _updateResizePreview(); + _resizeDialogCleanup = () => { + try { + zoomSlider.replaceWith(zoomSlider.cloneNode(true)); + } catch {} + try { + if (zoomInBtn) zoomInBtn.replaceWith(zoomInBtn.cloneNode(true)); + } catch {} + try { + if (zoomOutBtn) zoomOutBtn.replaceWith(zoomOutBtn.cloneNode(true)); + } catch {} + }; + setTimeout(() => { + if (typeof computeFitZoom === 'function') { + const z = computeFitZoom(); + if (!isNaN(z) && isFinite(z)) { + applyZoom(z); + centerInView(); + } + } else { + centerInView(); } - }) + }, 0); } - if (uploadBtn) { - uploadBtn.addEventListener("click", async () => { - try { - updateUI("loadingImage", "default") - const imageSrc = await Utils.createImageUploader() - - const processor = new ImageProcessor(imageSrc) - await processor.load() + function closeResizeDialog() { + try { + if (typeof _resizeDialogCleanup === 'function') { + _resizeDialogCleanup(); + } + } catch {} + resizeOverlay.style.display = 'none'; + resizeContainer.style.display = 'none'; + _updateResizePreview = () => {}; + try { + if (typeof cancelAnimationFrame === 'function' && _panRaf) { + cancelAnimationFrame(_panRaf); + } + } catch {} + try { + if (_previewTimer) { + clearTimeout(_previewTimer); + _previewTimer = null; + } + } catch {} + _maskImageData = null; + _maskData = null; + _dirty = null; + _ditherWorkBuf = null; + _ditherEligibleBuf = null; + _resizeDialogCleanup = null; + } - const { width, height } = processor.getDimensions() - const pixels = processor.getPixelData() + if (uploadBtn) { + uploadBtn.addEventListener('click', async () => { + const availableColors = Utils.extractAvailableColors(); + if (availableColors === null || availableColors.length < 10) { + updateUI('noColorsFound', 'error'); + Utils.showAlert(Utils.t('noColorsFound'), 'error'); + return; + } - let totalValidPixels = 0 - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const idx = (y * width + x) * 4 - const r = pixels[idx] - const g = pixels[idx + 1] - const b = pixels[idx + 2] - const alpha = pixels[idx + 3] + if (!state.colorsChecked) { + state.availableColors = availableColors; + state.colorsChecked = true; + updateUI('colorsFound', 'success', { count: availableColors.length }); + updateStats(); + selectPosBtn.disabled = false; + // Only enable resize button if image is also loaded + if (state.imageLoaded) { + resizeBtn.disabled = false; + } + } - if (alpha < CONFIG.TRANSPARENCY_THRESHOLD) continue - if (Utils.isWhitePixel(r, g, b)) continue + try { + updateUI('loadingImage', 'default'); + const imageSrc = await Utils.createImageUploader(); + if (!imageSrc) { + updateUI('colorsFound', 'success', { + count: state.availableColors.length, + }); + return; + } - totalValidPixels++ + const processor = new ImageProcessor(imageSrc); + await processor.load(); + + const { width, height } = processor.getDimensions(); + const pixels = processor.getPixelData(); + + let totalValidPixels = 0; + for (let i = 0; i < pixels.length; i += 4) { + const isTransparent = + !state.paintTransparentPixels && + pixels[i + 3] < (state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD); + const isWhiteAndSkipped = + !state.paintWhitePixels && + Utils.isWhitePixel(pixels[i], pixels[i + 1], pixels[i + 2]); + if (!isTransparent && !isWhiteAndSkipped) { + totalValidPixels++; } } @@ -2174,367 +6508,1400 @@ pixels, totalPixels: totalValidPixels, processor, + }; + + state.totalPixels = totalValidPixels; + state.paintedPixels = 0; + state.imageLoaded = true; + state.lastPosition = { x: 0, y: 0 }; + + // Initialize painted map for tracking + Utils.initializePaintedMap(width, height); + + // New image: clear previous resize settings + state.resizeSettings = null; + // Also clear any previous ignore mask + state.resizeIgnoreMask = null; + // Save original image for this browser (dataUrl + dims) + state.originalImage = { dataUrl: imageSrc, width, height }; + saveBotSettings(); + + // Use the original image for the overlay initially + const imageBitmap = await createImageBitmap(processor.img); + await overlayManager.setImage(imageBitmap); + overlayManager.enable(); + toggleOverlayBtn.disabled = false; + toggleOverlayBtn.classList.add('active'); + toggleOverlayBtn.setAttribute('aria-pressed', 'true'); + + // Only enable resize button if colors have also been captured + if (state.colorsChecked) { + resizeBtn.disabled = false; } - - state.totalPixels = totalValidPixels - state.paintedPixels = 0 - state.imageLoaded = true - state.lastPosition = { x: 0, y: 0 } - - resizeBtn.disabled = false - saveBtn.disabled = false + saveBtn.disabled = false; if (state.startPosition) { - startBtn.disabled = false + startBtn.disabled = false; } - updateStats() - updateDataButtons() - updateUI("imageLoaded", "success", { count: totalValidPixels }) + updateStats(); + updateDataButtons(); + updateUI('imageLoaded', 'success', { count: totalValidPixels }); } catch { - updateUI("imageError", "error") + updateUI('imageError', 'error'); } - }) + }); } if (resizeBtn) { - resizeBtn.addEventListener("click", () => { - if (state.imageLoaded && state.imageData.processor) { - showResizeDialog(state.imageData.processor) + resizeBtn.addEventListener('click', () => { + if (state.imageLoaded && state.imageData.processor && state.colorsChecked) { + showResizeDialog(state.imageData.processor); + } else if (!state.colorsChecked) { + Utils.showAlert(Utils.t('uploadImageFirstColors'), 'warning'); } - }) + }); } if (selectPosBtn) { - selectPosBtn.addEventListener("click", async () => { - if (state.selectingPosition) return + selectPosBtn.addEventListener('click', async () => { + if (state.selectingPosition) return; - state.selectingPosition = true - state.startPosition = null - state.region = null - startBtn.disabled = true + state.selectingPosition = true; + state.startPosition = null; + state.region = null; + startBtn.disabled = true; - Utils.showAlert(Utils.t("selectPositionAlert"), "info") - updateUI("waitingPosition", "default") + Utils.showAlert(Utils.t('selectPositionAlert'), 'info'); + updateUI('waitingPosition', 'default'); - const originalFetch = window.fetch - - window.fetch = async (url, options) => { + const tempFetch = async (url, options) => { if ( - typeof url === "string" && - url.includes("https://backend.wplace.live/s0/pixel/") && - options?.method?.toUpperCase() === "POST" + typeof url === 'string' && + url.includes('https://backend.wplace.live/s0/pixel/') && + options?.method?.toUpperCase() === 'POST' ) { try { - const response = await originalFetch(url, options) - const clonedResponse = response.clone() - const data = await clonedResponse.json() + const response = await originalFetch(url, options); + const clonedResponse = response.clone(); + const data = await clonedResponse.json(); if (data?.painted === 1) { - const regionMatch = url.match(/\/pixel\/(\d+)\/(\d+)/) + const regionMatch = url.match(/\/pixel\/(\d+)\/(\d+)/); if (regionMatch && regionMatch.length >= 3) { state.region = { x: Number.parseInt(regionMatch[1]), y: Number.parseInt(regionMatch[2]), - } + }; } - const payload = JSON.parse(options.body) + const payload = JSON.parse(options.body); if (payload?.coords && Array.isArray(payload.coords)) { state.startPosition = { x: payload.coords[0], y: payload.coords[1], - } - state.lastPosition = { x: 0, y: 0 } + }; + state.lastPosition = { x: 0, y: 0 }; + + await overlayManager.setPosition(state.startPosition, state.region); if (state.imageLoaded) { - startBtn.disabled = false + startBtn.disabled = false; } - window.fetch = originalFetch - state.selectingPosition = false - updateUI("positionSet", "success") + window.fetch = originalFetch; + state.selectingPosition = false; + updateUI('positionSet', 'success'); } } - return response + return response; } catch { - return originalFetch(url, options) + return originalFetch(url, options); } } - return originalFetch(url, options) - } + return originalFetch(url, options); + }; + + const originalFetch = window.fetch; + window.fetch = tempFetch; setTimeout(() => { if (state.selectingPosition) { - window.fetch = originalFetch - state.selectingPosition = false - updateUI("positionTimeout", "error") - Utils.showAlert(Utils.t("positionTimeout"), "error") + window.fetch = originalFetch; + state.selectingPosition = false; + updateUI('positionTimeout', 'error'); + Utils.showAlert(Utils.t('positionTimeout'), 'error'); } - }, 120000) - }) + }, 120000); + }); } - // Function to start painting (can be called programmatically) async function startPainting() { if (!state.imageLoaded || !state.startPosition || !state.region) { - updateUI("missingRequirements", "error") - return false - } - if (!capturedCaptchaToken) { - updateUI("captchaNeeded", "error") - Utils.showAlert(Utils.t("captchaNeeded"), "error") - return false + updateUI('missingRequirements', 'error'); + return; } + await ensureToken(); + if (!turnstileToken) return; - state.running = true - state.stopFlag = false - startBtn.disabled = true - stopBtn.disabled = false - uploadBtn.disabled = true - selectPosBtn.disabled = true - resizeBtn.disabled = true - saveBtn.disabled = true + state.running = true; + state.stopFlag = false; + startBtn.disabled = true; + stopBtn.disabled = false; + uploadBtn.disabled = true; + selectPosBtn.disabled = true; + resizeBtn.disabled = true; + saveBtn.disabled = true; + toggleOverlayBtn.disabled = true; - updateUI("startPaintingMsg", "success") + updateUI('startPaintingMsg', 'success'); try { - await processImage() - return true - } catch { - updateUI("paintingError", "error") - return false + await processImage(); + } catch (e) { + console.error('Unexpected error:', e); + updateUI('paintingError', 'error'); } finally { - state.running = false - stopBtn.disabled = true - saveBtn.disabled = false - - if (!state.stopFlag) { - startBtn.disabled = true - uploadBtn.disabled = false - selectPosBtn.disabled = false - resizeBtn.disabled = false + state.running = false; + stopBtn.disabled = true; + saveBtn.disabled = false; + + if (state.stopFlag) { + startBtn.disabled = false; } else { - startBtn.disabled = false + startBtn.disabled = true; + uploadBtn.disabled = false; + selectPosBtn.disabled = false; + resizeBtn.disabled = false; } + toggleOverlayBtn.disabled = false; } } if (startBtn) { - startBtn.addEventListener("click", startPainting) + startBtn.addEventListener('click', startPainting); } if (stopBtn) { - stopBtn.addEventListener("click", () => { - state.stopFlag = true - state.running = false - stopBtn.disabled = true - updateUI("paintingStopped", "warning") + stopBtn.addEventListener('click', () => { + state.stopFlag = true; + state.running = false; + stopBtn.disabled = true; + updateUI('paintingStoppedByUser', 'warning'); - // Auto save when stopping if (state.imageLoaded && state.paintedPixels > 0) { - Utils.saveProgress() - Utils.showAlert(Utils.t("autoSaved"), "success") + Utils.saveProgress(); + Utils.showAlert(Utils.t('autoSaved'), 'success'); } - }) + }); } - // Check for saved progress on startup const checkSavedProgress = () => { - const savedData = Utils.loadProgress() + const savedData = Utils.loadProgress(); if (savedData && savedData.state.paintedPixels > 0) { - const savedDate = new Date(savedData.timestamp).toLocaleString() - const progress = Math.round((savedData.state.paintedPixels / savedData.state.totalPixels) * 100) + const savedDate = new Date(savedData.timestamp).toLocaleString(); + const progress = Math.round( + (savedData.state.paintedPixels / savedData.state.totalPixels) * 100 + ); Utils.showAlert( - `${Utils.t("savedDataFound")}\n\n` + + `${Utils.t('savedDataFound')}\n\n` + `Saved: ${savedDate}\n` + `Progress: ${savedData.state.paintedPixels}/${savedData.state.totalPixels} pixels (${progress}%)\n` + - `${Utils.t("clickLoadToContinue")}`, - "info", - ) + `${Utils.t('clickLoadToContinue')}`, + 'info' + ); } + }; + + setTimeout(checkSavedProgress, 1000); + + if (cooldownSlider && cooldownInput && cooldownValue && cooldownDecrease && cooldownIncrease) { + const updateCooldown = (newValue) => { + const threshold = Math.max(1, Math.min(state.maxCharges || 999, parseInt(newValue))); + state.cooldownChargeThreshold = threshold; + + // Update both controls (value shows in input, label shows unit only) + cooldownSlider.value = threshold; + cooldownInput.value = threshold; + cooldownValue.textContent = `${Utils.t('charges')}`; + + saveBotSettings(); + NotificationManager.resetEdgeTracking(); // prevent spurious notify after threshold change + }; + + // Slider event listener + cooldownSlider.addEventListener('input', (e) => { + updateCooldown(e.target.value); + }); + + // Number input event listener + cooldownInput.addEventListener('input', (e) => { + updateCooldown(e.target.value); + }); + + // Decrease button + cooldownDecrease.addEventListener('click', () => { + updateCooldown(parseInt(cooldownInput.value) - 1); + }); + + // Increase button + cooldownIncrease.addEventListener('click', () => { + updateCooldown(parseInt(cooldownInput.value) + 1); + }); + + // Add scroll-to-adjust for cooldown slider + Utils.createScrollToAdjust(cooldownSlider, updateCooldown, 1, state.maxCharges, 1); } - // Check for saved progress after a short delay to let UI settle - setTimeout(checkSavedProgress, 1000) - - const themeBtn = container.querySelector("#themeBtn") - if (themeBtn) { - themeBtn.addEventListener("click", () => { - createThemePopup() - }) - } + loadBotSettings(); + // Ensure notification poller reflects current settings + NotificationManager.syncFromState(); } - async function processImage() { - const { width, height, pixels } = state.imageData - const { x: startX, y: startY } = state.startPosition - const { x: regionX, y: regionY } = state.region + function getMsToTargetCharges(current, target, cooldown, intervalMs = 0) { + const remainingCharges = target - current; + return Math.max(0, remainingCharges * cooldown - intervalMs); + } - const startRow = state.lastPosition.y || 0 - const startCol = state.lastPosition.x || 0 + function updateChargesThresholdUI(intervalMs) { + if (state.stopFlag) return; + + const threshold = state.cooldownChargeThreshold; + const remainingMs = getMsToTargetCharges( + state.preciseCurrentCharges, + threshold, + state.cooldown, + intervalMs + ); + const timeText = Utils.msToTimeText(remainingMs); + + updateUI( + 'noChargesThreshold', + 'warning', + { + threshold, + current: state.displayCharges, + time: timeText, + }, + true + ); + } - if (!state.paintedMap) { - state.paintedMap = Array(height) - .fill() - .map(() => Array(width).fill(false)) + function generateCoordinates(width, height, mode, direction, snake, blockWidth, blockHeight) { + const coords = []; + console.log( + 'Generating coordinates with \n mode:', + mode, + '\n direction:', + direction, + '\n snake:', + snake, + '\n blockWidth:', + blockWidth, + '\n blockHeight:', + blockHeight + ); + // --------- Standard 4 corners traversal ---------- + let xStart, xEnd, xStep; + let yStart, yEnd, yStep; + switch (direction) { + case 'top-left': + xStart = 0; + xEnd = width; + xStep = 1; + yStart = 0; + yEnd = height; + yStep = 1; + break; + case 'top-right': + xStart = width - 1; + xEnd = -1; + xStep = -1; + yStart = 0; + yEnd = height; + yStep = 1; + break; + case 'bottom-left': + xStart = 0; + xEnd = width; + xStep = 1; + yStart = height - 1; + yEnd = -1; + yStep = -1; + break; + case 'bottom-right': + xStart = width - 1; + xEnd = -1; + xStep = -1; + yStart = height - 1; + yEnd = -1; + yStep = -1; + break; + default: + throw new Error(`Unknown direction: ${direction}`); } - let pixelBatch = [] - - try { - outerLoop: for (let y = startRow; y < height; y++) { - for (let x = y === startRow ? startCol : 0; x < width; x++) { - if (state.stopFlag) { - if (pixelBatch.length > 0) { - await sendPixelBatch(pixelBatch, regionX, regionY) + // --------- Traversal modes ---------- + if (mode === 'rows') { + for (let y = yStart; y !== yEnd; y += yStep) { + if (snake && (y - yStart) % 2 !== 0) { + for (let x = xEnd - xStep; x !== xStart - xStep; x -= xStep) { + coords.push([x, y]); + } + } else { + for (let x = xStart; x !== xEnd; x += xStep) { + coords.push([x, y]); + } + } + } + } else if (mode === 'columns') { + for (let x = xStart; x !== xEnd; x += xStep) { + if (snake && (x - xStart) % 2 !== 0) { + for (let y = yEnd - yStep; y !== yStart - yStep; y -= yStep) { + coords.push([x, y]); + } + } else { + for (let y = yStart; y !== yEnd; y += yStep) { + coords.push([x, y]); + } + } + } + } else if (mode === 'circle-out') { + const cx = Math.floor(width / 2); + const cy = Math.floor(height / 2); + const maxRadius = Math.ceil(Math.sqrt(cx * cx + cy * cy)); + + for (let r = 0; r <= maxRadius; r++) { + for (let y = cy - r; y <= cy + r; y++) { + for (let x = cx - r; x <= cx + r; x++) { + if (x >= 0 && x < width && y >= 0 && y < height) { + const dist = Math.max(Math.abs(x - cx), Math.abs(y - cy)); + if (dist === r) coords.push([x, y]); + } + } + } + } + } else if (mode === 'circle-in') { + const cx = Math.floor(width / 2); + const cy = Math.floor(height / 2); + const maxRadius = Math.ceil(Math.sqrt(cx * cx + cy * cy)); + + for (let r = maxRadius; r >= 0; r--) { + for (let y = cy - r; y <= cy + r; y++) { + for (let x = cx - r; x <= cx + r; x++) { + if (x >= 0 && x < width && y >= 0 && y < height) { + const dist = Math.max(Math.abs(x - cx), Math.abs(y - cy)); + if (dist === r) coords.push([x, y]); + } + } + } + } + } else if (mode === 'blocks' || mode === 'shuffle-blocks') { + const blocks = []; + for (let by = 0; by < height; by += blockHeight) { + for (let bx = 0; bx < width; bx += blockWidth) { + const block = []; + for (let y = by; y < Math.min(by + blockHeight, height); y++) { + for (let x = bx; x < Math.min(bx + blockWidth, width); x++) { + block.push([x, y]); } - state.lastPosition = { x, y } - updateUI("paintingPaused", "warning", { x, y }) - break outerLoop } + blocks.push(block); + } + } - if (state.paintedMap[y][x]) continue + if (mode === 'shuffle-blocks') { + // Simple Fisher-Yates shuffle + for (let i = blocks.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [blocks[i], blocks[j]] = [blocks[j], blocks[i]]; + } + } - const idx = (y * width + x) * 4 - const r = pixels[idx] - const g = pixels[idx + 1] - const b = pixels[idx + 2] - const alpha = pixels[idx + 3] + // Concatenate all blocks + for (const block of blocks) { + coords.push(...block); + } + } else { + throw new Error(`Unknown mode: ${mode}`); + } - if (alpha < CONFIG.TRANSPARENCY_THRESHOLD) continue - if (Utils.isWhitePixel(r, g, b)) continue + return coords; + } - const rgb = [r, g, b] - const colorId = findClosestColor(rgb, state.availableColors) - const pixelX = startX + x - const pixelY = startY + y + async function flushPixelBatch(pixelBatch) { + if (!pixelBatch || pixelBatch.pixels.length === 0) return true; + + const batchSize = pixelBatch.pixels.length; + console.log( + `📦 Sending batch with ${batchSize} pixels (region: ${pixelBatch.regionX},${pixelBatch.regionY})` + ); + const success = await sendBatchWithRetry( + pixelBatch.pixels, + pixelBatch.regionX, + pixelBatch.regionY + ); + if (success) { + pixelBatch.pixels.forEach((p) => { + state.paintedPixels++; + Utils.markPixelPainted(p.x, p.y, pixelBatch.regionX, pixelBatch.regionY); + }); + state.fullChargeData = { + ...state.fullChargeData, + spentSinceShot: state.fullChargeData.spentSinceShot + batchSize, + }; + updateStats(); + updateUI('paintingProgress', 'default', { + painted: state.paintedPixels, + total: state.totalPixels, + }); + Utils.performSmartSave(); + + if (CONFIG.PAINTING_SPEED_ENABLED && state.paintingSpeed > 0 && batchSize > 0) { + const delayPerPixel = 1000 / state.paintingSpeed; + const totalDelay = Math.max(100, delayPerPixel * batchSize); + await Utils.sleep(totalDelay); + } + } else { + console.error(`❌ Batch failed permanently after retries. Stopping painting.`); + state.stopFlag = true; + updateUI('paintingBatchFailed', 'error'); + } - pixelBatch.push({ - x: pixelX, - y: pixelY, - color: colorId, - localX: x, - localY: y, - }) + pixelBatch.pixels = []; + return success; + } - if (pixelBatch.length >= Math.floor(state.currentCharges)) { - const success = await sendPixelBatch(pixelBatch, regionX, regionY) + async function processImage() { + const { width, height, pixels } = state.imageData; + const { x: startX, y: startY } = state.startPosition; + const { x: regionX, y: regionY } = state.region; + + // todo force load tiles + const tilesReady = await overlayManager.waitForTiles( + regionX, + regionY, + width, + height, + startX, + startY, + 10000 // timeout 10s + ); + + if (!tilesReady) { + updateUI('overlayTilesNotLoaded', 'error'); + state.stopFlag = true; + return; + } - if (success === "token_error") { - state.stopFlag = true - updateUI("captchaNeeded", "error") - Utils.showAlert(Utils.t("captchaNeeded"), "error") - break outerLoop - } + let pixelBatch = null; + let skippedPixels = { + transparent: 0, + white: 0, + alreadyPainted: 0, + colorUnavailable: 0, + }; + + const transparencyThreshold = + state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; + + function checkPixelEligibility(x, y) { + const idx = (y * width + x) * 4; + const r = pixels[idx], + g = pixels[idx + 1], + b = pixels[idx + 2], + a = pixels[idx + 3]; + + if (!state.paintTransparentPixels && a < transparencyThreshold) + return { + eligible: false, + reason: 'transparent', + }; + if (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b)) + return { + eligible: false, + reason: 'white', + }; + + let targetRgb = Utils.isWhitePixel(r, g, b) + ? [255, 255, 255] + : Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette); + + // Template color ID, normalized/mapped to the nearest available color in our palette. + // Example: template requires "Slate", but we only have "Dark Gray" available + // → mappedTargetColorId = ID of Dark Gray. + // + // If `state.paintUnavailablePixels` is enabled, the painting would stop earlier + // because "Slate" was not found (null returned). + // + // Else, the template "Slate" is mapped to the closest available color (e.g., "Dark Gray"), + // and we proceed with painting using that mapped color. + // + // In this case, if the canvas pixel is already Slate (mapped to available Dark Gray), + // we skip painting, since template and canvas both resolve to the same available color (Dark Gray). + const mappedTargetColorId = Utils.resolveColor( + targetRgb, + state.availableColors, + !state.paintUnavailablePixels + ); + + // Technically, checking only `!mappedTargetColorId.id` would be enough, + // but combined with `state.paintUnavailablePixels` it makes the logic explicit: + // we only skip when the template color cannot be mapped AND strict mode is on. + if (!state.paintUnavailablePixels && !mappedTargetColorId.id) { + return { + eligible: false, + reason: 'colorUnavailable', + r, + g, + b, + a, + mappedColorId: mappedTargetColorId.id, + }; + } + return { eligible: true, r, g, b, a, mappedColorId: mappedTargetColorId.id }; + } + + function skipPixel(reason, id, rgb, x, y) { + if (reason !== 'transparent') { + console.log(`Skipped pixel for ${reason} (id: ${id}, (${rgb.join(', ')})) at (${x}, ${y})`); + } + skippedPixels[reason]++; + } + + try { + const coords = generateCoordinates( + width, + height, + state.coordinateMode, + state.coordinateDirection, + state.coordinateSnake, + state.blockWidth, + state.blockHeight + ); + + outerLoop: for (const [x, y] of coords) { + if (state.stopFlag) { + if (pixelBatch && pixelBatch.pixels.length > 0) { + console.log( + `🎯 Sending last batch before stop with ${pixelBatch.pixels.length} pixels` + ); + await flushPixelBatch(pixelBatch); + } + state.lastPosition = { x, y }; + updateUI('paintingPaused', 'warning', { x, y }); + // noinspection UnnecessaryLabelOnBreakStatementJS + break outerLoop; + } + + const targetPixelInfo = checkPixelEligibility(x, y); + 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; + + // 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; + } + + // console.log(`[DEBUG] Pixel at (${pixelX}, ${pixelY}) eligible: RGB=${targetPixelInfo.r}, ${targetPixelInfo.g}, ${targetPixelInfo.b}, + // alpha=${targetPixelInfo.a}, mappedColorId=${targetMappedColorId}`); + + if ( + !pixelBatch || + pixelBatch.regionX !== regionX + adderX || + pixelBatch.regionY !== regionY + adderY + ) { + if (pixelBatch && pixelBatch.pixels.length > 0) { + console.log( + `🌍 Sending region-change batch with ${pixelBatch.pixels.length} pixels (switching to region ${ + regionX + adderX + },${regionY + adderY})` + ); + const success = await flushPixelBatch(pixelBatch); if (success) { - pixelBatch.forEach((pixel) => { - state.paintedMap[pixel.localY][pixel.localX] = true - state.paintedPixels++ - }) - - state.currentCharges -= pixelBatch.length - updateStats() - updateUI("paintingProgress", "default", { - painted: state.paintedPixels, - total: state.totalPixels, - }) - - // Auto-save progress every 50 pixels - if (state.paintedPixels % 50 === 0) { - Utils.saveProgress() + 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 { + console.error(`❌ Batch failed permanently after retries. Stopping painting.`); + state.stopFlag = true; + updateUI('paintingBatchFailed', 'error'); + // noinspection UnnecessaryLabelOnBreakStatementJS + break outerLoop; } + } - pixelBatch = [] + pixelBatch = { + regionX: regionX + adderX, + regionY: regionY + adderY, + pixels: [], + }; + } + + try { + const tileKeyParts = [pixelBatch.regionX, pixelBatch.regionY]; - if (state.currentCharges < 1) { - updateUI("noCharges", "warning", { - time: Utils.formatTime(state.cooldown), - }) - await Utils.sleep(state.cooldown) + const tilePixelRGBA = await overlayManager.getTilePixelColor( + tileKeyParts[0], + tileKeyParts[1], + pixelX, + pixelY + ); - const chargeUpdate = await WPlaceService.getCharges() - state.currentCharges = chargeUpdate.charges - state.cooldown = chargeUpdate.cooldown + 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; } + console.debug( + `[COMPARE] Pixel at 📍 (${pixelX}, ${pixelY}) in region (${ + regionX + adderX + }, ${regionY + adderY})\n` + + ` ├── Current color: rgb(${tilePixelRGBA.slice(0, 3).join(', ')}) (id: ${mappedCanvasColor.id})\n` + + ` ├── Target color: rgb(${targetPixelInfo.r}, ${targetPixelInfo.g}, ${targetPixelInfo.b}) (id: ${targetMappedColorId})\n` + + ` └── Status: ${ + isMatch ? '✅ Already painted → SKIP' : '🔴 Needs paint → PAINT' + }\n` + ); } + } catch (e) { + console.error(`[DEBUG] Error checking existing pixel at (${pixelX}, ${pixelY}):`, e); + updateUI('paintingPixelCheckFailed', 'error', { x: pixelX, y: pixelY }); + state.stopFlag = true; + // noinspection UnnecessaryLabelOnBreakStatementJS + break outerLoop; + } + + pixelBatch.pixels.push({ + x: pixelX, + y: pixelY, + color: targetMappedColorId, + localX: x, + localY: y, + }); + + const maxBatchSize = calculateBatchSize(); + if (pixelBatch.pixels.length >= maxBatchSize) { + const modeText = + state.batchMode === 'random' + ? `random (${state.randomBatchMin}-${state.randomBatchMax})` + : 'normal'; + console.log( + `📦 Sending batch with ${pixelBatch.pixels.length} pixels (mode: ${modeText}, target: ${maxBatchSize})` + ); + const success = await flushPixelBatch(pixelBatch); + if (!success) { + console.error(`❌ Batch failed permanently after retries. Stopping painting.`); + state.stopFlag = true; + updateUI('paintingBatchFailed', 'error'); + // noinspection UnnecessaryLabelOnBreakStatementJS + break outerLoop; + } + + pixelBatch.pixels = []; + } + + if (state.displayCharges < state.cooldownChargeThreshold && !state.stopFlag) { + await Utils.dynamicSleep(() => { + if (state.displayCharges >= state.cooldownChargeThreshold) { + NotificationManager.maybeNotifyChargesReached(true); + return 0; + } + if (state.stopFlag) return 0; + return getMsToTargetCharges( + state.preciseCurrentCharges, + state.cooldownChargeThreshold, + state.cooldown + ); + }); + } + + if (state.stopFlag) { + // noinspection UnnecessaryLabelOnBreakStatementJS + break outerLoop; } } - if (pixelBatch.length > 0 && !state.stopFlag) { - const success = await sendPixelBatch(pixelBatch, regionX, regionY) - if (success) { - pixelBatch.forEach((pixel) => { - state.paintedMap[pixel.localY][pixel.localX] = true - state.paintedPixels++ - }) - state.currentCharges -= pixelBatch.length + 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.` + ); } } } finally { - if (window._chargesInterval) clearInterval(window._chargesInterval) - window._chargesInterval = null + if (window._chargesInterval) clearInterval(window._chargesInterval); + window._chargesInterval = null; } if (state.stopFlag) { - updateUI("paintingStopped", "warning") - // Save progress when stopped - Utils.saveProgress() + // Save progress when stopped to preserve painted map + Utils.saveProgress(); } else { - updateUI("paintingComplete", "success", { count: state.paintedPixels }) - state.lastPosition = { x: 0, y: 0 } - state.paintedMap = null - // Clear saved data when completed - Utils.clearProgress() + updateUI('paintingComplete', 'success', { count: state.paintedPixels }); + state.lastPosition = { x: 0, y: 0 }; + // Keep painted map until user starts new project + // state.paintedMap = null // Commented out to preserve data + Utils.saveProgress(); // Save final complete state + overlayManager.clear(); + const toggleOverlayBtn = document.getElementById('toggleOverlayBtn'); + if (toggleOverlayBtn) { + toggleOverlayBtn.classList.remove('active'); + toggleOverlayBtn.disabled = true; + } } - updateStats() + // 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(); } - async function sendPixelBatch(pixelBatch, regionX, regionY) { - if (!capturedCaptchaToken) { - return "token_error" + // Helper function to calculate batch size based on mode + function calculateBatchSize() { + let targetBatchSize; + + if (state.batchMode === 'random') { + // Generate random batch size within the specified range + const min = Math.max(1, state.randomBatchMin); + const max = Math.max(min, state.randomBatchMax); + targetBatchSize = Math.floor(Math.random() * (max - min + 1)) + min; + console.log(`🎲 Random batch size generated: ${targetBatchSize} (range: ${min}-${max})`); + } else { + // Normal mode - use the fixed paintingSpeed value + targetBatchSize = state.paintingSpeed; } - const coords = [] - const colors = [] + // Always limit by available charges + const maxAllowed = state.displayCharges; + const finalBatchSize = Math.min(targetBatchSize, maxAllowed); - pixelBatch.forEach((pixel) => { - coords.push(pixel.x, pixel.y) - colors.push(pixel.color) - }) + return finalBatchSize; + } - try { - const payload = { - coords: coords, - colors: colors, - t: capturedCaptchaToken, + // Helper function to retry batch until success with exponential backoff + async function sendBatchWithRetry(pixels, regionX, regionY, maxRetries = MAX_BATCH_RETRIES) { + let attempt = 0; + while (attempt < maxRetries && !state.stopFlag) { + attempt++; + console.log( + `🔄 Attempting to send batch (attempt ${attempt}/${maxRetries}) for region ${regionX},${regionY} with ${pixels.length} pixels` + ); + + const result = await sendPixelBatch(pixels, regionX, regionY); + + if (result === true) { + console.log(`✅ Batch succeeded on attempt ${attempt}`); + return true; + } else if (result === 'token_error') { + console.log(`🔑 Token error on attempt ${attempt}, regenerating...`); + updateUI('captchaSolving', 'warning'); + try { + await handleCaptcha(); + // Don't count token regeneration as a failed attempt + attempt--; + continue; + } catch (e) { + console.error(`❌ Token regeneration failed on attempt ${attempt}:`, e); + updateUI('captchaFailed', 'error'); + // Wait longer before retrying after token failure + await Utils.sleep(5000); + } + } else { + console.warn(`⚠️ Batch failed on attempt ${attempt}, retrying...`); + // Exponential backoff with jitter + const baseDelay = Math.min(1000 * Math.pow(2, attempt - 1), 30000); // Max 30s + const jitter = Math.random() * 1000; // Add up to 1s random delay + await Utils.sleep(baseDelay + jitter); + } + } + + if (attempt >= maxRetries) { + console.error( + `❌ Batch failed after ${maxRetries} attempts (MAX_BATCH_RETRIES=${MAX_BATCH_RETRIES}). This will stop painting to prevent infinite loops.` + ); + updateUI('paintingError', 'error'); + return false; + } + + return false; + } + + async function sendPixelBatch(pixelBatch, regionX, regionY) { + let token = turnstileToken; + + // Generate new token if we don't have one + if (!token) { + try { + console.log('🔑 Generating Turnstile token for pixel batch...'); + token = await handleCaptcha(); + turnstileToken = token; // Store for potential reuse + } catch (error) { + console.error('❌ Failed to generate Turnstile token:', error); + tokenPromise = new Promise((resolve) => { + _resolveToken = resolve; + }); + return 'token_error'; } + } + + const coords = new Array(pixelBatch.length * 2); + const colors = new Array(pixelBatch.length); + for (let i = 0; i < pixelBatch.length; i++) { + const pixel = pixelBatch[i]; + coords[i * 2] = pixel.x; + coords[i * 2 + 1] = pixel.y; + colors[i] = pixel.color; + } + + try { + const payload = { coords, colors, t: token }; const res = await fetch(`https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, { - method: "POST", - headers: { "Content-Type": "text/plain;charset=UTF-8" }, - credentials: "include", + method: 'POST', + headers: { 'Content-Type': 'text/plain;charset=UTF-8' }, + credentials: 'include', body: JSON.stringify(payload), - }) + }); if (res.status === 403) { - console.error("❌ 403 Forbidden. CAPTCHA token might be invalid or expired.") - capturedCaptchaToken = null - return "token_error" + let data = null; + try { + data = await res.json(); + } catch (_) {} + console.error('❌ 403 Forbidden. Turnstile token might be invalid or expired.'); + + // Try to generate a new token and retry once + try { + console.log('🔄 Regenerating Turnstile token after 403...'); + token = await handleCaptcha(); + turnstileToken = token; + + // Retry the request with new token + const retryPayload = { coords, colors, t: token }; + const retryRes = await fetch( + `https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, + { + method: 'POST', + headers: { 'Content-Type': 'text/plain;charset=UTF-8' }, + credentials: 'include', + body: JSON.stringify(retryPayload), + } + ); + + if (retryRes.status === 403) { + turnstileToken = null; + tokenPromise = new Promise((resolve) => { + _resolveToken = resolve; + }); + return 'token_error'; + } + + const retryData = await retryRes.json(); + return retryData?.painted === pixelBatch.length; + } catch (retryError) { + console.error('❌ Token regeneration failed:', retryError); + turnstileToken = null; + tokenPromise = new Promise((resolve) => { + _resolveToken = resolve; + }); + return 'token_error'; + } + } + + const data = await res.json(); + return data?.painted === pixelBatch.length; + } catch (e) { + console.error('Batch paint request failed:', e); + return false; + } + } + + function saveBotSettings() { + try { + const settings = { + paintingSpeed: state.paintingSpeed, + paintingSpeedEnabled: document.getElementById('enableSpeedToggle')?.checked, + batchMode: state.batchMode, // "normal" or "random" + randomBatchMin: state.randomBatchMin, + randomBatchMax: state.randomBatchMax, + cooldownChargeThreshold: state.cooldownChargeThreshold, + tokenSource: state.tokenSource, // "generator", "hybrid", or "manual" + minimized: state.minimized, + overlayOpacity: state.overlayOpacity, + blueMarbleEnabled: document.getElementById('enableBlueMarbleToggle')?.checked, + ditheringEnabled: state.ditheringEnabled, + colorMatchingAlgorithm: state.colorMatchingAlgorithm, + enableChromaPenalty: state.enableChromaPenalty, + chromaPenaltyWeight: state.chromaPenaltyWeight, + customTransparencyThreshold: state.customTransparencyThreshold, + customWhiteThreshold: state.customWhiteThreshold, + paintWhitePixels: state.paintWhitePixels, + paintTransparentPixels: state.paintTransparentPixels, + resizeSettings: state.resizeSettings, + paintUnavailablePixels: state.paintUnavailablePixels, + coordinateMode: state.coordinateMode, + coordinateDirection: state.coordinateDirection, + coordinateSnake: state.coordinateSnake, + blockWidth: state.blockWidth, + blockHeight: state.blockHeight, // Save ignore mask (as base64) with its dimensions + resizeIgnoreMask: + state.resizeIgnoreMask && + state.resizeSettings && + state.resizeSettings.width * state.resizeSettings.height === state.resizeIgnoreMask.length + ? { + w: state.resizeSettings.width, + h: state.resizeSettings.height, + data: btoa(String.fromCharCode(...state.resizeIgnoreMask)), + } + : null, // Notifications + notificationsEnabled: state.notificationsEnabled, + notifyOnChargesReached: state.notifyOnChargesReached, + notifyOnlyWhenUnfocused: state.notifyOnlyWhenUnfocused, + notificationIntervalMinutes: state.notificationIntervalMinutes, + originalImage: state.originalImage, + }; + CONFIG.PAINTING_SPEED_ENABLED = settings.paintingSpeedEnabled; + // AUTO_CAPTCHA_ENABLED is always true - no need to save/load + + localStorage.setItem('wplace-bot-settings', JSON.stringify(settings)); + } catch (e) { + console.warn('Could not save bot settings:', e); + } + } + + function loadBotSettings() { + try { + const saved = localStorage.getItem('wplace-bot-settings'); + if (!saved) return; + const settings = JSON.parse(saved); + + state.paintingSpeed = settings.paintingSpeed || CONFIG.PAINTING_SPEED.DEFAULT; + state.batchMode = settings.batchMode || CONFIG.BATCH_MODE; // Default to "normal" + state.randomBatchMin = settings.randomBatchMin || CONFIG.RANDOM_BATCH_RANGE.MIN; + state.randomBatchMax = settings.randomBatchMax || CONFIG.RANDOM_BATCH_RANGE.MAX; + state.cooldownChargeThreshold = + settings.cooldownChargeThreshold || CONFIG.COOLDOWN_CHARGE_THRESHOLD; + state.tokenSource = settings.tokenSource || CONFIG.TOKEN_SOURCE; // Default to "generator" + state.minimized = settings.minimized ?? false; + CONFIG.PAINTING_SPEED_ENABLED = settings.paintingSpeedEnabled ?? false; + CONFIG.AUTO_CAPTCHA_ENABLED = settings.autoCaptchaEnabled ?? false; + state.overlayOpacity = settings.overlayOpacity ?? CONFIG.OVERLAY.OPACITY_DEFAULT; + state.blueMarbleEnabled = settings.blueMarbleEnabled ?? CONFIG.OVERLAY.BLUE_MARBLE_DEFAULT; + state.ditheringEnabled = settings.ditheringEnabled ?? false; + state.colorMatchingAlgorithm = settings.colorMatchingAlgorithm || 'lab'; + state.enableChromaPenalty = settings.enableChromaPenalty ?? true; + state.chromaPenaltyWeight = settings.chromaPenaltyWeight ?? 0.15; + state.customTransparencyThreshold = + settings.customTransparencyThreshold ?? CONFIG.TRANSPARENCY_THRESHOLD; + state.customWhiteThreshold = settings.customWhiteThreshold ?? CONFIG.WHITE_THRESHOLD; + state.paintWhitePixels = settings.paintWhitePixels ?? true; + state.paintTransparentPixels = settings.paintTransparentPixels ?? false; + state.resizeSettings = settings.resizeSettings ?? null; + state.originalImage = settings.originalImage ?? null; + state.paintUnavailablePixels = settings.paintUnavailablePixels ?? CONFIG.PAINT_UNAVAILABLE; + state.coordinateMode = settings.coordinateMode ?? CONFIG.COORDINATE_MODE; + state.coordinateDirection = settings.coordinateDirection ?? CONFIG.COORDINATE_DIRECTION; + state.coordinateSnake = settings.coordinateSnake ?? CONFIG.COORDINATE_SNAKE; + state.blockWidth = settings.blockWidth ?? CONFIG.COORDINATE_BLOCK_WIDTH; + state.blockHeight = settings.blockHeight ?? CONFIG.COORDINATE_BLOCK_HEIGHT; + // Notifications + state.notificationsEnabled = settings.notificationsEnabled ?? CONFIG.NOTIFICATIONS.ENABLED; + state.notifyOnChargesReached = + settings.notifyOnChargesReached ?? CONFIG.NOTIFICATIONS.ON_CHARGES_REACHED; + state.notifyOnlyWhenUnfocused = + settings.notifyOnlyWhenUnfocused ?? CONFIG.NOTIFICATIONS.ONLY_WHEN_UNFOCUSED; + state.notificationIntervalMinutes = + settings.notificationIntervalMinutes ?? CONFIG.NOTIFICATIONS.REPEAT_MINUTES; + // Restore ignore mask if dims match current resizeSettings + if ( + settings.resizeIgnoreMask && + settings.resizeIgnoreMask.data && + state.resizeSettings && + settings.resizeIgnoreMask.w === state.resizeSettings.width && + settings.resizeIgnoreMask.h === state.resizeSettings.height + ) { + try { + const bin = atob(settings.resizeIgnoreMask.data); + const arr = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i); + state.resizeIgnoreMask = arr; + } catch { + state.resizeIgnoreMask = null; + } + } else { + state.resizeIgnoreMask = null; + } + // Initialize coordinate generation UI + const coordinateModeSelect = document.getElementById('coordinateModeSelect'); + if (coordinateModeSelect) coordinateModeSelect.value = state.coordinateMode; + + const coordinateDirectionSelect = document.getElementById('coordinateDirectionSelect'); + if (coordinateDirectionSelect) coordinateDirectionSelect.value = state.coordinateDirection; + + const coordinateSnakeToggle = document.getElementById('coordinateSnakeToggle'); + if (coordinateSnakeToggle) coordinateSnakeToggle.checked = state.coordinateSnake; + + const settingsContainer = document.getElementById('wplace-settings-container'); + const directionControls = settingsContainer.querySelector('#directionControls'); + const snakeControls = settingsContainer.querySelector('#snakeControls'); + const blockControls = settingsContainer.querySelector('#blockControls'); + Utils.updateCoordinateUI({ + mode: state.coordinateMode, + directionControls, + snakeControls, + blockControls, + }); + + const paintUnavailablePixelsToggle = document.getElementById('paintUnavailablePixelsToggle'); + if (paintUnavailablePixelsToggle) { + paintUnavailablePixelsToggle.checked = state.paintUnavailablePixels; + } + + const settingsPaintWhiteToggle = settingsContainer.querySelector('#settingsPaintWhiteToggle'); + if (settingsPaintWhiteToggle) { + settingsPaintWhiteToggle.checked = state.paintWhitePixels; + } + + const settingsPaintTransparentToggle = settingsContainer.querySelector( + '#settingsPaintTransparentToggle' + ); + if (settingsPaintTransparentToggle) { + settingsPaintTransparentToggle.checked = state.paintTransparentPixels; + } + + const speedSlider = document.getElementById('speedSlider'); + const speedInput = document.getElementById('speedInput'); + const speedValue = document.getElementById('speedValue'); + if (speedSlider) speedSlider.value = state.paintingSpeed; + if (speedInput) speedInput.value = state.paintingSpeed; + if (speedValue) speedValue.textContent = `pixels`; + + const enableSpeedToggle = document.getElementById('enableSpeedToggle'); + if (enableSpeedToggle) enableSpeedToggle.checked = CONFIG.PAINTING_SPEED_ENABLED; + + // Batch mode UI initialization + const batchModeSelect = document.getElementById('batchModeSelect'); + if (batchModeSelect) batchModeSelect.value = state.batchMode; + + const normalBatchControls = document.getElementById('normalBatchControls'); + const randomBatchControls = document.getElementById('randomBatchControls'); + + // Show/hide appropriate controls based on batch mode + if (normalBatchControls && randomBatchControls) { + if (state.batchMode === 'random') { + normalBatchControls.style.display = 'none'; + randomBatchControls.style.display = 'block'; + } else { + normalBatchControls.style.display = 'block'; + randomBatchControls.style.display = 'none'; + } } - const data = await res.json() - return data?.painted === pixelBatch.length + const randomBatchMin = document.getElementById('randomBatchMin'); + if (randomBatchMin) randomBatchMin.value = state.randomBatchMin; + + const randomBatchMax = document.getElementById('randomBatchMax'); + if (randomBatchMax) randomBatchMax.value = state.randomBatchMax; + + // AUTO_CAPTCHA_ENABLED is always true - no toggle to set + + const cooldownSlider = document.getElementById('cooldownSlider'); + const cooldownInput = document.getElementById('cooldownInput'); + const cooldownValue = document.getElementById('cooldownValue'); + if (cooldownSlider) cooldownSlider.value = state.cooldownChargeThreshold; + if (cooldownInput) cooldownInput.value = state.cooldownChargeThreshold; + if (cooldownValue) cooldownValue.textContent = `${Utils.t('charges')}`; + + const overlayOpacitySlider = document.getElementById('overlayOpacitySlider'); + if (overlayOpacitySlider) overlayOpacitySlider.value = state.overlayOpacity; + const overlayOpacityValue = document.getElementById('overlayOpacityValue'); + if (overlayOpacityValue) + overlayOpacityValue.textContent = `${Math.round(state.overlayOpacity * 100)}%`; + const enableBlueMarbleToggle = document.getElementById('enableBlueMarbleToggle'); + if (enableBlueMarbleToggle) enableBlueMarbleToggle.checked = state.blueMarbleEnabled; + + const tokenSourceSelect = document.getElementById('tokenSourceSelect'); + if (tokenSourceSelect) tokenSourceSelect.value = state.tokenSource; + + const colorAlgorithmSelect = document.getElementById('colorAlgorithmSelect'); + if (colorAlgorithmSelect) colorAlgorithmSelect.value = state.colorMatchingAlgorithm; + const enableChromaPenaltyToggle = document.getElementById('enableChromaPenaltyToggle'); + if (enableChromaPenaltyToggle) enableChromaPenaltyToggle.checked = state.enableChromaPenalty; + const chromaPenaltyWeightSlider = document.getElementById('chromaPenaltyWeightSlider'); + if (chromaPenaltyWeightSlider) chromaPenaltyWeightSlider.value = state.chromaPenaltyWeight; + const chromaWeightValue = document.getElementById('chromaWeightValue'); + if (chromaWeightValue) chromaWeightValue.textContent = state.chromaPenaltyWeight; + const transparencyThresholdInput = document.getElementById('transparencyThresholdInput'); + if (transparencyThresholdInput) + transparencyThresholdInput.value = state.customTransparencyThreshold; + const whiteThresholdInput = document.getElementById('whiteThresholdInput'); + if (whiteThresholdInput) whiteThresholdInput.value = state.customWhiteThreshold; + // Notifications UI + const notifEnabledToggle = document.getElementById('notifEnabledToggle'); + if (notifEnabledToggle) notifEnabledToggle.checked = state.notificationsEnabled; + const notifOnChargesToggle = document.getElementById('notifOnChargesToggle'); + if (notifOnChargesToggle) notifOnChargesToggle.checked = state.notifyOnChargesReached; + const notifOnlyUnfocusedToggle = document.getElementById('notifOnlyUnfocusedToggle'); + if (notifOnlyUnfocusedToggle) + notifOnlyUnfocusedToggle.checked = state.notifyOnlyWhenUnfocused; + const notifIntervalInput = document.getElementById('notifIntervalInput'); + if (notifIntervalInput) notifIntervalInput.value = state.notificationIntervalMinutes; + NotificationManager.resetEdgeTracking(); } catch (e) { - console.error("Batch paint request failed:", e) - return false + console.warn('Could not load bot settings:', e); + } + } + + // Initialize Turnstile generator integration + console.log('🚀 WPlace Auto-Image with Turnstile Token Generator loaded'); + console.log('🔑 Turnstile token generator: ALWAYS ENABLED (Background mode)'); + console.log('🎯 Manual pixel captcha solving: Available as fallback/alternative'); + console.log('📱 Turnstile widgets: DISABLED - pure background token generation only!'); + + // Function to enable file operations after initial startup setup is complete + function enableFileOperations() { + state.initialSetupComplete = true; + + const loadBtn = document.querySelector('#loadBtn'); + const loadFromFileBtn = document.querySelector('#loadFromFileBtn'); + const uploadBtn = document.querySelector('#uploadBtn'); + + if (loadBtn) { + loadBtn.disabled = false; + loadBtn.title = ''; + // Add a subtle animation to indicate the button is now available + loadBtn.style.animation = 'pulse 0.6s ease-in-out'; + setTimeout(() => { + if (loadBtn) loadBtn.style.animation = ''; + }, 600); + console.log('✅ Load Progress button enabled after initial setup'); + } + + if (loadFromFileBtn) { + loadFromFileBtn.disabled = false; + loadFromFileBtn.title = ''; + // Add a subtle animation to indicate the button is now available + loadFromFileBtn.style.animation = 'pulse 0.6s ease-in-out'; + setTimeout(() => { + if (loadFromFileBtn) loadFromFileBtn.style.animation = ''; + }, 600); + console.log('✅ Load from File button enabled after initial setup'); + } + + if (uploadBtn) { + uploadBtn.disabled = false; + uploadBtn.title = ''; + // Add a subtle animation to indicate the button is now available + uploadBtn.style.animation = 'pulse 0.6s ease-in-out'; + setTimeout(() => { + if (uploadBtn) uploadBtn.style.animation = ''; + }, 600); + console.log('✅ Upload Image button enabled after initial setup'); + } + + // Show a notification that file operations are now available + Utils.showAlert(Utils.t('fileOperationsAvailable'), 'success'); + } + + // Optimized token initialization with better timing and error handling + async function initializeTokenGenerator() { + // Skip if already have valid token + if (isTokenValid()) { + console.log('✅ Valid token already available, skipping initialization'); + updateUI('tokenReady', 'success'); + enableFileOperations(); // Enable file operations since initial setup is complete + return; + } + + try { + console.log('🔧 Initializing Turnstile token generator...'); + updateUI('initializingToken', 'default'); + + console.log('Attempting to load Turnstile script...'); + await Utils.loadTurnstile(); + console.log('Turnstile script loaded. Attempting to generate token...'); + + const token = await handleCaptchaWithRetry(); + if (token) { + setTurnstileToken(token); + console.log('✅ Startup token generated successfully'); + updateUI('tokenReady', 'success'); + Utils.showAlert(Utils.t('tokenGeneratorReady'), 'success'); + enableFileOperations(); // Enable file operations since initial setup is complete + } else { + console.warn( + '⚠️ Startup token generation failed (no token received), will retry when needed' + ); + updateUI('tokenRetryLater', 'warning'); + // Still enable file operations even if initial token generation fails + // Users can load progress and use manual/hybrid modes + enableFileOperations(); + } + } catch (error) { + console.error('❌ Critical error during Turnstile initialization:', error); // More specific error + updateUI('tokenRetryLater', 'warning'); + // Still enable file operations even if initial setup fails + // Users can load progress and use manual/hybrid modes + enableFileOperations(); + // Don't show error alert for initialization failures, just log them } } - createUI() -})() + // Load theme preference immediately on startup before creating UI + loadThemePreference(); + applyTheme(); + + createUI().then(() => { + // Generate token automatically after UI is ready + setTimeout(initializeTokenGenerator, 1000); + + // Attach advanced color matching listeners (resize dialog) + const advancedInit = () => { + const chromaSlider = document.getElementById('chromaPenaltyWeightSlider'); + const chromaValue = document.getElementById('chromaWeightValue'); + const resetBtn = document.getElementById('resetAdvancedColorBtn'); + const algoSelect = document.getElementById('colorAlgorithmSelect'); + const chromaToggle = document.getElementById('enableChromaPenaltyToggle'); + const transInput = document.getElementById('transparencyThresholdInput'); + const whiteInput = document.getElementById('whiteThresholdInput'); + const ditherToggle = document.getElementById('enableDitheringToggle'); + if (algoSelect) + algoSelect.addEventListener('change', (e) => { + state.colorMatchingAlgorithm = e.target.value; + saveBotSettings(); + _updateResizePreview(); + }); + if (chromaToggle) + chromaToggle.addEventListener('change', (e) => { + state.enableChromaPenalty = e.target.checked; + saveBotSettings(); + _updateResizePreview(); + }); + if (chromaSlider && chromaValue) + chromaSlider.addEventListener('input', (e) => { + state.chromaPenaltyWeight = parseFloat(e.target.value) || 0.15; + chromaValue.textContent = state.chromaPenaltyWeight.toFixed(2); + saveBotSettings(); + _updateResizePreview(); + }); + if (transInput) + transInput.addEventListener('change', (e) => { + const v = parseInt(e.target.value, 10); + if (!isNaN(v) && v >= 0 && v <= 255) { + state.customTransparencyThreshold = v; + CONFIG.TRANSPARENCY_THRESHOLD = v; + saveBotSettings(); + _updateResizePreview(); + } + }); + if (whiteInput) + whiteInput.addEventListener('change', (e) => { + const v = parseInt(e.target.value, 10); + if (!isNaN(v) && v >= 200 && v <= 255) { + state.customWhiteThreshold = v; + CONFIG.WHITE_THRESHOLD = v; + saveBotSettings(); + _updateResizePreview(); + } + }); + if (ditherToggle) + ditherToggle.addEventListener('change', (e) => { + state.ditheringEnabled = e.target.checked; + saveBotSettings(); + _updateResizePreview(); + }); + if (resetBtn) + resetBtn.addEventListener('click', () => { + state.colorMatchingAlgorithm = 'lab'; + state.enableChromaPenalty = true; + state.chromaPenaltyWeight = 0.15; + state.customTransparencyThreshold = CONFIG.TRANSPARENCY_THRESHOLD = 100; + state.customWhiteThreshold = CONFIG.WHITE_THRESHOLD = 250; + saveBotSettings(); + const a = document.getElementById('colorAlgorithmSelect'); + if (a) a.value = 'lab'; + const ct = document.getElementById('enableChromaPenaltyToggle'); + if (ct) ct.checked = true; + if (chromaSlider) chromaSlider.value = 0.15; + if (chromaValue) chromaValue.textContent = '0.15'; + if (transInput) transInput.value = 100; + if (whiteInput) whiteInput.value = 250; + _updateResizePreview(); + Utils.showAlert(Utils.t('advancedColorSettingsReset'), 'success'); + }); + }; + // Delay to ensure resize UI built + setTimeout(advancedInit, 500); + + // Add cleanup on page unload + window.addEventListener('beforeunload', () => { + Utils.cleanupTurnstile(); + }); + }); +})(); diff --git a/DE.md b/DE.md new file mode 100644 index 00000000..674fcc12 --- /dev/null +++ b/DE.md @@ -0,0 +1,97 @@ + +

WPlace AutoBOT

+

+Dieses Projekt ist ein Fork von https://github.com/DarkModde/WPlace-AutoBOT +Tritt unserem Discord bei: https://discord.gg/CBB4abRmGM +

+

+Praktische Scripts mit intuitiven Menüs, die dein Leben auf WPlace erleichtern!
+Perfekt für alle, die automatisch Level aufsteigen oder riesige Pixel Artworks erstellen wollen ohne Zeit zu verschwenden. +

+ +
+ +

+⚠️ Hinweis: Dieses Script dient ausschließlich zu Bildungszwecken, um zu demonstrieren, was auf wplace.live möglich ist. Wir übernehmen keine Verantwortung für die Nutzung. +

+ +

+Wenn du dich fragst — ja! Diese Scripts wurden von einem Team aus Brasilianern, Vietnamesen und Indonesiern entwickelt. HUEHUE! +

+ +--- + +

🚀┃Wie man die Scripts benutzt:

+ +

+Es ist super einfach: Kopiere einen der Codes unten, füge ihn in die Lesezeichenleiste deines Browsers ein,
+und führe ihn aus, während du auf wplace.live bist. +

+ +### 🎯┃Auto-Farm +**Auto-Farm verwendet Ladungen, um Levels zu Farmen. Es zeichnet keine Bilder! Nutze dafür Auto-Image.** + +```js +javascript:fetch("https://raw.githubusercontent.com/Wplace-AutoBot/WPlace-AutoBOT/refs/heads/main/Auto-Farm.js").then(t=>t.text()).then(eval); +```` + +### 🖼️┃Auto-Image + +```js +javascript:fetch("https://raw.githubusercontent.com/Wplace-AutoBot/WPlace-AutoBOT/refs/heads/main/Auto-Image.js").then(t=>t.text()).then(eval); +``` + +
+

📖┃Tutorials

+ +--- + +![Teil 1](https://i.imgur.com/yneG5if.png) + +--- + +![Teil 2](https://i.imgur.com/ZRpU0wZ.png) + +--- + +![Teil 3](https://i.imgur.com/lfjfcEw.png) + +
+ +--- + +### ✨┃Features + +* Automatisches Farmen von Drops/Levels +* Korrektes Überspringen von Pixeln mit falscher Farbe +* Overlay (BlueMarble nicht mehr nötig) +* Malen über mehrere Tiles +* AutoCaptcha-Solver mit Turnstile Token Generator +* Erweiterte Farbverarbeitung +* Mehrsprachige Unterstützung +* Multi-Account über Speicher- und Ladefunktion +* Funktioniert auch auf Mobilgeräten \:P + +--- + +### 📋┃To-Do + +* [x] AutoFarm zum Laufen bringen +* [ ] Beide Scripts zu einem zusammenführen +* [ ] Multi-Account-Switcher (in Entwicklung) +* [ ] Multi-Account-Warteschlange +* [ ] Overlay-System hinzufügen +* [ ] Unterstützung über mehrere Tiles +* [ ] AutoCaptcha-Solver hinzufügen +* [ ] Korrektes Überspringen von Pixeln +* [ ] Separate CSS-Dateien + +--- + +

+ Demo des Scripts +

+ +

+ +

diff --git a/NL.md b/NL.md new file mode 100644 index 00000000..9b3827eb --- /dev/null +++ b/NL.md @@ -0,0 +1,124 @@ +

+ image +

+ +

WPlace AutoBOT

+

+Dit project is een fork van https://github.com/DarkModde/WPlace-AutoBOT +

+

+ Overweeg om onze Discord te bekijken: https://discord.gg/knkNRYyQcm +

+

+ Praktische scripts met intuïtieve menu’s om je leven makkelijker te maken op WPlace!
+ Perfect voor wie automatisch wil levelen of enorme pixelkunst wil maken — zonder tijd te verspillen. + + Waarschuwing: Dit is een puur educatief script om te laten zien wat er op wplace.live mogelijk is, wij zijn niet verantwoordelijk voor wat er gebeurt als je het script gebruikt. +

+ +
+ +

+ Als je je afvraagt—ja! Deze scripts zijn ontwikkeld door een team bestaande uit Brazilianen, Vietnamezen en Indonesiërs. HUEHUE! +

+ Brazilië +   + Vietnam +   + Indonesië +

+

+ +--- + +

🚀┃Hoe de scripts te gebruiken:

+ +

+ Het is super eenvoudig: kopieer een van de codes hieronder, plak deze in de bladwijzerbalk van je browser,
+ en voer het uit terwijl je op wplace.live bent. +

+ +
+ +### 🎯┃Auto-Farm +#### AUTOFARM GEBRUIKT LADINGEN OM LEVELS TE KRIJGEN, HET TEKENT GEEN AFBEELDING VOOR JE. GEBRUIK DAARVOOR AUTO-IMAGE + +```js +javascript:fetch("https://raw.githubusercontent.com/Wplace-AutoBot/WPlace-AutoBOT/refs/heads/main/Auto-Farm.js").then(t=>t.text()).then(eval); +``` + +### 🖼️┃Auto-Image + +```js +javascript:fetch("https://raw.githubusercontent.com/Wplace-AutoBot/WPlace-AutoBOT/refs/heads/main/Auto-Image.js").then(t=>t.text()).then(eval); +``` + +
+

📖┃Tutoriais

+ +--- + +![Deel 1](https://i.imgur.com/yneG5if.png) + +--- + +![Deel 2](https://i.imgur.com/ZRpU0wZ.png) + +--- + +![Deel 3](https://i.imgur.com/lfjfcEw.png) + +
+ +
+ +> \[!BELANGRIJK] +> +>

1. Geen van de scripts werkt als je het browsertabblad sluit. Je moet het WPlace-tabblad open houden, zelfs als het op de achtergrond staat.

+>

2. Gebruik nooit beide scripts op dezelfde pagina — dit kan WPlace laten haperen. Als dat gebeurt, ververs gewoon de pagina!

+>

3. Als het script de beschikbare kleuren niet detecteert, klik dan op de "Paint" knop om ze te tonen — start daarna pas de Auto-Imager.

+ +
+ +### ✨┃Functies + +* [x] Farmt automatisch druppels/levels +* [x] Correcte kleur pixel overslaan +* [x] Overlay (je hebt Bluemarble niet meer nodig) +* [x] Schilder over meerdere tegels +* [x] AutoCaptcha-oplosser met Turnstile Token Generator +* [x] Geavanceerde kleurverwerking +* [x] Meertalige ondersteuning +* [x] Meerdere accounts via opslaan en laden +* [x] Werkt op mobiel \:P + +--- + +--- + +

+ Script Demonstratie +

+ +--- + +### 📋┃Te doen + +* [x] AutoFarm werkend maken +* [ ] Beide scripts samenvoegen +* [ ] Multi Account Switcher (momenteel in ontwikkeling) +* [ ] Multi Account Wachtrijsysteem +* [x] Overlay-systeem toevoegen +* [x] Ondersteuning voor kruistegels toevoegen +* [x] AutoCaptcha-oplosser toevoegen +* [x] Correcte kleur pixel overslaan toevoegen +* [ ] CSS scheiden + +--- + +* Bijdragers: Contributors + +

+ +

+``` diff --git a/README-ES.md b/README-ES.md new file mode 100644 index 00000000..df1e3712 --- /dev/null +++ b/README-ES.md @@ -0,0 +1,125 @@ +

+ image +

+ +

WPlace AutoBOT

+

+Este proyecto es un fork de https://github.com/DarkModde/WPlace-AutoBOT +

+

+ Considera unirte a nuestro Discord: https://discord.gg/knkNRYyQcm +

+

+ ¡Scripts prácticos con menús intuitivos para facilitarte la vida en WPlace!
+ Perfecto para quienes quieren subir de nivel automáticamente o construir pixel arts masivos, sin perder tiempo. + + ⚠️ Advertencia: Este es un script puramente educativo para demostrar lo que se puede hacer en wplace.live. No nos hacemos responsables de lo que suceda si utilizas el script. +

+ +
+ +

+ Por si te lo preguntas, ¡sí! Estos scripts fueron desarrollados por un equipo de brasileños, vietnamitas e indonesios. ¡HUEHUE! +

+ Brasil +   + Vietnam +   + Indonesia +

+

+ +--- + +

🚀┃Cómo usar los scripts:

+ +

+ Es súper fácil: copia uno de los códigos de abajo, pégalo en la barra de marcadores de tu navegador
+ y ejecútalo mientras estás en wplace.live. +

+ +
+ +### 🎯┃Auto-Farm +#### EL AUTOFARM USA CARGAS PARA OBTENER NIVELES, NO DIBUJA UNA IMAGEN POR TI. POR FAVOR, USA AUTO-IMAGEN PARA ESO. +```js +javascript:fetch("[https://raw.githubusercontent.com/Wplace-AutoBot/WPlace-AutoBOT/refs/heads/main/Auto-Farm.js](https://raw.githubusercontent.com/Wplace-AutoBot/WPlace-AutoBOT/refs/heads/main/Auto-Farm.js)").then(t=>t.text()).then(eval); +``` + +### 🖼️┃Auto-Imagen + +```js +javascript:fetch("[https://raw.githubusercontent.com/Wplace-AutoBot/WPlace-AutoBOT/refs/heads/main/Auto-Image.js](https://raw.githubusercontent.com/Wplace-AutoBot/WPlace-AutoBOT/refs/heads/main/Auto-Image.js)").then(t=>t.text()).then(eval); +``` + +
+

📖┃Tutoriales

+ +--- + +![Parte 1](https://i.imgur.com/yneG5if.png) + +--- + +![Parte 2](https://i.imgur.com/ZRpU0wZ.png) + +--- + +![Parte 3](https://i.imgur.com/lfjfcEw.png) + +
+ + +
+ +> [!IMPORTANTE] +>

1. Ninguno de los scripts funcionará si cierras la pestaña del navegador. Debes mantener la pestaña de WPlace abierta, incluso si está en segundo plano.

+>

2. Nunca ejecutes ambos scripts en la misma página, esto puede bugear tu WPlace. Si sucede, ¡simplemente recarga la página!

+>

3. Si el script no detecta los colores disponibles, simplemente haz clic en el botón "Pintar" para mostrarlos y solo después inicia el Auto-Imager.

+ +
+ +### ✨┃Características + +- [x] Farmea automáticamente gotas/niveles +- [x] Omite píxeles del color correcto +- [x] Superposición (ya no necesitas Bluemarble) +- [x] Pinta sobre múltiples áreas (tiles) +- [x] Cambiador automático de multicuentas a través de otra rama: https://github.com/Wplace-AutoBot/WPlace-AutoBOT/tree/Acc-switch (Eficiente en RAM LOL) +- [x] Solucionador de AutoCaptcha con generador de tokens Turnstile +- [x] Procesamiento de color avanzado +- [x] Soporte multi-idioma +- [x] Multicuentas a través de la función de guardar y cargar +- [x] Funciona en móviles :P + + +--- + + +--- + +

+ Demostración del Script +

+ +--- + +### 📋┃Tareas pendientes + +- [x] Hacer que el AutoFarm funcione +- [ ] Unir ambos scripts en uno solo +- [x] Añadir sistema de superposición +- [x] Añadir soporte para pintar entre áreas (Cross Tiles) +- [x] Añadir solucionador de AutoCaptcha +- [x] Añadir omisión de píxeles del color correcto +- [x] Separar CSS + +--- + +- Contribuidores: + Contribuidores + + +

+ +

diff --git a/README.md b/README.md index 9a080230..f6ba3d4a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +

+ image +

+

WPlace AutoBOT

This project was fork from https://github.com/DarkModde/WPlace-AutoBOT @@ -15,8 +19,14 @@ This project was fork from https://github.com/DarkModde/WPlace-AutoBOT

- It wasn't me who "hacked" WPlace and placed that giant Herobrine there...
- But if you're wondering: yes! These scripts were made by a Brazilian and VietNamese. HUEHUE!
+ If you're wondering—yes! These scripts were Developed by a team consisting of Brazilians, Vietnamese, and Indonesians. HUEHUE! +

+ Brazil +   + Vietnam +   + Indonesia +

--- @@ -33,13 +43,13 @@ This project was fork from https://github.com/DarkModde/WPlace-AutoBOT ### 🎯┃Auto-Farm #### AUTOFARM USES CHARGES TO GET LEVELS, IT DOES NOT DRAW AN IMAGE FOR YOU. PLEASE USE AUTO-IMAGE FOR THAT ```js -javascript:fetch("https://raw.githubusercontent.com/Wplace-AutoBot/WPlace-AutoBOT/refs/heads/main/Auto-Farm.js").then(t=>t.text()).then(eval); +javascript:(async()=>{ const u='https://raw.githubusercontent.com/Wplace-AutoBot/WPlace-AutoBOT/refs/heads/main/Auto-Farm.js'; const c=await (await fetch(u,{cache:'no-store'})).text(); const blob=new Blob([c],{type:'application/javascript'}); const url=URL.createObjectURL(blob); const s=document.createElement('script'); s.src=url; document.body.appendChild(s); })(); ``` ### 🖼️┃Auto-Image ```js -javascript:fetch("https://raw.githubusercontent.com/Wplace-AutoBot/WPlace-AutoBOT/refs/heads/main/Auto-Image.js").then(t=>t.text()).then(eval); +javascript:(async()=>{ const u='https://raw.githubusercontent.com/Wplace-AutoBot/WPlace-AutoBOT/refs/heads/main/Auto-Image.js'; const c=await (await fetch(u,{cache:'no-store'})).text(); const blob=new Blob([c],{type:'application/javascript'}); const url=URL.createObjectURL(blob); const s=document.createElement('script'); s.src=url; document.body.appendChild(s); })();; ```
@@ -69,24 +79,52 @@ javascript:fetch("https://raw.githubusercontent.com/Wplace-AutoBot/WPlace-AutoBO
+### ✨┃Features + +- [x] Automatically farms droplets/levels +- [x] Correct Color Pixel Skip +- [x] Overlay(you dont need bluemarble anymore) +- [x] Paint over multiple tiles +- [x] Multi Account Auto Switcher Via Other Branch ; https://github.com/Wplace-AutoBot/WPlace-AutoBOT/tree/Acc-switch (Ram Efficient LOL) +- [x] AutoCaptcha Solver With Turnstile Token Generator +- [x] Wider Paint Control Options +- [x] Advance Color Processing +- [x] MultiLanguage Support +- [x] Multi account through save and load feature +- [x] Works on mobile :P + + +--- + + ---

- Script Demonstration + Script Demonstration

+--- +

+ Script Demonstration +

--- ### 📋┃To-do - [x] Making AutoFarm working -- [ ] Merge both scripts into one -- [x] Fix script bugs -- [] Add new options -- [ ] Tutorials +- [ ] Merge both scripts into one +- [x] Add Overlay system +- [x] Add Cross Tiles Support +- [x] Add AutoCaptcha Solver +- [x] Add Correct Color Pixel Skip +- [x] Separate Css --- +- Contributors: + Contributors + +

diff --git a/RU.md b/RU.md new file mode 100644 index 00000000..cb07c8e1 --- /dev/null +++ b/RU.md @@ -0,0 +1,126 @@ +

+ image +

+ +

WPlace AutoBOT

+

+Этот проект является форком от https://github.com/DarkModde/WPlace-AutoBOT +

+

+ Взгляните на наш дискорд: https://discord.gg/knkNRYyQcm +

+

+ Практичные скрипты с интуитивно понятным меню облегчат вам жизнь на WPlace!
+ Идеально подходит для тех, кто хочет автоматически повышать свой уровень или создавать масштабные пиксельные рисунки, не теряя времени даром. + + Внимание: Это чисто образовательный скрипт, демонстрирующий, что можно сделать на wplace.live. Мы не несем ответственности за то, что произойдет, если вы воспользуетесь этим скриптом. +

+ +
+ +

+ Если вам интересно — да! Эти сценарии были разработаны командой, состоящей из бразильцев, вьетнамцев и индонезийцев. HUEHUE! +

+ Brazil +   + Vietnam +   + Indonesia +

+

+ +--- + +

🚀┃Как пользоваться скриптами:

+ +

+ Это очень просто: скопируйте один из приведенных ниже кодов, вставьте его в строку закладок вашего браузера,
+ и запустите его, находясь на wplace.live. +

+ +
+ +### 🎯┃Авто-Фарм +#### АВТОФАРМ ИСПОЛЬЗУЕТ ЗАРЯДЫ ДЛЯ ПОЛУЧЕНИЯ УРОВНЕЙ, А НЕ РИСУЕТ ИЗОБРАЖЕНИЕ ЗА ВАС. ПОЖАЛУЙСТА, ИСПОЛЬЗУЙТЕ ДЛЯ ЭТОГО АВТОМАТИЧЕСКОЕ ИЗОБРАЖЕНИЕ +```js +javascript:fetch("https://raw.githubusercontent.com/Wplace-AutoBot/WPlace-AutoBOT/refs/heads/main/Auto-Farm.js").then(t=>t.text()).then(eval); +``` + +### 🖼️┃Auto-Image + +```js +javascript:fetch("https://raw.githubusercontent.com/Wplace-AutoBot/WPlace-AutoBOT/refs/heads/main/Auto-Image.js").then(t=>t.text()).then(eval); +``` + +
+

📖┃Гайды

+ +--- + +![Часть 1](https://i.imgur.com/yneG5if.png) + +--- + +![Часть 2](https://i.imgur.com/ZRpU0wZ.png) + +--- + +![Часть 3](https://i.imgur.com/lfjfcEw.png) + +
+ + +
+ +> [!ВАЖНО] +>

1. Ни один из сценариев не будет работать, если вы закроете вкладку браузера. Вы должны оставить вкладку WPlace открытой, даже если она находится в фоновом режиме.

+>

2. Никогда не запускайте оба скрипта на одной странице — это может привести к ошибке в вашем WPlace. Если это произойдет, просто обновите страницу!

+>

3. Если скрипт не распознает доступные цвета, просто нажмите кнопку "Нарисовать", чтобы отобразить их — только после этого запустите автоматическое создание изображений.

+ +
+ +### ✨┃Функции + +- [x] Автоматически фармит заряды/уровни +- [x] Пропуск пикселей с правильным цветом +- [x] Оверлей(вам больше не нужен bluemarble) +- [x] Нанесение краски на несколько плиток +- [x] Автокапча с генератором токенов Turnstile +- [x] Предварительная обработка цвета +- [x] Мультиязычность +- [x] Несколько учетных записей с помощью функции сохранения и загрузки + + +--- + + +--- + +

+ Script Demonstration +

+ +--- + +### 📋┃To-do + +- [x] Как заставить работать автоферму +- [ ] Объединить оба скрипта в один +- [ ] Переключатель нескольких учетных записей (в настоящее время разрабатывается) +- [ ] Система очередей для нескольких учетных записей +- [x] Добавить систему наложения +- [x] Добавить поддержку перекрестных плиток +- [x] Добавить средство автоподчеты +- [x] Добавить правильный пропуск цветного пикселя +- [ ] Отдельный Css +- [ ] Учебные пособия + +--- + +- Контрибьютуоры: + Contributors + + +

+ +

diff --git a/UK.md b/UK.md new file mode 100644 index 00000000..4703d2ed --- /dev/null +++ b/UK.md @@ -0,0 +1,125 @@ +

+ image +

+ +

WPlace AutoBOT

+

+Цей проєкт є форком з https://github.com/DarkModde/WPlace-AutoBOT +

+

+ Завітайте на наш Discord: https://discord.gg/knkNRYyQcm +

+

+ Практичні скрипти з інтуїтивним меню, щоб зробити ваше життя на WPlace простішим!
+ Ідеально для тих, хто хоче автоматично прокачувати рівень або будувати масивні піксель-арти — без витрати часу. + + ⚠️ Увага: Це освітній скрипт, створений для демонстрації можливостей на wplace.live. Ми не несемо відповідальності за наслідки його використання. +

+ +
+ +

+ Якщо ви цікавитеся — так! Скрипти були розроблені командою з Бразилії, В’єтнаму та Індонезії. HUEHUE! +

+ Brazil +   + Vietnam +   + Indonesia +

+

+ +--- + +

🚀┃Як користуватися скриптами:

+ +

+ Це дуже просто: скопіюйте один з кодів нижче, вставте його у панель закладок вашого браузера,
+ та запустіть, коли перебуваєте на wplace.live. +

+ +
+ +### 🎯┃Авто-Фарм +#### AUTOFARM ВИКОРИСТОВУЄ ЧАРДЖІ ДЛЯ ОТРИМАННЯ РІВНІВ, ВІН НЕ МАЛЮЄ ЗОБРАЖЕННЯ. ДЛЯ ЦЬОГО ВИКОРИСТОВУЙТЕ AUTO-IMAGE +```js +javascript:fetch("https://raw.githubusercontent.com/Wplace-AutoBot/WPlace-AutoBOT/refs/heads/main/Auto-Farm.js").then(t=>t.text()).then(eval); +``` + +### 🖼️┃Авто-Зображення + +```js +javascript:fetch("https://raw.githubusercontent.com/Wplace-AutoBot/WPlace-AutoBOT/refs/heads/main/Auto-Image.js").then(t=>t.text()).then(eval); +``` + +
+

📖┃Інструкції

+ +--- + +![Частина 1](https://i.imgur.com/yneG5if.png) + +--- + +![Частина 2](https://i.imgur.com/ZRpU0wZ.png) + +--- + +![Частина 3](https://i.imgur.com/lfjfcEw.png) + +
+ + +
+ +> [!IMPORTANT] +>

1. Жоден скрипт не працюватиме, якщо ви закриєте вкладку браузера. Вкладка WPlace має залишатися відкритою, навіть у фоні.

+>

2. Ніколи не запускайте обидва скрипти на одній сторінці — це може зламати WPlace. Якщо це сталося — просто оновіть сторінку!

+>

3. Якщо скрипт не визначає доступні кольори, натисніть кнопку «Paint», щоб вони з’явилися — лише тоді запускайте Auto-Imager.

+ +
+ +### ✨┃Можливості + +- [x] Автоматичне фармлення крапель/рівнів +- [x] Пропуск пікселів з правильним кольором +- [x] Оверлей (більше не потрібен bluemarble) +- [x] Малювання по кількох тайлах +- [x] Авто-розв’язувач капчі з генератором токенів Turnstile +- [x] Розширена обробка кольорів +- [x] Підтримка кількох мов +- [x] Багатоакаунтність через функції збереження та завантаження +- [x] Працює на мобільних :P + + +--- + +--- + +

+ Демонстрація скрипта +

+ +--- + +### 📋┃Список завдань + +- [x] Запуск AutoFarm +- [ ] Об’єднання обох скриптів в один +- [ ] Перемикач акаунтів (у розробці) +- [ ] Система черги акаунтів +- [x] Додати систему оверлею +- [x] Додати підтримку крос-тайлів +- [x] Додати авто-розв’язувач капчі +- [x] Додати пропуск правильних пікселів +- [ ] Винести CSS окремо + +--- + +- Контриб’ютори: + Contributors + + +

+ +

diff --git a/auto-image-styles.css b/auto-image-styles.css new file mode 100644 index 00000000..95b1ffa8 --- /dev/null +++ b/auto-image-styles.css @@ -0,0 +1,1914 @@ +/* WPlace Auto-Image Bot - Unified CSS Styles (decoupled from JS) + Why: bring external CSS in-sync with the UI that Auto-Image.js renders, + fix layout (positions, widths, z-index), and ensure class names match JS + (e.g., .wplace-dragging) so buttons and panels behave correctly. */ + +/* ========================= */ +/* Theme tokens (CSS vars) */ +/* ========================= */ + +/* Import theme files - classic is default */ +@import url('https://wplace-autobot.github.io/WPlace-AutoBOT/main/themes/classic.css'); +@import url('https://wplace-autobot.github.io/WPlace-AutoBOT/main/themes/classic-light.css'); +@import url('https://wplace-autobot.github.io/WPlace-AutoBOT/main/themes/neon.css'); + +/* Default :root CSS variables for 100% classic theme compliance */ +/* These ensure the bot works perfectly even if theme files fail to load */ +:root { + /* Classic theme colors - exact upstream main values */ + --wplace-primary: linear-gradient(135deg, #000 0%, #1a1a1a 100%); + --wplace-secondary: linear-gradient(135deg, #111 0%, #2a2a2a 100%); + --wplace-accent: #222; + --wplace-text: #fff; + --wplace-highlight: #775ce3; + --wplace-highlight-secondary: #d3a4ff; + --wplace-success: #0f0; + --wplace-error: #f00; + --wplace-warning: #fa0; + + /* UI properties */ + --wplace-radius: 12px; + --wplace-btn-radius: 16px; + --wplace-border-style: solid; + --wplace-border-width: 1px; + --wplace-border-color: #222; + --wplace-shadow: 0 8px 32px rgb(0 0 0 / 60%), 0 0 0 1px rgb(255 255 255 / 10%); + --wplace-backdrop: blur(10px); + --wplace-font: 'Segoe UI', roboto, sans-serif; + + /* Z-index layers */ + --wplace-z-overlay: 10000; + --wplace-z-alert: 10002; + --wplace-z-settings: 10002; + + /* Feature toggles */ + --wplace-scanline: 0; + --wplace-pixel-blink: 0; + + /* Icon colors */ + --wplace-icon-primary: #4facfe; + --wplace-icon-secondary: #00f2fe; + --wplace-icon-palette: #f093fb; + + /* Additional UI colors */ + --wplace-danger: #ff6a6a; + --wplace-danger-dark: #ff4757; + --wplace-muted-text: #fffb; + + /* Text variants */ + --wplace-text-secondary: rgb(255 255 255 / 90%); + --wplace-text-muted: rgb(255 255 255 / 70%); + --wplace-text-dim: rgb(255 255 255 / 60%); + --wplace-text-faded: rgb(255 255 255 / 80%); + + /* Background variants */ + --wplace-bg-input: rgb(255 255 255 / 15%); + --wplace-bg-subtle: rgb(255 255 255 / 10%); + --wplace-bg-faint: rgb(255 255 255 / 8%); + --wplace-bg-ghost: rgb(255 255 255 / 6%); + --wplace-bg-whisper: rgb(255 255 255 / 5%); + + /* Border variants */ + --wplace-border-subtle: rgb(255 255 255 / 20%); + --wplace-border-faint: rgb(255 255 255 / 15%); + --wplace-border-ghost: rgb(255 255 255 / 10%); + --wplace-border-ultra-faint: rgb(255 255 255 / 5%); + + /* Shadow variants */ + --wplace-shadow-drag: 0 12px 40px rgb(0 0 0 / 80%), 0 0 0 2px rgb(255 255 255 / 20%); + --wplace-shadow-notification: 0 4px 12px rgb(0 0 0 / 30%); + --wplace-shadow-slider-thumb: 0 3px 6px rgb(0 0 0 / 30%), 0 0 0 2px var(--wplace-icon-primary); + --wplace-shadow-slider-hover: 0 4px 8px rgb(0 0 0 / 40%), 0 0 0 3px var(--wplace-icon-primary); + + /* Animation colors */ + --wplace-pulse-start: rgb(0 255 0 / 70%); + --wplace-pulse-mid: rgb(0 255 0 / 0%); + --wplace-pulse-end: rgb(0 255 0 / 0%); + + /* Slider colors - defaults for classic compatibility */ + --wplace-slider-thumb-bg: white; + --wplace-slider-track-bg: linear-gradient( + to right, + var(--wplace-icon-primary) 0%, + var(--wplace-icon-secondary) 100% + ); +} + +/* Theme classes are now defined in separate files */ +/* Classic theme: ./themes/classic.css */ +/* Neon theme: ./themes/neon.css */ + +/* ========================= */ +/* Core animations (shared) */ +/* ========================= */ +@keyframes neon-glow { + 0%, + 100% { + text-shadow: + 0 0 5px currentcolor, + 0 0 10px currentcolor, + 0 0 15px currentcolor; + } + + 50% { + text-shadow: + 0 0 2px currentcolor, + 0 0 5px currentcolor, + 0 0 8px currentcolor; + } +} + +@keyframes pixel-blink { + 0%, + 50% { + opacity: 1; + } + + 51%, + 100% { + opacity: 0.7; + } +} + +@keyframes scanline { + 0% { + transform: translateY(-100%); + } + + 100% { + transform: translateY(400px); + } +} + +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 var(--wplace-pulse-start); + } + + 70% { + box-shadow: 0 0 0 10px var(--wplace-pulse-mid); + } + + 100% { + box-shadow: 0 0 0 0 var(--wplace-pulse-end); + } +} + +@keyframes slide-in { + from { + transform: translateY(-10px); + opacity: 0; + } + + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes slide-down { + from { + transform: translateX(-50%) translateY(-20px); + opacity: 0; + } + + to { + transform: translateX(-50%) translateY(0); + opacity: 1; + } +} + +@keyframes shimmer { + 0% { + transform: translateX(-100%); + } + + 100% { + transform: translateX(100%); + } +} + +/* ========================= */ +/* Main containers (fixed) */ +/* Align with JS structure */ +/* ========================= */ +#wplace-image-bot-container { + position: fixed; + top: 20px; + left: 20px; + width: 280px; + max-height: calc(100vh - 40px); + padding: 0; + z-index: 9998; + animation: slide-in 0.4s ease-out; + overflow: hidden auto; + transition: all 0.3s ease; + user-select: none; + + /* Default classic theme styling for 100% compliance */ + background: var(--wplace-primary); + color: var(--wplace-text); + border-radius: var(--wplace-radius); + box-shadow: var(--wplace-shadow); + font-family: var(--wplace-font); + backdrop-filter: var(--wplace-backdrop); + border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color); +} + +#wplace-image-bot-container.wplace-dragging { + transition: none; + box-shadow: var(--wplace-shadow-drag); + transform: scale(1.02); + z-index: 9999; +} + +#wplace-image-bot-container.wplace-compact { + width: 240px; +} + +#wplace-image-bot-container.wplace-minimized { + width: 200px; + height: auto; + overflow: hidden; +} + +/* Stats container sits to the right of main (280 + 30 = 330) */ +#wplace-stats-container { + position: fixed; + top: 20px; + left: 310px; + width: 230px; + max-height: calc(100vh - 40px); + padding: 0; + z-index: 9997; + animation: slide-in 0.4s ease-out; + overflow-y: auto; + transition: all 0.3s ease; + user-select: none; + display: none; + + /* Default classic theme styling for 100% compliance */ + background: var(--wplace-primary); + color: var(--wplace-text); + border-radius: var(--wplace-radius); + box-shadow: var(--wplace-shadow); + font-family: var(--wplace-font); + backdrop-filter: var(--wplace-backdrop); + border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color); +} + +#wplace-stats-container.wplace-dragging { + transition: none; +} + +/* Back-compat for legacy class (some earlier CSS used this) */ +.wplace-drag-active { + transition: none !important; + box-shadow: var(--wplace-shadow-drag) !important; + transform: scale(1.02) !important; + z-index: 9999 !important; +} + +/* ========================= */ +/* Header and content blocks */ +/* ========================= */ +.wplace-header { + padding: 8px 12px; + font-size: 13px; + font-weight: 700; + display: flex; + justify-content: space-between; + align-items: center; + cursor: move; + user-select: none; + border-bottom: 1px solid var(--wplace-border-ghost); + transition: background 0.2s ease; + position: relative; + z-index: 2; + + /* Default styling for 100% classic compliance */ + background: var(--wplace-secondary); + color: var(--wplace-highlight); + text-shadow: 0 1px 2px rgb(0 0 0 / 50%); +} + +.wplace-header-title { + display: flex; + align-items: center; + gap: 6px; +} + +.wplace-header-controls { + display: flex; + gap: 6px; +} + +.wplace-header-btn { + border: none; + cursor: pointer; + width: 18px; + height: 18px; + padding: 0; + font-size: 10px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + + /* Default styling for 100% classic compliance */ + background: rgb(255 255 255 / 10%); + color: var(--wplace-text); + border-radius: 4px; + font-family: var(--wplace-font); +} + +.wplace-header-btn:hover { + transform: scale(1.1); + + /* Default styling for 100% classic compliance */ + background: var(--wplace-accent); + color: var(--wplace-text); +} + +.wplace-content { + display: block; + position: relative; + z-index: 2; + padding: 12px; +} + +.wplace-content.wplace-hidden { + display: none; +} + +/* Sections */ +.wplace-status-section { + margin-bottom: 6px; + padding: 8px; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-section { + margin-bottom: 6px; + padding: 8px; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-section-title { + font-size: 11px; + font-weight: 600; + margin-bottom: 8px; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + gap: 6px; + text-transform: uppercase; + letter-spacing: 1px; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-section-title i.arrow { + transition: transform 0.3s ease; +} + +.wplace-section.collapsed .wplace-section-title i.arrow { + transform: rotate(-90deg); +} + +/* ========================= */ +/* Controls and buttons */ +/* ========================= */ +.wplace-controls { + display: flex; + flex-direction: column; + gap: 8px; +} + +.wplace-cooldown-control { + margin-top: 8px; +} + +.wplace-section.collapsed .wplace-controls, +.wplace-section.collapsed .wplace-cooldown-control { + max-height: 0; + opacity: 0; + pointer-events: none; +} + +.wplace-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} + +.wplace-row.single { + grid-template-columns: 1fr; +} + +.wplace-btn { + padding: 8px 12px; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + font-size: 11px; + transition: all 0.3s ease; + position: relative; + overflow: hidden; + font-weight: 500; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none !important; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-btn::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + transition: left 0.5s ease; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-btn:disabled::before { + display: none; +} + +.wplace-btn:hover:not(:disabled)::before { + left: 100%; +} + +.wplace-btn:hover:not(:disabled) { + transform: translateY(-1px); + + /* Theme-specific styling applied via theme files */ +} + +.wplace-btn:active:not(:disabled) { + transform: translateY(0); +} + +/* Button variants moved to theme files */ + +/* ========================= */ +/* Stats and progress */ +/* ========================= */ +.wplace-stats { + margin-bottom: 8px; + padding: 8px; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-stat-item { + display: flex; + justify-content: space-between; + padding: 4px 0; + font-size: 11px; + border-bottom: 1px solid var(--wplace-border-ultra-faint); +} + +.wplace-stat-item:last-child { + border-bottom: none; +} + +.wplace-stat-label { + display: flex; + align-items: center; + gap: 6px; + opacity: 0.9; + font-size: 10px; +} + +.wplace-stat-value { + font-weight: 600; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-colors-section { + margin-top: 10px; + padding-top: 8px; + border-top: 1px solid var(--wplace-border-ultra-faint); +} + +.wplace-stat-colors-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(16px, 1fr)); + gap: 4px; + margin-top: 8px; + padding: 4px; + max-height: 80px; + overflow-y: auto; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-colors-placeholder { + text-align: center; + color: #888; + padding: 20px; + font-style: italic; +} + +.wplace-cooldown-value { + font-weight: bold; + min-width: 20px; + text-align: center; + display: inline-block; +} + +.wplace-stat-color-swatch { + width: 16px; + height: 16px; + + /* Theme-specific styling applied via theme files */ +} + +/* Progress */ +.wplace-progress { + width: 100%; + margin: 8px 0; + overflow: hidden; + height: 6px; + position: relative; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-progress-bar { + height: 6px; + transition: width 0.5s ease; + position: relative; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-progress-bar::after { + content: ''; + position: absolute; + inset: 0; + animation: shimmer 2s infinite; + + /* Theme-specific styling applied via theme files */ +} + +/* ========================= */ +/* Status blocks */ +/* ========================= */ +.wplace-status { + text-align: center; + position: relative; + overflow: hidden; + padding: 6px; + border: 1px solid; + font-size: 11px; + + /* Theme-specific styling applied via theme files */ +} + +/* Status styling moved to theme files */ + +/* ========================= */ +/* Resize dialog */ +/* ========================= */ +.resize-container { + display: none; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + padding: 20px; + z-index: 10000; + width: 90%; + max-width: 700px; + max-height: 90%; + overflow: auto; + + /* Theme-specific styling applied via theme files */ +} + +.resize-preview-wrapper { + display: flex; + justify-content: center; + align-items: center; + margin: 15px 0; + height: 300px; + overflow: hidden; + + /* Theme-specific styling applied via theme files */ +} + +.resize-canvas-stack { + position: relative; + transform-origin: center center; + display: inline-block; +} + +.resize-base-canvas, +.resize-mask-canvas { + position: absolute; + left: 0; + top: 0; + image-rendering: -moz-crisp-edges; + image-rendering: pixelated; +} + +.resize-mask-canvas { + pointer-events: auto; +} + +.resize-tools { + display: flex; + gap: 8px; + align-items: center; + margin-top: 8px; + font-size: 12px; +} + +/* Missing button hover styles */ +.resize-tools button { + padding: 6px 10px; + border-radius: 6px; + border: 1px solid var(--wplace-border-subtle); + background: var(--wplace-bg-ghost); + color: var(--wplace-text); + cursor: pointer; + + /* Theme-specific styling applied via theme files */ +} + +/* Button active states moved to theme files */ + +.resize-controls { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; + align-items: center; +} + +.resize-slider { + width: 100%; + height: 4px; + border: none; + outline: none; + -webkit-appearance: none; + + /* Theme-specific styling applied via theme files */ +} + +.resize-zoom-controls { + grid-column: 1 / -1; + display: flex; + align-items: center; + gap: 8px; + margin-top: 15px; + flex-wrap: wrap; +} + +.resize-buttons { + display: flex; + gap: 10px; + justify-content: center; + margin-top: 20px; +} + + +/* ========================= */ +/* Color grid */ +/* ========================= */ +.wplace-color-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(60px, 1fr)); + gap: 10px; + padding-top: 8px; + max-height: 300px; + overflow-y: auto; +} + +.wplace-color-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; +} + +.wplace-color-item-name { + font-size: 9px; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 100%; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-color-swatch { + width: 22px; + height: 22px; + cursor: pointer; + transition: + transform 0.1s ease, + box-shadow 0.2s ease; + position: relative; + margin: 0 auto; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-color-swatch.unavailable { + border-style: dashed; + cursor: not-allowed; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-color-swatch:hover { + transform: scale(1.1); + z-index: 1; +} + +.wplace-color-swatch:not(.active) { + opacity: 0.3; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-color-swatch.unavailable:not(.active) { + opacity: 0.2; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-color-swatch.active::after { + content: '✔'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: var(--wplace-text); + font-size: 12px; + font-weight: bold; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-color-divider { + border: none; + height: 1px; + margin: 8px 0; + + /* Theme-specific styling applied via theme files */ +} + +/* ========================= */ +/* Cooldown controls */ +/* ========================= */ +.wplace-cooldown-control label { + font-size: 11px; + margin-bottom: 4px; + display: block; +} + +.wplace-slider-container { + display: flex; + align-items: center; + gap: 8px; +} + +.wplace-slider { + flex: 1; + -webkit-appearance: none; + appearance: none; + height: 4px; + outline: none; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 14px; + height: 14px; + cursor: pointer; + + /* Theme-specific styling applied via theme files */ +} + +/* ========================= */ +/* Settings container (base) */ +/* ========================= */ +#wplace-settings-container { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + padding: 0; + z-index: 10002; + display: none; + min-width: 420px; + max-width: 480px; + overflow: hidden; + + /* Default classic theme styling for 100% compliance */ + background: var(--wplace-primary); + color: var(--wplace-text); + border-radius: var(--wplace-radius); + box-shadow: var(--wplace-shadow); + font-family: var(--wplace-font); + backdrop-filter: var(--wplace-backdrop); + border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color); +} + +#wplace-settings-container.show { + display: block; + animation: settings-slide-in 0.4s ease-out; +} + +@keyframes settings-slide-in { + from { + opacity: 0; + transform: translate(-50%, -50%) scale(0.9); + } + + to { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } +} + +@keyframes settings-fade-out { + from { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } + + to { + opacity: 0; + transform: translate(-50%, -50%) scale(0.9); + } +} + +.wplace-settings { + padding: 16px; + max-height: 400px; + overflow-y: auto; +} + +.wplace-setting-section { + margin-bottom: 20px; + padding: 12px; +} + +/* ========================= */ +/* Form controls */ +/* ========================= */ +.wplace-select { + width: 100%; + padding: 8px 12px; + font-size: 14px; + margin-bottom: 10px; +} + +.wplace-select:focus { + outline: none; +} + +.wplace-description { + font-size: 12px; + opacity: 0.8; + line-height: 1.4; +} + +/* Speed controls */ +.wplace-speed-control { + margin-top: 12px; + padding: 12px; +} + +.wplace-speed-label { + display: flex; + align-items: center; + margin-bottom: 8px; + font-size: 13px; + font-weight: 600; +} + +.wplace-speed-label i { + margin-right: 6px; +} + + +.wplace-speed-slider { + flex: 1; + height: 8px; + outline: none; + -webkit-appearance: none; + appearance: none; + cursor: pointer; + background: var(--wplace-slider-track-bg); + border-radius: 4px; +} + + +.wplace-speed-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 18px; + height: 18px; + cursor: pointer; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-speed-slider::-moz-range-thumb { + width: 18px; + height: 18px; + cursor: pointer; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-speed-display { + display: flex; + align-items: center; + gap: 4px; + min-width: 90px; + justify-content: flex-end; +} + +/* ========================= */ +/* Turnstile overlay */ +/* ========================= */ +/* Hidden invisible widget host (token generator) */ +.wplace-turnstile-hidden { + 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; +} + +/* Visible overlay (interactive fallback) */ +.wplace-turnstile-overlay { + position: fixed !important; + bottom: 20px !important; + right: 20px !important; + z-index: 99999 !important; + padding: 20px !important; + min-width: 300px !important; + max-width: 400px !important; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-turnstile-title { + font: + 600 12px/1.3 'Segoe UI', + sans-serif !important; + margin-bottom: 8px !important; + opacity: 0.9 !important; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-turnstile-host { + width: 100% !important; + min-height: 70px !important; +} + +.wplace-turnstile-hide-btn { + position: absolute !important; + top: 6px !important; + right: 6px !important; + font-size: 11px !important; + background: transparent !important; + padding: 2px 6px !important; + cursor: pointer !important; + transition: background 0.2s ease !important; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-turnstile-hide-btn:hover { + /* Theme-specific styling applied via theme files */ +} + +/* ========================= */ +/* Alert system (used by JS) */ +/* ========================= */ +.wplace-alert-base { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + padding: 12px 20px; + color: var(--wplace-text); + font-weight: 600; + z-index: 10002; + max-width: 400px; + text-align: center; + animation: slide-down 0.3s ease-out; + box-shadow: var(--wplace-shadow-notification); + + /* Theme-specific styling applied via theme files */ +} + +/* Alert styling moved to theme files */ + +/* ========================= */ +/* Modal overlay helpers */ +/* ========================= */ +.wplace-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 100002; + display: none; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-overlay.show { + display: block; +} + +/* Overlay visibility helpers */ +.wplace-overlay-hidden { + display: none !important; +} + +.wplace-overlay-visible { + display: block !important; +} + +/* ========================= */ +/* Responsive tweaks */ +/* ========================= */ +@media (width <= 768px) { + #wplace-image-bot-container { + left: 10px; + width: calc(100vw - 20px); + max-height: calc(100vh - 20px); + } + + #wplace-stats-container { + display: none !important; /* hide secondary panel on small screens */ + } + + .wplace-alert-base { + max-width: 90vw; + margin: 0 5vw; + } + + .wplace-turnstile-overlay { + bottom: 10px !important; + right: 10px !important; + left: 10px !important; + min-width: auto !important; + } +} + +/* Auto light/dark support moved to theme files */ + +/* ===================================== */ +/* Settings container variants (optional) */ +/* ===================================== */ +.wplace-settings-container-base { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 100001; + display: none; + min-width: 400px; + max-width: 500px; + max-height: 100vh; + overflow-y: auto; + animation: slide-in 0.3s ease-out; + border: 1px solid var(--wplace-border-ghost); + padding: 0; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-settings-container-base.show { + display: block; +} + +/* ===================================== */ +/* Utility styles */ +/* ===================================== */ +.wplace-paint-effect { + animation: pulse 0.5s ease-out; +} + +.wplace-settings-error { + /* Theme-specific styling applied via theme files */ +} + +.wplace-stats-container.hidden { + display: none; +} + +/* Theme-specific effects are now in separate theme files */ + +/* ===================================== */ +/* Settings Dialog Styles */ +/* ===================================== */ + +/* Settings content container */ +.wplace-settings-content { + padding: 25px 25px 0; + max-height: 67vh; + overflow-y: auto; +} + +/* Settings section containers */ +.wplace-settings-section { + margin-bottom: 25px; +} + +/* Section labels */ +.wplace-settings-section-label { + margin-bottom: 12px; + color: var(--wplace-text); + font-weight: 500; + font-size: 16px; + display: flex; + align-items: center; + gap: 8px; +} + +/* Icon colors */ +.wplace-icon-key { + font-size: 16px; +} + +.wplace-icon-robot { + font-size: 16px; +} + +.wplace-icon-speed { + font-size: 16px; +} + +.wplace-icon-bell { + font-size: 16px; +} + +.wplace-icon-palette { + font-size: 16px; +} + +.wplace-icon-globe { + font-size: 16px; +} + +.wplace-icon-paint { + font-size: 16px; +} + +/* Section wrapper styling */ +.wplace-settings-section-wrapper { + padding: 18px; + + /* Theme-specific styling applied via theme files */ +} + +/* Select dropdowns */ +.wplace-settings-select { + width: 100%; + padding: 12px 16px; + font-size: 14px; + outline: none; + cursor: pointer; + transition: all 0.3s ease; + font-family: inherit; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-settings-option { + padding: 10px; + + /* Theme-specific styling applied via theme files */ +} + +/* Settings description text */ +.wplace-settings-description { + font-size: 12px; + color: var(--wplace-text-muted); + margin: 8px 0 0; +} + +/* Batch Controls */ +.wplace-batch-controls { + padding: 18px; + margin-bottom: 15px; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-random-batch-controls { + display: none; +} + +/* Speed Slider Container */ +.wplace-speed-slider-container { + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 10px; +} + + +.wplace-speed-value { + min-width: 100px; + text-align: center; + padding: 8px 12px; + font-weight: bold; + font-size: 13px; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-speed-labels { + display: flex; + justify-content: space-between; + color: var(--wplace-text-muted); + font-size: 11px; + margin-top: 8px; +} + +/* Random Batch Controls */ +.wplace-random-batch-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; +} + +.wplace-random-batch-label { + display: block; + color: var(--wplace-text-faded); + font-size: 12px; + margin-bottom: 8px; +} + +.wplace-icon-min { + color: var(--wplace-icon-primary); + margin-right: 4px; +} + +.wplace-icon-max { + color: var(--wplace-icon-secondary); + margin-right: 4px; +} + +.wplace-settings-number-input { + width: 100%; + padding: 10px 12px; + background: var(--wplace-bg-input); + color: var(--wplace-text); + border: 1px solid var(--wplace-border-subtle); + border-radius: 8px; + font-size: 13px; + outline: none; +} + +.wplace-random-batch-description { + font-size: 11px; + color: var(--wplace-text-dim); + margin: 8px 0 0; + text-align: center; +} + +/* Speed Control Toggle */ +.wplace-speed-control-toggle { + display: flex; + align-items: center; + gap: 8px; + color: var(--wplace-text); +} + +.wplace-speed-checkbox { + cursor: pointer; +} + +/* Overlay Settings */ +.wplace-overlay-opacity-control { + margin-bottom: 15px; +} + +.wplace-overlay-opacity-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.wplace-overlay-opacity-label { + font-weight: 500; + font-size: 13px; +} + +.wplace-overlay-opacity-value { + min-width: 40px; + text-align: center; + background: var(--wplace-accent); + color: var(--wplace-text); + padding: 4px 8px; + border-radius: 6px; + font-size: 12px; + border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color); +} + +.wplace-overlay-opacity-slider { + width: 100%; + -webkit-appearance: none; + height: 8px; + outline: none; + cursor: pointer; +} + +.wplace-settings-toggle { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; +} + +.wplace-settings-toggle-title { + font-weight: 500; +} + +.wplace-settings-toggle-description { + font-size: 12px; + margin: 4px 0 0; + color: var(--wplace-muted-text); +} + +.wplace-settings-checkbox { + cursor: pointer; + width: 20px; + height: 20px; + flex-shrink: 0; + accent-color: var(--wplace-highlight); +} + +/* Notifications */ +.wplace-notifications-wrapper { + display: flex; + flex-direction: column; + gap: 10px; +} + +.wplace-notification-toggle { + display: flex; + align-items: center; + justify-content: space-between; +} + +.wplace-notification-checkbox { + width: 18px; + height: 18px; + cursor: pointer; +} + +.wplace-notification-interval { + display: flex; + align-items: center; + gap: 10px; +} + +.wplace-notification-interval-input { + width: 70px; + padding: 6px 8px; + border-radius: 6px; + border: 1px solid var(--wplace-border-subtle); + background: var(--wplace-bg-faint); + color: var(--wplace-text); +} + +.wplace-notification-buttons { + display: flex; + gap: 10px; +} + +.wplace-notification-perm-btn, +.wplace-notification-test-btn { + flex: 1; +} + +/* Settings Footer */ +.wplace-settings-footer { + border-top: 1px solid var(--wplace-border-ghost); + padding: 20px; + position: sticky; + bottom: 0; + background: var(--wplace-secondary); +} + +/* Settings select option styling moved to theme files */ +/* Settings description styling moved to theme files */ + +.wplace-settings-apply-btn { + width: 100%; + border: none; + padding: 10px 16px; + cursor: pointer; + font-weight: 500; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 8px; + justify-content: center; + + /* Theme-specific styling applied via theme files */ +} + +/* ===================================== */ +/* Resize Dialog Styles */ +/* ===================================== */ + +/* Resize dialog title */ +.resize-dialog-title { + margin-top: 0; + color: var(--wplace-text); +} + +/* Resize control labels */ +.resize-control-label { + display: block; + margin-bottom: 8px; + font-size: 14px; +} + +.resize-checkbox-label { + display: flex; + align-items: center; + margin-bottom: 8px; + font-size: 14px; + gap: 8px; +} + +/* Zoom controls */ + +.resize-zoom-btn { + padding: 4px 8px; +} + +.resize-zoom-slider { + max-width: 220px; +} + +.resize-zoom-value { + margin-left: 6px; + min-width: 48px; + text-align: right; + opacity: 0.85; + font-size: 12px; +} + +.resize-camera-help { + font-size: 11px; + opacity: 0.75; + margin-left: auto; +} + +/* Canvas positioning */ +.resize-pan-stage { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; +} + +.resize-canvas-positioned { + position: absolute; + left: 0; + top: 0; + transform-origin: top left; +} + +/* Resize tools */ +.resize-tools-container { + display: flex; + gap: 10px; + flex-wrap: wrap; + align-items: center; +} + +.resize-brush-controls { + display: flex; + flex-direction: column; + gap: 8px; +} + +.resize-brush-control { + display: flex; + align-items: center; + gap: 6px; + justify-content: space-between; +} + +.resize-tool-label { + font-size: 12px; + opacity: 0.85; +} + +.resize-tool-input-group { + display: flex; + align-items: center; + gap: 6px; +} + +.resize-tool-slider { + width: 120px; +} + +.resize-tool-value { + font-size: 12px; + opacity: 0.85; + min-width: 18px; + text-align: center; +} + +.resize-mode-controls { + display: flex; + align-items: center; + gap: 6px; +} + +.resize-mode-group { + display: flex; + gap: 6px; +} + +.resize-mode-btn, +.resize-clear-btn, +.resize-invert-btn { + padding: 4px 8px; + font-size: 12px; +} + +.resize-shortcut-help { + opacity: 0.8; + font-size: 12px; +} + +/* Color palette section */ +.resize-color-palette-section { + margin-top: 15px; +} + +.resize-color-toggle-label { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; +} + +.resize-color-checkbox { + cursor: pointer; +} + +/* Advanced color section */ +.resize-advanced-color-section { + margin-top: 15px; +} + +.resize-advanced-controls { + display: flex; + flex-direction: column; + gap: 10px; +} + +.resize-advanced-label { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 12px; +} + +.resize-advanced-label-text { + font-weight: 600; +} + +.resize-advanced-select { + padding: 6px 8px; + border-radius: 6px; + border: 1px solid var(--wplace-border-faint); + background: var(--wplace-bg-whisper); + color: var(--wplace-text); +} + +.resize-advanced-toggle { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 12px; +} + +.resize-advanced-toggle-content { + flex: 1; +} + +.resize-advanced-description { + margin-top: 2px; + opacity: 0.65; +} + +.resize-advanced-checkbox { + width: 18px; + height: 18px; + cursor: pointer; +} + +.resize-chroma-weight-control { + display: flex; + flex-direction: column; + gap: 4px; +} + +.resize-chroma-weight-header { + display: flex; + justify-content: space-between; + font-size: 11px; + margin-bottom: 4px; +} + +.resize-chroma-weight-value { + background: var(--wplace-bg-faint); + padding: 2px 6px; + border-radius: 4px; +} + +.resize-chroma-weight-slider { + width: 100%; +} + +.resize-threshold-controls { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +.resize-threshold-label { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 12px; +} + +.resize-threshold-input { + padding: 6px 8px; + border-radius: 6px; + border: 1px solid var(--wplace-border-faint); + background: var(--wplace-bg-whisper); + color: var(--wplace-text); +} + +.resize-reset-advanced-btn { + background: linear-gradient(135deg, var(--wplace-danger), var(--wplace-danger-dark)); + font-size: 11px; +} + +/* ===================================== */ +/* Additional styles for exact upstream match */ +/* ===================================== */ + +/* Overlay styles exact match */ +.resize-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 9999; + display: none; + + /* Theme-specific styling applied via theme files */ +} + +/* Settings animations - removed duplicate, handled above */ + +/* Settings Header Styling */ +.wplace-settings-header { + background: var(--wplace-accent); + padding: 20px; + border-bottom: 1px solid var(--wplace-border-color); + cursor: move; +} + +.wplace-settings-title-wrapper { + display: flex; + justify-content: space-between; + align-items: center; +} + +.wplace-settings-title { + margin: 0; + color: var(--wplace-text); + font-size: 20px; + font-weight: 300; + display: flex; + align-items: center; + gap: 10px; +} + +.wplace-settings-icon { + font-size: 18px; + color: var(--wplace-highlight); + animation: spin 2s linear infinite; +} + +.wplace-settings-close-btn { + background: rgb(34 34 34 / 40%); + color: var(--wplace-text); + border: 1px solid var(--wplace-border-color); + border-radius: 50%; + width: 32px; + height: 32px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + font-size: 14px; + font-weight: 300; +} + +.wplace-settings-close-btn:hover { + background: rgb(255 0 0 / 40%); + transform: scale(1.1); +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} + +/* Font styling corrections */ +.resize-controls label { + font-size: 12px; + color: var(--wplace-text); +} + + +/* Additional precision fixes for exact upstream main matching */ +/* Status default styling moved to theme files */ + +/* Duplicate settings section removed - all settings styling moved to theme files */ + +/* Settings header styling moved to theme files */ +/* All remaining settings styling moved to theme files */ + + +/* Icon styling moved to theme files */ +.wplace-icon-eye { + font-size: 16px; +} + +/* Overlay Settings Controls */ +.wplace-overlay-opacity-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--wplace-slider-thumb-bg); + box-shadow: var(--wplace-shadow-slider-thumb); + cursor: pointer; + transition: all 0.2s ease; +} + +.wplace-overlay-opacity-slider::-webkit-slider-thumb:hover { + transform: scale(1.2); + box-shadow: var(--wplace-shadow-slider-hover); +} + +/* Coordinate Generation Controls */ +.wplace-icon-route { + color: var(--wplace-icon-primary); + font-size: 16px; +} + +.wplace-icon-table { + color: var(--wplace-icon-palette); + margin-right: 6px; +} + +.wplace-icon-compass { + color: var(--wplace-icon-secondary); + margin-right: 6px; +} + +/* Pixel Filter Controls */ +.wplace-pixel-filter-controls { + padding: 18px; + margin-bottom: 15px; + display: flex; + flex-direction: column; + gap: 12px; + + /* Theme-specific styling applied via theme files */ +} + +/* Snake Pattern Controls */ +.wplace-snake-pattern-controls { + padding: 18px; + margin-bottom: 15px; + + /* Theme-specific styling applied via theme files */ +} + +/* Block Size Controls */ +.wplace-block-size-controls { + padding: 18px; + margin-bottom: 15px; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-block-size-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; +} + +.wplace-block-size-label { + display: block; + color: var(--wplace-text-faded); + font-size: 12px; + margin-bottom: 8px; +} + +.wplace-icon-width { + color: var(--wplace-icon-primary); + margin-right: 4px; +} + +.wplace-icon-height { + color: var(--wplace-icon-secondary); + margin-right: 4px; +} + +.wplace-block-size-description { + font-size: 11px; + color: var(--wplace-text-dim); + margin: 8px 0 0; + text-align: center; +} + +/* Random Block Controls */ +.wplace-shuffle-block-size-controls { + display: none; +} + +/* Batch Mode Controls */ +.wplace-mode-selection { + margin-bottom: 15px; +} + +.wplace-mode-label { + display: block; + margin-bottom: 8px; + color: var(--wplace-text-secondary); + font-weight: 500; + font-size: 14px; +} + +.wplace-icon-dice { + color: var(--wplace-icon-palette); + margin-right: 6px; +} diff --git a/lang/en.json b/lang/en.json new file mode 100644 index 00000000..d2c493d2 --- /dev/null +++ b/lang/en.json @@ -0,0 +1,154 @@ +{ + "title": "Auto-Image", + "toggleOverlay": "Toggle Overlay", + "scanColors": "Scan Colors", + "uploadImage": "Upload", + "resizeImage": "Resize", + "selectPosition": "Select Position", + "startPainting": "Start", + "stopPainting": "Stop", + "checkingColors": "🔍 Checking available colors...", + "noColorsFound": "❌ To update the color swatch, open the color palette on the site and try again!", + "colorsUpdated": "✅ Available colors increased {oldCount} -> {newCount}, {diffCount} new colors found", + "colorsFound": "✅ {count} available colors found. Ready to upload.", + "loadingImage": "🖼️ Loading image...", + "imageLoaded": "✅ Image loaded with {count} valid pixels", + "imageError": "❌ Error loading image", + "selectPositionAlert": "Paint the first pixel at the location where you want the art to start!", + "waitingPosition": "👆 Waiting for you to paint the reference pixel...", + "positionSet": "✅ Position set successfully!", + "positionTimeout": "❌ Timeout for position selection", + "startPaintingMsg": "🎨 Starting painting...", + "paintingProgress": "🧱 Progress: {painted}/{total} pixels...", + "noCharges": "⌛ No charges. Waiting {time}...", + "overlayTilesNotLoaded": "❌ Required map tiles not loaded. Check connection or retry.", + "paintingStoppedByUser": "⏹️ Painting stopped by user", + "paintingBatchFailed": "❌ Failed to send pixel batch after retries. Painting stopped.", + "paintingPixelCheckFailed": "❌ Failed to read pixel at ({x}, {y}). Painting stopped.", + "paintingFinalBatchFailed": "⚠️ Final batch of {count} pixels failed after retries.", + "paintingComplete": "✅ Painting complete! {count} pixels painted.", + "paintingError": "❌ Unexpected error during painting", + "missingRequirements": "❌ Load an image and select a position first", + "progress": "Progress", + "pixels": "Pixels", + "charges": "Charges", + "fullChargeIn": "Full Charge In", + "estimatedTime": "Estimated time", + "initMessage": "Click 'Upload Image' to begin", + "waitingInit": "Waiting for initialization...", + "initializingToken": "🔧 Initializing Turnstile token generator...", + "tokenReady": "✅ Token generator ready - you can now start painting!", + "tokenRetryLater": "⚠️ Token generator will retry when needed", + "resizeSuccess": "✅ Image resized to {width}x{height}", + "paintingPaused": "⏸️ Painting paused at position X: {x}, Y: {y}", + "captchaNeeded": "❗ Token generation failed. Please try again in a moment.", + "saveData": "Save Progress", + "loadData": "Load Progress", + "saveToFile": "Save to File", + "loadFromFile": "Load from File", + "dataManager": "Data Manager", + "autoSaved": "✅ Progress saved automatically", + "dataLoaded": "✅ Progress loaded successfully", + "fileSaved": "✅ Progress saved to file successfully", + "fileLoaded": "✅ Progress loaded from file successfully", + "noSavedData": "❌ No saved progress found", + "savedDataFound": "✅ Saved progress found! Load to continue?", + "savedDate": "Saved on: {date}", + "clickLoadToContinue": "Click 'Load Progress' to continue.", + "fileError": "❌ Error processing file", + "invalidFileFormat": "❌ Invalid file format", + "paintingSpeed": "Painting Speed", + "pixelsPerSecond": "pixels/second", + "speedSetting": "Speed: {speed} pixels/sec", + "settings": "Settings", + "botSettings": "Bot Settings", + "close": "Close", + "language": "Language", + "themeSettings": "Theme Settings", + "themeSettingsDesc": "Choose your preferred color theme for the interface.", + "languageSelectDesc": "Select your preferred language. Changes will take effect immediately.", + "autoCaptcha": "Auto-CAPTCHA Solver (Turnstile)", + "autoCaptchaDesc": "Automatically generates Turnstile tokens using integrated generator. Falls back to browser automation if needed.", + "applySettings": "Apply Settings", + "settingsSaved": "✅ Settings saved successfully!", + "speedOn": "On", + "speedOff": "Off", + "cooldownSettings": "Cooldown Settings", + "waitCharges": "Wait until charges reach", + "captchaSolving": "🔑 Generating Turnstile token...", + "captchaFailed": "❌ Turnstile token generation failed. Trying fallback method...", + "automation": "Automation", + "noChargesThreshold": "⌛ Waiting to reach {threshold} charges. Currently {current}. Estimated time: {time}.", + "tokenCapturedSuccess": "Token captured successfully! You can start the bot now.", + "notificationsNotSupported": "Notifications are not supported in this browser.", + "chargesReadyNotification": "WPlace — Charges Ready", + "chargesReadyMessage": "Charges ready: {current} / {max}. Threshold: {threshold}.", + "testNotificationTitle": "WPlace — Test", + "testNotificationMessage": "This is a test notification.", + "showStats": "Show Stats", + "compactMode": "Compact Mode", + "refreshCharges": "Refresh Charges", + "closeStats": "Close Stats", + "zoomOut": "Zoom Out", + "zoomIn": "Zoom In", + "fitToView": "Fit to view", + "actualSize": "Actual size (100%)", + "panMode": "Pan (drag to move view)", + "clearIgnoredPixels": "Clear all ignored pixels", + "invertMask": "Invert mask", + "waitingSetupComplete": "🔄 Waiting for initial setup to complete...", + "waitingTokenGenerator": "🔄 Waiting for token generator to initialize...", + "uploadImageFirst": "Upload an image first to capture available colors", + "pleaseWaitInitialSetup": "🔄 Please wait for the initial setup to complete before loading progress.", + "pleaseWaitFileSetup": "🔄 Please wait for the initial setup to complete before loading from file.", + "errorSavingProgress": "❌ Error saving progress", + "errorLoadingProgress": "❌ Error loading progress", + "fileOperationsAvailable": "📂 File operations (Load/Upload) are now available!", + "tokenGeneratorReady": "🔑 Token generator ready!", + "paintingStats": "Painting Stats", + "enablePaintingSpeedLimit": "Enable painting speed limit (batch size control)", + "enableNotifications": "Enable notifications", + "notifyOnChargesThreshold": "Notify when charges reach threshold", + "onlyWhenNotFocused": "Only when tab is not focused", + "repeatEvery": "Repeat every", + "minutesPl": "minute(s)", + "grantPermission": "Grant Permission", + "test": "Test", + "showAllColorsIncluding": "Show All Colors (including unavailable)", + "chromaWeight": "Chroma Weight", + "downloadPreview": "Download Preview", + "apply": "Apply", + "cancel": "Cancel", + "fit": "Fit", + "hundred": "100%", + "clear": "Clear", + "invert": "Invert", + "reprocessingOverlay": "Re-processing overlay...", + "overlayUpdated": "Overlay updated!", + "notificationsEnabled": "Notifications enabled.", + "notificationsPermissionDenied": "Notifications permission denied.", + "overlayEnabled": "Overlay enabled.", + "overlayDisabled": "Overlay disabled.", + "tokenSourceSet": "Token source set to: {source}", + "batchModeSet": "Batch mode set to: {mode}", + "randomRange": "Random Range", + "normalFixedSize": "Normal Fixed Size", + "advancedColorSettingsReset": "Advanced color settings reset.", + "shiftRowAltColumn": "Shift = Row • Alt = Column", + "hideTurnstileBtn": "Hide", + "turnstileInstructions": "Cloudflare Turnstile — please complete the check if shown", + "uploadImageFirstColors": "Please upload an image first to capture available colors", + "availableColors": "Available Colors ({count})", + "colorTooltip": "ID: {id}\nRGB: {rgb}", + "expandMode": "Expand Mode", + "minimize": "Minimize", + "restore": "Restore", + "hideStats": "Hide Stats", + "paintOptions": "Paint Options", + "paintWhitePixels": "Paint White", + "paintWhitePixelsDescription": "If enabled, template white pixels will be painted.", + "paintTransparentPixels": "Paint Transparent", + "paintTransparentPixelsDescription": "If enabled, template transparent pixels will be painted", + "paintUnavailablePixels": "Paint Unavailable", + "paintUnavailablePixelsDescription": "If enabled, template colors that are unavailable will be painted using the closest available color" +} \ No newline at end of file diff --git a/lang/fr.json b/lang/fr.json new file mode 100644 index 00000000..8d0d2f52 --- /dev/null +++ b/lang/fr.json @@ -0,0 +1,144 @@ +{ + "title": "WPlace Auto-Image", + "toggleOverlay": "Basculer l'overlay", + "scanColors": "Scanner les couleurs", + "uploadImage": "Télécharger l'image", + "resizeImage": "Redimensionner l'image", + "selectPosition": "Sélectionner la position", + "startPainting": "Commencer à peindre", + "stopPainting": "Arrêter de peindre", + "checkingColors": "🔍 Vérification des couleurs disponibles...", + "noColorsFound": "❌ Ouvrez la palette de couleurs sur le site et réessayez!", + "colorsFound": "✅ {count} couleurs trouvées. Prêt à télécharger.", + "loadingImage": "🖼️ Chargement de l'image...", + "imageLoaded": "✅ Image chargée avec {count} pixels valides", + "imageError": "❌ Erreur lors du chargement de l'image", + "selectPositionAlert": "Peignez le premier pixel à l'endroit où vous voulez que l'art commence!", + "waitingPosition": "👆 En attente que vous peigniez le pixel de référence...", + "positionSet": "✅ Position définie avec succès!", + "positionTimeout": "❌ Délai d'attente pour la sélection de position", + "startPaintingMsg": "🎨 Début de la peinture...", + "paintingProgress": "🧱 Progrès: {painted}/{total} pixels...", + "noCharges": "⌛ Aucune charge. En attente {time}...", + "paintingStopped": "⏹️ Peinture arrêtée par l'utilisateur", + "paintingComplete": "✅ Peinture terminée! {count} pixels peints.", + "paintingError": "❌ Erreur pendant la peinture", + "missingRequirements": "❌ Veuillez charger une image et sélectionner une position d'abord", + "progress": "Progrès", + "pixels": "Pixels", + "charges": "Charges", + "estimatedTime": "Temps estimé", + "initMessage": "Cliquez sur 'Télécharger l'image' pour commencer", + "waitingInit": "En attente d'initialisation...", + "initializingToken": "🔧 Initialisation du générateur de tokens Turnstile...", + "tokenReady": "✅ Générateur de tokens prêt - vous pouvez commencer à peindre!", + "tokenRetryLater": "⚠️ Le générateur de tokens réessaiera si nécessaire", + "resizeSuccess": "✅ Image redimensionnée en {width}x{height}", + "paintingPaused": "⏸️ Peinture en pause à la position X: {x}, Y: {y}", + "captchaNeeded": "❗ Échec de la génération de token. Veuillez réessayer dans un moment.", + "saveData": "Sauvegarder le progrès", + "loadData": "Charger le progrès", + "saveToFile": "Sauvegarder dans un fichier", + "loadFromFile": "Charger depuis un fichier", + "dataManager": "Données", + "autoSaved": "✅ Progrès sauvegardé automatiquement", + "dataLoaded": "✅ Progrès chargé avec succès", + "fileSaved": "✅ Sauvegardé dans un fichier avec succès", + "fileLoaded": "✅ Chargé depuis un fichier avec succès", + "noSavedData": "❌ Aucun progrès sauvegardé trouvé", + "savedDataFound": "✅ Progrès sauvegardé trouvé! Charger pour continuer?", + "savedDate": "Sauvegardé le: {date}", + "clickLoadToContinue": "Cliquez sur 'Charger le progrès' pour continuer.", + "fileError": "❌ Erreur lors du traitement du fichier", + "invalidFileFormat": "❌ Format de fichier invalide", + "paintingSpeed": "Vitesse de peinture", + "pixelsPerSecond": "pixels/seconde", + "speedSetting": "Vitesse: {speed} pixels/sec", + "settings": "Paramètres", + "botSettings": "Paramètres du Bot", + "close": "Fermer", + "language": "Langue", + "themeSettings": "Paramètres de Thème", + "themeSettingsDesc": "Choisissez votre thème de couleurs préféré pour l'interface.", + "languageSelectDesc": "Sélectionnez votre langue préférée. Les changements prendront effet immédiatement.", + "autoCaptcha": "Résolveur de CAPTCHA automatique (Turnstile)", + "autoCaptchaDesc": "Génère automatiquement des jetons Turnstile en utilisant le générateur intégré. Se replie sur l'automatisation du navigateur si nécessaire.", + "applySettings": "Appliquer les paramètres", + "settingsSaved": "✅ Paramètres enregistrés avec succès !", + "speedOn": "Activé", + "speedOff": "Désactivé", + "cooldownSettings": "Paramètres de recharge", + "waitCharges": "Attendre que les charges atteignent", + "captchaSolving": "🔑 Génération du jeton Turnstile...", + "captchaFailed": "❌ Échec de génération du jeton Turnstile. Tentative de méthode alternative...", + "automation": "Automatisation", + "noChargesThreshold": "⌛ En attente que les charges atteignent {threshold}. Actuel: {current}. Prochaine dans {time}...", + "tokenCapturedSuccess": "Jeton capturé avec succès ! Vous pouvez démarrer le bot maintenant.", + "notificationsNotSupported": "Les notifications ne sont pas supportées dans ce navigateur.", + "chargesReadyNotification": "WPlace — Charges Prêtes", + "chargesReadyMessage": "Charges prêtes : {current} / {max}. Seuil : {threshold}.", + "testNotificationTitle": "WPlace — Test", + "testNotificationMessage": "Ceci est une notification de test.", + "showStats": "Afficher les Stats", + "compactMode": "Mode Compact", + "refreshCharges": "Actualiser les Charges", + "closeStats": "Fermer les Stats", + "zoomOut": "Dézoomer", + "zoomIn": "Zoomer", + "fitToView": "Ajuster à la vue", + "actualSize": "Taille réelle (100%)", + "panMode": "Panoramique (glisser pour déplacer la vue)", + "clearIgnoredPixels": "Effacer tous les pixels ignorés", + "invertMask": "Inverser le masque", + "waitingSetupComplete": "🔄 En attente de la fin de l'installation initiale...", + "waitingTokenGenerator": "🔄 En attente de l'initialisation du générateur de jetons...", + "uploadImageFirst": "Téléchargez d'abord une image pour capturer les couleurs disponibles", + "pleaseWaitInitialSetup": "🔄 Veuillez attendre la fin de l'installation initiale avant de charger les progrès.", + "pleaseWaitFileSetup": "🔄 Veuillez attendre la fin de l'installation initiale avant de charger depuis un fichier.", + "errorSavingProgress": "❌ Erreur lors de la sauvegarde des progrès", + "errorLoadingProgress": "❌ Erreur lors du chargement des progrès", + "fileOperationsAvailable": "📂 Les opérations sur fichiers (Charger/Télécharger) sont maintenant disponibles !", + "tokenGeneratorReady": "🔑 Générateur de jetons prêt !", + "paintingStats": "Statistiques de Peinture", + "enablePaintingSpeedLimit": "Activer la limite de vitesse de peinture (contrôle de la taille de lot)", + "enableNotifications": "Activer les notifications", + "notifyOnChargesThreshold": "Notifier quand les charges atteignent le seuil", + "onlyWhenNotFocused": "Seulement quand l'onglet n'est pas au premier plan", + "repeatEvery": "Répéter toutes les", + "minutesPl": "minute(s)", + "grantPermission": "Accorder la Permission", + "test": "Test", + "showAllColorsIncluding": "Afficher toutes les couleurs (y compris indisponibles)", + "chromaWeight": "Poids de Chrominance", + "downloadPreview": "Télécharger l'Aperçu", + "apply": "Appliquer", + "cancel": "Annuler", + "fit": "Ajuster", + "hundred": "100%", + "clear": "Effacer", + "invert": "Inverser", + "reprocessingOverlay": "Retraitement de l'overlay...", + "overlayUpdated": "Overlay mis à jour !", + "notificationsEnabled": "Notifications activées.", + "notificationsPermissionDenied": "Permission de notifications refusée.", + "overlayEnabled": "Overlay activé.", + "overlayDisabled": "Overlay désactivé.", + "tokenSourceSet": "Source de jeton définie à : {source}", + "batchModeSet": "Mode lot défini à : {mode}", + "randomRange": "Plage Aléatoire", + "normalFixedSize": "Taille Fixe Normale", + "advancedColorSettingsReset": "Paramètres de couleur avancés réinitialisés.", + "shiftRowAltColumn": "Shift = Ligne • Alt = Colonne", + "hideTurnstileBtn": "Masquer", + "turnstileInstructions": "Cloudflare Turnstile — veuillez compléter la vérification si affichée", + "uploadImageFirstColors": "Veuillez d'abord télécharger une image pour capturer les couleurs disponibles", + "availableColors": "Couleurs Disponibles ({count})", + "colorTooltip": "ID : {id}\nRVB : {rgb}", + "expandMode": "Mode Étendu", + "minimize": "Réduire", + "restore": "Restaurer", + "hideStats": "Masquer les Stats", + "paintOptions": "Options de peinture", + "paintWhitePixels": "Peindre les pixels blancs", + "paintTransparentPixels": "Peindre les pixels transparents" +} diff --git a/lang/id.json b/lang/id.json new file mode 100644 index 00000000..9cad6e2b --- /dev/null +++ b/lang/id.json @@ -0,0 +1,144 @@ +{ + "title": "WPlace Auto-Image", + "toggleOverlay": "Toggle Overlay", + "scanColors": "Pindai Warna", + "uploadImage": "Unggah Gambar", + "resizeImage": "Ubah Ukuran Gambar", + "selectPosition": "Pilih Posisi", + "startPainting": "Mulai Melukis", + "stopPainting": "Berhenti Melukis", + "checkingColors": "🔍 Memeriksa warna yang tersedia...", + "noColorsFound": "❌ Buka palet warna di situs dan coba lagi!", + "colorsFound": "✅ {count} warna ditemukan. Siap untuk diunggah.", + "loadingImage": "🖼️ Memuat gambar...", + "imageLoaded": "✅ Gambar dimuat dengan {count} piksel valid", + "imageError": "❌ Kesalahan saat memuat gambar", + "selectPositionAlert": "Lukis piksel pertama di lokasi tempat karya seni akan dimulai!", + "waitingPosition": "👆 Menunggu Anda melukis piksel referensi...", + "positionSet": "✅ Posisi berhasil diatur!", + "positionTimeout": "❌ Waktu habis untuk memilih posisi", + "startPaintingMsg": "🎨 Mulai melukis...", + "paintingProgress": "🧱 Progres: {painted}/{total} piksel...", + "noCharges": "⌛ Tidak ada muatan. Menunggu {time}...", + "paintingStopped": "⏹️ Melukis dihentikan oleh pengguna", + "paintingComplete": "✅ Melukis selesai! {count} piksel telah dilukis.", + "paintingError": "❌ Kesalahan selama melukis", + "missingRequirements": "❌ Unggah gambar dan pilih posisi terlebih dahulu", + "progress": "Progres", + "pixels": "Piksel", + "charges": "Muatan", + "estimatedTime": "Perkiraan waktu", + "initMessage": "Klik 'Unggah Gambar' untuk memulai", + "waitingInit": "Menunggu inisialisasi...", + "initializingToken": "🔧 Menginisialisasi generator token Turnstile...", + "tokenReady": "✅ Generator token siap - Anda bisa mulai melukis!", + "tokenRetryLater": "⚠️ Generator token akan mencoba lagi saat diperlukan", + "resizeSuccess": "✅ Gambar berhasil diubah ukurannya menjadi {width}x{height}", + "paintingPaused": "⏸️ Melukis dijeda di posisi X: {x}, Y: {y}", + "captchaNeeded": "❗ Pembuatan token gagal. Silakan coba lagi sebentar lagi.", + "saveData": "Simpan Progres", + "loadData": "Muat Progres", + "saveToFile": "Simpan ke File", + "loadFromFile": "Muat dari File", + "dataManager": "Data", + "autoSaved": "✅ Progres disimpan secara otomatis", + "dataLoaded": "✅ Progres berhasil dimuat", + "fileSaved": "✅ Berhasil disimpan ke file", + "fileLoaded": "✅ Berhasil dimuat dari file", + "noSavedData": "❌ Tidak ditemukan progres yang disimpan", + "savedDataFound": "✅ Progres yang disimpan ditemukan! Muat untuk melanjutkan?", + "savedDate": "Disimpan pada: {date}", + "clickLoadToContinue": "Klik 'Muat Progres' untuk melanjutkan.", + "fileError": "❌ Kesalahan saat memproses file", + "invalidFileFormat": "❌ Format file tidak valid", + "paintingSpeed": "Kecepatan Melukis", + "pixelsPerSecond": "piksel/detik", + "speedSetting": "Kecepatan: {speed} piksel/detik", + "settings": "Pengaturan", + "botSettings": "Pengaturan Bot", + "close": "Tutup", + "language": "Bahasa", + "themeSettings": "Pengaturan Tema", + "themeSettingsDesc": "Pilih tema warna favorit Anda untuk antarmuka.", + "languageSelectDesc": "Pilih bahasa yang Anda inginkan. Perubahan akan berlaku segera.", + "autoCaptcha": "Penyelesai CAPTCHA Otomatis", + "autoCaptchaDesc": "Mencoba menyelesaikan CAPTCHA secara otomatis dengan mensimulasikan penempatan piksel manual saat token kedaluwarsa.", + "applySettings": "Terapkan Pengaturan", + "settingsSaved": "✅ Pengaturan berhasil disimpan!", + "speedOn": "Nyala", + "speedOff": "Mati", + "cooldownSettings": "Pengaturan Cooldown", + "waitCharges": "Tunggu hingga muatan mencapai", + "captchaSolving": "🤖 Mencoba menyelesaikan CAPTCHA...", + "captchaFailed": "❌ Gagal menyelesaikan CAPTCHA. Lukis satu piksel secara manual.", + "automation": "Automasi", + "noChargesThreshold": "⌛ Menunggu muatan mencapai {threshold}. Saat ini: {current}. Berikutnya dalam {time}...", + "tokenCapturedSuccess": "Token berhasil ditangkap! Anda bisa memulai bot sekarang.", + "notificationsNotSupported": "Notifikasi tidak didukung di browser ini.", + "chargesReadyNotification": "WPlace — Muatan Siap", + "chargesReadyMessage": "Muatan siap: {current} / {max}. Batas: {threshold}.", + "testNotificationTitle": "WPlace — Tes", + "testNotificationMessage": "Ini adalah notifikasi tes.", + "showStats": "Tampilkan Statistik", + "compactMode": "Mode Kompak", + "refreshCharges": "Segarkan Muatan", + "closeStats": "Tutup Statistik", + "zoomOut": "Perkecil", + "zoomIn": "Perbesar", + "fitToView": "Sesuaikan tampilan", + "actualSize": "Ukuran sebenarnya (100%)", + "panMode": "Geser (seret untuk memindahkan tampilan)", + "clearIgnoredPixels": "Bersihkan semua piksel yang diabaikan", + "invertMask": "Balik masker", + "waitingSetupComplete": "🔄 Menunggu pengaturan awal selesai...", + "waitingTokenGenerator": "🔄 Menunggu generator token diinisialisasi...", + "uploadImageFirst": "Unggah gambar terlebih dahulu untuk menangkap warna yang tersedia", + "pleaseWaitInitialSetup": "🔄 Harap tunggu pengaturan awal selesai sebelum memuat progres.", + "pleaseWaitFileSetup": "🔄 Harap tunggu pengaturan awal selesai sebelum memuat dari file.", + "errorSavingProgress": "❌ Kesalahan menyimpan progres", + "errorLoadingProgress": "❌ Kesalahan memuat progres", + "fileOperationsAvailable": "📂 Operasi file (Muat/Unggah) sekarang tersedia!", + "tokenGeneratorReady": "🔑 Generator token siap!", + "paintingStats": "Statistik Melukis", + "enablePaintingSpeedLimit": "Aktifkan batas kecepatan melukis (kontrol ukuran batch)", + "enableNotifications": "Aktifkan notifikasi", + "notifyOnChargesThreshold": "Beri tahu saat muatan mencapai batas", + "onlyWhenNotFocused": "Hanya saat tab tidak difokuskan", + "repeatEvery": "Ulangi setiap", + "minutesPl": "menit", + "grantPermission": "Berikan Izin", + "test": "Tes", + "showAllColorsIncluding": "Tampilkan Semua Warna (termasuk yang tidak tersedia)", + "chromaWeight": "Bobot Kroma", + "downloadPreview": "Unduh Pratinjau", + "apply": "Terapkan", + "cancel": "Batal", + "fit": "Sesuaikan", + "hundred": "100%", + "clear": "Bersihkan", + "invert": "Balik", + "reprocessingOverlay": "Memproses ulang overlay...", + "overlayUpdated": "Overlay diperbarui!", + "notificationsEnabled": "Notifikasi diaktifkan.", + "notificationsPermissionDenied": "Izin notifikasi ditolak.", + "overlayEnabled": "Overlay diaktifkan.", + "overlayDisabled": "Overlay dinonaktifkan.", + "tokenSourceSet": "Sumber token diatur ke: {source}", + "batchModeSet": "Mode batch diatur ke: {mode}", + "randomRange": "Rentang Acak", + "normalFixedSize": "Ukuran Tetap Normal", + "advancedColorSettingsReset": "Pengaturan warna lanjutan direset.", + "shiftRowAltColumn": "Shift = Baris • Alt = Kolom", + "hideTurnstileBtn": "Sembunyikan", + "turnstileInstructions": "Cloudflare Turnstile — harap selesaikan pemeriksaan jika ditampilkan", + "uploadImageFirstColors": "Harap unggah gambar terlebih dahulu untuk menangkap warna yang tersedia", + "availableColors": "Warna Tersedia ({count})", + "colorTooltip": "ID: {id}\nRGB: {rgb}", + "expandMode": "Mode Perluas", + "minimize": "Minimalkan", + "restore": "Pulihkan", + "hideStats": "Sembunyikan Statistik", + "paintOptions": "Opsi Pewarnaan", + "paintWhitePixels": "Warnai piksel putih", + "paintTransparentPixels": "Warnai piksel transparan" +} diff --git a/lang/ja.json b/lang/ja.json new file mode 100644 index 00000000..d2fc9735 --- /dev/null +++ b/lang/ja.json @@ -0,0 +1,144 @@ +{ + "title": "WPlace 自動画像", + "toggleOverlay": "オーバーレイ切替", + "scanColors": "色をスキャン", + "uploadImage": "画像をアップロード", + "resizeImage": "画像サイズ変更", + "selectPosition": "位置を選択", + "startPainting": "描画開始", + "stopPainting": "描画停止", + "checkingColors": "🔍 利用可能な色を確認中...", + "noColorsFound": "❌ サイトでカラーパレットを開いて再試行してください!", + "colorsFound": "✅ 利用可能な色 {count} 件を検出。アップロード可能。", + "loadingImage": "🖼️ 画像を読み込み中...", + "imageLoaded": "✅ 画像を読み込みました。有効なピクセル {count}", + "imageError": "❌ 画像の読み込みエラー", + "selectPositionAlert": "作品を開始したい位置に最初のピクセルを置いてください!", + "waitingPosition": "👆 参照ピクセルの描画を待っています...", + "positionSet": "✅ 位置を設定しました!", + "positionTimeout": "❌ 位置選択のタイムアウト", + "startPaintingMsg": "🎨 描画を開始...", + "paintingProgress": "🧱 進捗: {painted}/{total} ピクセル...", + "noCharges": "⌛ チャージなし。{time} 待機...", + "paintingStopped": "⏹️ ユーザーにより停止されました", + "paintingComplete": "✅ 描画完了! {count} ピクセル描画。", + "paintingError": "❌ 描画中にエラー", + "missingRequirements": "❌ 先に画像を読み込み位置を選択してください", + "progress": "進捗", + "pixels": "ピクセル", + "charges": "チャージ", + "estimatedTime": "推定時間", + "initMessage": "「画像をアップロード」をクリックして開始", + "waitingInit": "初期化待機中...", + "initializingToken": "🔧 Turnstile トークン生成器を初期化中...", + "tokenReady": "✅ トークン生成器準備完了 - 描画できます!", + "tokenRetryLater": "⚠️ 必要に応じて再試行します", + "resizeSuccess": "✅ 画像を {width}x{height} にリサイズ", + "paintingPaused": "⏸️ X: {x}, Y: {y} で一時停止", + "captchaNeeded": "❗ トークン生成に失敗。少ししてから再試行してください。", + "saveData": "進捗を保存", + "loadData": "進捗を読み込み", + "saveToFile": "ファイルへ保存", + "loadFromFile": "ファイルから読み込み", + "dataManager": "データ管理", + "autoSaved": "✅ 自動保存しました", + "dataLoaded": "✅ 進捗を読み込みました", + "fileSaved": "✅ ファイルに保存しました", + "fileLoaded": "✅ ファイルから読み込みました", + "noSavedData": "❌ 保存された進捗がありません", + "savedDataFound": "✅ 保存された進捗が見つかりました。続行しますか?", + "savedDate": "保存日時: {date}", + "clickLoadToContinue": "「進捗を読み込み」をクリックして続行。", + "fileError": "❌ ファイル処理エラー", + "invalidFileFormat": "❌ 無効なファイル形式", + "paintingSpeed": "描画速度", + "pixelsPerSecond": "ピクセル/秒", + "speedSetting": "速度: {speed} ピクセル/秒", + "settings": "設定", + "botSettings": "ボット設定", + "close": "閉じる", + "language": "言語", + "themeSettings": "テーマ設定", + "themeSettingsDesc": "インターフェースの好きなカラーテーマを選択。", + "languageSelectDesc": "希望言語を選択。変更は即時反映されます。", + "autoCaptcha": "自動 CAPTCHA ソルバー", + "autoCaptchaDesc": "統合ジェネレーターで Turnstile トークンを自動生成し必要に応じてブラウザ自動化にフォールバック。", + "applySettings": "設定を適用", + "settingsSaved": "✅ 設定を保存しました!", + "speedOn": "オン", + "speedOff": "オフ", + "cooldownSettings": "クールダウン設定", + "waitCharges": "チャージ数が次に達するまで待機", + "captchaSolving": "🔑 Turnstile トークン生成中...", + "captchaFailed": "❌ トークン生成失敗。フォールバックを試行...", + "automation": "自動化", + "noChargesThreshold": "⌛ チャージ {threshold} を待機中。現在 {current}。次は {time} 後...", + "tokenCapturedSuccess": "トークンキャプチャ成功!ボットを開始できます。", + "notificationsNotSupported": "このブラウザでは通知がサポートされていません。", + "chargesReadyNotification": "WPlace — チャージ準備完了", + "chargesReadyMessage": "チャージ準備完了: {current} / {max}。しきい値: {threshold}。", + "testNotificationTitle": "WPlace — テスト", + "testNotificationMessage": "これはテスト通知です。", + "showStats": "統計表示", + "compactMode": "コンパクトモード", + "refreshCharges": "チャージ更新", + "closeStats": "統計を閉じる", + "zoomOut": "縮小", + "zoomIn": "拡大", + "fitToView": "画面に合わせる", + "actualSize": "実際のサイズ (100%)", + "panMode": "パン(ドラッグで移動)", + "clearIgnoredPixels": "無視されたピクセルをすべてクリア", + "invertMask": "マスクを反転", + "waitingSetupComplete": "🔄 初期セットアップの完了を待機中...", + "waitingTokenGenerator": "🔄 トークンジェネレータの初期化を待機中...", + "uploadImageFirst": "利用可能な色を取得するために最初に画像をアップロードしてください", + "pleaseWaitInitialSetup": "🔄 進捗を読み込む前に初期セットアップの完了をお待ちください。", + "pleaseWaitFileSetup": "🔄 ファイルから読み込む前に初期セットアップの完了をお待ちください。", + "errorSavingProgress": "❌ 進捗の保存エラー", + "errorLoadingProgress": "❌ 進捗の読み込みエラー", + "fileOperationsAvailable": "📂 ファイル操作(読み込み/アップロード)が利用可能になりました!", + "tokenGeneratorReady": "🔑 トークンジェネレータ準備完了!", + "paintingStats": "描画統計", + "enablePaintingSpeedLimit": "描画速度制限を有効化(バッチサイズ制御)", + "enableNotifications": "通知を有効化", + "notifyOnChargesThreshold": "チャージがしきい値に達したら通知", + "onlyWhenNotFocused": "タブが非アクティブの時のみ", + "repeatEvery": "繰り返し間隔", + "minutesPl": "分", + "grantPermission": "許可を与える", + "test": "テスト", + "showAllColorsIncluding": "すべての色を表示(利用不可含む)", + "chromaWeight": "彩度重み", + "downloadPreview": "プレビューダウンロード", + "apply": "適用", + "cancel": "キャンセル", + "fit": "フィット", + "hundred": "100%", + "clear": "クリア", + "invert": "反転", + "reprocessingOverlay": "オーバーレイ再処理中...", + "overlayUpdated": "オーバーレイ更新完了!", + "notificationsEnabled": "通知が有効になりました。", + "notificationsPermissionDenied": "通知の許可が拒否されました。", + "overlayEnabled": "オーバーレイが有効になりました。", + "overlayDisabled": "オーバーレイが無効になりました。", + "tokenSourceSet": "トークンソースを設定: {source}", + "batchModeSet": "バッチモードを設定: {mode}", + "randomRange": "ランダム範囲", + "normalFixedSize": "通常固定サイズ", + "advancedColorSettingsReset": "高度な色設定をリセットしました。", + "shiftRowAltColumn": "Shift = 行 • Alt = 列", + "hideTurnstileBtn": "隠す", + "turnstileInstructions": "Cloudflare Turnstile — 表示された場合は確認を完了してください", + "uploadImageFirstColors": "利用可能な色を取得するために最初に画像をアップロードしてください", + "availableColors": "利用可能な色 ({count})", + "colorTooltip": "ID: {id}\nRGB: {rgb}", + "expandMode": "展開モード", + "minimize": "最小化", + "restore": "復元", + "hideStats": "統計を非表示", + "paintOptions": "描画オプション", + "paintWhitePixels": "白いピクセルを描画", + "paintTransparentPixels": "透明ピクセルを描画" +} diff --git a/lang/ko.json b/lang/ko.json new file mode 100644 index 00000000..570183be --- /dev/null +++ b/lang/ko.json @@ -0,0 +1,144 @@ +{ + "title": "WPlace 자동 이미지", + "toggleOverlay": "오버레이 전환", + "scanColors": "색상 스캔", + "uploadImage": "이미지 업로드", + "resizeImage": "크기 조정", + "selectPosition": "위치 선택", + "startPainting": "그리기 시작", + "stopPainting": "그리기 중지", + "checkingColors": "🔍 사용 가능한 색상 확인 중...", + "noColorsFound": "❌ 사이트에서 색상 팔레트를 연 후 다시 시도하세요!", + "colorsFound": "✅ 사용 가능한 색상 {count}개 발견. 업로드 준비 완료.", + "loadingImage": "🖼️ 이미지 불러오는 중...", + "imageLoaded": "✅ 이미지 로드 완료. 유효 픽셀 {count}개", + "imageError": "❌ 이미지 로드 오류", + "selectPositionAlert": "작품을 시작할 위치에 첫 픽셀을 칠하세요!", + "waitingPosition": "👆 기준 픽셀을 칠할 때까지 대기 중...", + "positionSet": "✅ 위치 설정 완료!", + "positionTimeout": "❌ 위치 선택 시간 초과", + "startPaintingMsg": "🎨 그리기 시작...", + "paintingProgress": "🧱 진행: {painted}/{total} 픽셀...", + "noCharges": "⌛ 사용 가능 횟수 없음. {time} 대기...", + "paintingStopped": "⏹️ 사용자에 의해 중지됨", + "paintingComplete": "✅ 그리기 완료! {count} 픽셀 그렸습니다.", + "paintingError": "❌ 그리는 중 오류", + "missingRequirements": "❌ 먼저 이미지를 불러오고 위치를 선택하세요", + "progress": "진행", + "pixels": "픽셀", + "charges": "횟수", + "estimatedTime": "예상 시간", + "initMessage": "'이미지 업로드'를 클릭하여 시작", + "waitingInit": "초기화 대기 중...", + "initializingToken": "🔧 Turnstile 토큰 생성기 초기화 중...", + "tokenReady": "✅ 토큰 생성 준비 완료 - 그리기를 시작할 수 있습니다!", + "tokenRetryLater": "⚠️ 필요 시 다시 시도합니다", + "resizeSuccess": "✅ 이미지가 {width}x{height} 크기로 조정됨", + "paintingPaused": "⏸️ 위치 X: {x}, Y: {y} 에서 일시 중지", + "captchaNeeded": "❗ 토큰 생성 실패. 잠시 후 다시 시도하세요.", + "saveData": "진행 저장", + "loadData": "진행 불러오기", + "saveToFile": "파일로 저장", + "loadFromFile": "파일에서 불러오기", + "dataManager": "데이터", + "autoSaved": "✅ 진행 자동 저장됨", + "dataLoaded": "✅ 진행 불러오기 성공", + "fileSaved": "✅ 파일 저장 성공", + "fileLoaded": "✅ 파일 불러오기 성공", + "noSavedData": "❌ 저장된 진행 없음", + "savedDataFound": "✅ 저장된 진행 발견! 계속하려면 불러오시겠습니까?", + "savedDate": "저장 시각: {date}", + "clickLoadToContinue": "'진행 불러오기'를 클릭하여 계속.", + "fileError": "❌ 파일 처리 오류", + "invalidFileFormat": "❌ 잘못된 파일 형식", + "paintingSpeed": "그리기 속도", + "pixelsPerSecond": "픽셀/초", + "speedSetting": "속도: {speed} 픽셀/초", + "settings": "설정", + "botSettings": "봇 설정", + "close": "닫기", + "language": "언어", + "themeSettings": "테마 설정", + "themeSettingsDesc": "인터페이스용 선호 색상 테마를 선택하세요.", + "languageSelectDesc": "선호 언어를 선택하세요. 변경 사항은 즉시 적용됩니다.", + "autoCaptcha": "자동 CAPTCHA 해결", + "autoCaptchaDesc": "통합 생성기를 사용해 Turnstile 토큰을 자동 생성하고 필요 시 브라우저 자동화로 폴백.", + "applySettings": "설정 적용", + "settingsSaved": "✅ 설정 저장 완료!", + "speedOn": "켜짐", + "speedOff": "꺼짐", + "cooldownSettings": "쿨다운 설정", + "waitCharges": "횟수가 다음 값에 도달할 때까지 대기", + "captchaSolving": "🔑 Turnstile 토큰 생성 중...", + "captchaFailed": "❌ 토큰 생성 실패. 폴백 시도...", + "automation": "자동화", + "noChargesThreshold": "⌛ 횟수가 {threshold} 에 도달할 때까지 대기 중. 현재 {current}. 다음 {time} 후...", + "tokenCapturedSuccess": "토큰 생성 성공! 이제 봇을 시작할 수 있습니다.", + "notificationsNotSupported": "이 브라우저는 알림을 지원하지 않습니다.", + "chargesReadyNotification": "WPlace — 횟수 준비 완료", + "chargesReadyMessage": "횟수 준비 완료: {current} / {max}. 임계값: {threshold}.", + "testNotificationTitle": "WPlace — 테스트", + "testNotificationMessage": "이것은 테스트 알림입니다.", + "showStats": "통계 보기", + "compactMode": "컴팩트 모드", + "refreshCharges": "횟수 새로고침", + "closeStats": "통계 닫기", + "zoomOut": "축소", + "zoomIn": "확대", + "fitToView": "화면에 맞춤", + "actualSize": "실제 크기 (100%)", + "panMode": "이동 (끌어서 보기 이동)", + "clearIgnoredPixels": "무시된 모든 픽셀 지우기", + "invertMask": "마스크 반전", + "waitingSetupComplete": "🔄 초기 설정 완료 대기 중...", + "waitingTokenGenerator": "🔄 토큰 생성기 초기화 대기 중...", + "uploadImageFirst": "사용 가능한 색상을 얻기 위해 먼저 이미지를 업로드하세요", + "pleaseWaitInitialSetup": "🔄 진행도를 로드하기 전에 초기 설정 완료를 기다리세요.", + "pleaseWaitFileSetup": "🔄 파일에서 로드하기 전에 초기 설정 완료를 기다리세요.", + "errorSavingProgress": "❌ 진행 저장 오류", + "errorLoadingProgress": "❌ 진행 로드 오류", + "fileOperationsAvailable": "📂 파일 작업(로드/업로드)이 이제 사용 가능합니다!", + "tokenGeneratorReady": "🔑 토큰 생성기 준비 완료!", + "paintingStats": "그리기 통계", + "enablePaintingSpeedLimit": "그리기 속도 제한 활성화 (배치 크기 제어)", + "enableNotifications": "알림 활성화", + "notifyOnChargesThreshold": "횟수가 임계값에 도달하면 알림", + "onlyWhenNotFocused": "탭이 포커스되지 않았을 때만", + "repeatEvery": "반복 간격", + "minutesPl": "분", + "grantPermission": "권한 부여", + "test": "테스트", + "showAllColorsIncluding": "모든 색상 보기 (사용 불가 포함)", + "chromaWeight": "색도 가중치", + "downloadPreview": "미리보기 다운로드", + "apply": "적용", + "cancel": "취소", + "fit": "맞춤", + "hundred": "100%", + "clear": "지우기", + "invert": "반전", + "reprocessingOverlay": "오버레이 재처리 중...", + "overlayUpdated": "오버레이 업데이트 완료!", + "notificationsEnabled": "알림이 활성화되었습니다.", + "notificationsPermissionDenied": "알림 권한이 거부되었습니다.", + "overlayEnabled": "오버레이가 활성화되었습니다.", + "overlayDisabled": "오버레이가 비활성화되었습니다.", + "tokenSourceSet": "토큰 소스 설정: {source}", + "batchModeSet": "배치 모드 설정: {mode}", + "randomRange": "랜덤 범위", + "normalFixedSize": "일반 고정 크기", + "advancedColorSettingsReset": "고급 색상 설정이 초기화되었습니다.", + "shiftRowAltColumn": "Shift = 행 • Alt = 열", + "hideTurnstileBtn": "숨기기", + "turnstileInstructions": "Cloudflare Turnstile — 표시되면 검사를 완료하세요", + "uploadImageFirstColors": "사용 가능한 색상을 얻기 위해 먼저 이미지를 업로드하세요", + "availableColors": "사용 가능한 색상 ({count})", + "colorTooltip": "ID: {id}\nRGB: {rgb}", + "expandMode": "확장 모드", + "minimize": "최소화", + "restore": "복원", + "hideStats": "통계 숨김", + "paintOptions": "페인트 옵션", + "paintWhitePixels": "흰색 픽셀 칠하기", + "paintTransparentPixels": "투명 픽셀 칠하기" +} diff --git a/lang/lang/es-MX.json b/lang/lang/es-MX.json new file mode 100644 index 00000000..8ca3e2b1 --- /dev/null +++ b/lang/lang/es-MX.json @@ -0,0 +1,144 @@ +{ + "title": "WPlace Auto-Imagen", + "toggleOverlay": "Alternar superposición", + "scanColors": "Escanear colores", + "uploadImage": "Subir imagen", + "resizeImage": "Cambiar tamaño de imagen", + "selectPosition": "Seleccionar posición", + "startPainting": "Iniciar pintado", + "stopPainting": "Detener pintado", + "checkingColors": "🔍 Verificando colores disponibles...", + "noColorsFound": "❌ ¡Abre la paleta de colores en el sitio e inténtalo de nuevo!", + "colorsFound": "✅ Se encontraron {count} colores disponibles. Listo para subir.", + "loadingImage": "🖼️ Cargando imagen...", + "imageLoaded": "✅ Imagen cargada con {count} píxeles válidos", + "imageError": "❌ Error al cargar la imagen", + "selectPositionAlert": "¡Pinta el primer píxel en la ubicación donde quieres que comience el arte!", + "waitingPosition": "👆 Esperando que pintes el píxel de referencia...", + "positionSet": "✅ ¡Posición establecida con éxito!", + "positionTimeout": "❌ Tiempo de espera para seleccionar la posición agotado", + "startPaintingMsg": "🎨 Iniciando pintado...", + "paintingProgress": "🧱 Progreso: {painted}/{total} píxeles...", + "noCharges": "⌛ Sin cargas. Esperando {time}...", + "paintingStopped": "⏹️ Pintado detenido por el usuario", + "paintingComplete": "✅ ¡Pintado completo! Se pintaron {count} píxeles.", + "paintingError": "❌ Error durante el pintado", + "missingRequirements": "❌ Primero carga una imagen y selecciona una posición", + "progress": "Progreso", + "pixels": "Píxeles", + "charges": "Cargas", + "estimatedTime": "Tiempo estimado", + "initMessage": "Haz clic en 'Subir imagen' para comenzar", + "waitingInit": "Esperando inicialización...", + "initializingToken": "🔧 Inicializando generador de token Turnstile...", + "tokenReady": "✅ ¡Generador de token listo! Ya puedes empezar a pintar.", + "tokenRetryLater": "⚠️ El generador de token volverá a intentarlo cuando sea necesario", + "resizeSuccess": "✅ Imagen redimensionada a {width}x{height}", + "paintingPaused": "⏸️ Pintado pausado en la posición X: {x}, Y: {y}", + "captchaNeeded": "❗ Falló la generación de token. Por favor, inténtalo de nuevo en un momento.", + "saveData": "Guardar progreso", + "loadData": "Cargar progreso", + "saveToFile": "Guardar en archivo", + "loadFromFile": "Cargar desde archivo", + "dataManager": "Gestor de datos", + "autoSaved": "✅ Progreso guardado automáticamente", + "dataLoaded": "✅ Progreso cargado con éxito", + "fileSaved": "✅ Progreso guardado en archivo con éxito", + "fileLoaded": "✅ Progreso cargado desde archivo con éxito", + "noSavedData": "❌ No se encontró progreso guardado", + "savedDataFound": "✅ ¡Se encontró progreso guardado! ¿Cargar para continuar?", + "savedDate": "Guardado el: {date}", + "clickLoadToContinue": "Haz clic en 'Cargar progreso' para continuar.", + "fileError": "❌ Error al procesar el archivo", + "invalidFileFormat": "❌ Formato de archivo inválido", + "paintingSpeed": "Velocidad de pintado", + "pixelsPerSecond": "píxeles/segundo", + "speedSetting": "Velocidad: {speed} píxeles/seg", + "settings": "Ajustes", + "botSettings": "Ajustes del bot", + "close": "Cerrar", + "language": "Idioma", + "themeSettings": "Ajustes de tema", + "themeSettingsDesc": "Elige tu tema de color preferido para la interfaz.", + "languageSelectDesc": "Selecciona tu idioma preferido. Los cambios se aplicarán inmediatamente.", + "autoCaptcha": "Solucionador automático de CAPTCHA (Turnstile)", + "autoCaptchaDesc": "Genera automáticamente tokens de Turnstile usando el generador integrado. Utiliza la automatización del navegador como alternativa si es necesario.", + "applySettings": "Aplicar ajustes", + "settingsSaved": "✅ ¡Ajustes guardados con éxito!", + "speedOn": "Activado", + "speedOff": "Desactivado", + "cooldownSettings": "Ajustes de tiempo de espera", + "waitCharges": "Esperar hasta que las cargas lleguen a", + "captchaSolving": "🔑 Generando token de Turnstile...", + "captchaFailed": "❌ Falló la generación de token de Turnstile. Probando método alternativo...", + "automation": "Automatización", + "noChargesThreshold": "⌛ Esperando que las cargas lleguen a {threshold}. Actual: {current}. Siguiente en {time}...", + "tokenCapturedSuccess": "¡Token capturado con éxito! Ya puedes iniciar el bot.", + "notificationsNotSupported": "Las notificaciones no son compatibles con este navegador.", + "chargesReadyNotification": "WPlace — Cargas listas", + "chargesReadyMessage": "Cargas listas: {current} / {max}. Umbral: {threshold}.", + "testNotificationTitle": "WPlace — Prueba", + "testNotificationMessage": "Esta es una notificación de prueba.", + "showStats": "Mostrar estadísticas", + "compactMode": "Modo compacto", + "refreshCharges": "Actualizar cargas", + "closeStats": "Cerrar estadísticas", + "zoomOut": "Alejar", + "zoomIn": "Acercar", + "fitToView": "Ajustar a la vista", + "actualSize": "Tamaño real (100%)", + "panMode": "Desplazamiento (arrastra para mover la vista)", + "clearIgnoredPixels": "Limpiar todos los píxeles ignorados", + "invertMask": "Invertir máscara", + "waitingSetupComplete": "🔄 Esperando que se complete la configuración inicial...", + "waitingTokenGenerator": "🔄 Esperando que se inicialice el generador de tokens...", + "uploadImageFirst": "Sube una imagen primero para capturar los colores disponibles", + "pleaseWaitInitialSetup": "🔄 Por favor, espera a que se complete la configuración inicial antes de cargar el progreso.", + "pleaseWaitFileSetup": "🔄 Por favor, espera a que se complete la configuración inicial antes de cargar desde un archivo.", + "errorSavingProgress": "❌ Error al guardar el progreso", + "errorLoadingProgress": "❌ Error al cargar el progreso", + "fileOperationsAvailable": "📂 ¡Las operaciones de archivo (Cargar/Subir) ya están disponibles!", + "tokenGeneratorReady": "🔑 ¡Generador de tokens listo!", + "paintingStats": "Estadísticas de pintado", + "enablePaintingSpeedLimit": "Activar límite de velocidad de pintado (control de lotes)", + "enableNotifications": "Activar notificaciones", + "notifyOnChargesThreshold": "Notificar cuando las cargas alcancen el umbral", + "onlyWhenNotFocused": "Solo cuando la pestaña no esté en foco", + "repeatEvery": "Repetir cada", + "minutesPl": "minuto(s)", + "grantPermission": "Otorgar permiso", + "test": "Probar", + "showAllColorsIncluding": "Mostrar todos los colores (incluidos los no disponibles)", + "chromaWeight": "Peso de croma", + "downloadPreview": "Descargar vista previa", + "apply": "Aplicar", + "cancel": "Cancelar", + "fit": "Ajustar", + "hundred": "100%", + "clear": "Limpiar", + "invert": "Invertir", + "reprocessingOverlay": "Reprocesando superposición...", + "overlayUpdated": "¡Superposición actualizada!", + "notificationsEnabled": "Notificaciones activadas.", + "notificationsPermissionDenied": "Permiso de notificaciones denegado.", + "overlayEnabled": "Superposición activada.", + "overlayDisabled": "Superposición desactivada.", + "tokenSourceSet": "Fuente de token establecida en: {source}", + "batchModeSet": "Modo por lotes establecido en: {mode}", + "randomRange": "Rango aleatorio", + "normalFixedSize": "Tamaño fijo normal", + "advancedColorSettingsReset": "Ajustes de color avanzados restablecidos.", + "shiftRowAltColumn": "Shift = Fila • Alt = Columna", + "hideTurnstileBtn": "Ocultar", + "turnstileInstructions": "Cloudflare Turnstile — por favor, completa la verificación si se muestra", + "uploadImageFirstColors": "Por favor, sube una imagen primero para capturar los colores disponibles", + "availableColors": "Colores disponibles ({count})", + "colorTooltip": "ID: {id}\nRGB: {rgb}", + "expandMode": "Modo expandido", + "minimize": "Minimizar", + "restore": "Restaurar", + "hideStats": "Ocultar estadísticas", + "paintOptions": "Opciones de pintado", + "paintWhitePixels": "Pintar píxeles blancos", + "paintTransparentPixels": "Pintar píxeles transparentes" +} diff --git a/lang/pt.json b/lang/pt.json new file mode 100644 index 00000000..ebbc4f6e --- /dev/null +++ b/lang/pt.json @@ -0,0 +1,143 @@ +{ + "title": "WPlace Auto-Image", + "toggleOverlay": "Toggle Overlay", + "scanColors": "Escanear Cores", + "uploadImage": "Upload da Imagem", + "resizeImage": "Redimensionar Imagem", + "selectPosition": "Selecionar Posição", + "startPainting": "Iniciar Pintura", + "stopPainting": "Parar Pintura", + "checkingColors": "🔍 Verificando cores disponíveis...", + "noColorsFound": "❌ Abra a paleta de cores no site e tente novamente!", + "colorsFound": "✅ {count} cores encontradas. Pronto para upload.", + "loadingImage": "🖼️ Carregando imagem...", + "imageLoaded": "✅ Imagem carregada com {count} pixels válidos", + "imageError": "❌ Erro ao carregar imagem", + "selectPositionAlert": "Pinte o primeiro pixel на localização onde deseja que a arte comece!", + "waitingPosition": "👆 Aguardando você pintar o pixel de referência...", + "positionSet": "✅ Posição definida com sucesso!", + "positionTimeout": "❌ Tempo esgotado para selecionar posição", + "startPaintingMsg": "🎨 Iniciando pintura...", + "paintingProgress": "🧱 Progresso: {painted}/{total} pixels...", + "noCharges": "⌛ Sem cargas. Aguardando {time}...", + "paintingStopped": "⏹️ Pintura interromпида pelo usuário", + "paintingComplete": "✅ Pintura concluída! {count} pixels pintados.", + "paintingError": "❌ Erro durante a pintura", + "missingRequirements": "❌ Carregue uma imagem e selecione uma posição primeiro", + "progress": "Progresso", + "pixels": "Pixels", + "charges": "Cargas", + "estimatedTime": "Tempo estimado", + "initMessage": "Clique em 'Upload da Imagem' para começar", + "waitingInit": "Aguardando inicialização...", + "initializingToken": "🔧 Inicializando gerador de tokens Turnstile...", + "tokenReady": "✅ Gerador de tokens pronto - você pode começar a pintar!", + "tokenRetryLater": "⚠️ Gerador de tokens tentará novamente quando necessário", + "resizeSuccess": "✅ Imagem redimensionada для {width}x{height}", + "paintingPaused": "⏸️ Pintura pausada na posição X: {x}, Y: {y}", + "captchaNeeded": "❗ Falha na geração de token. Tente novamente em alguns instantes.", + "saveData": "Salvar Progresso", + "loadData": "Carregar Progresso", + "saveToFile": "Salvar em Arquivo", + "loadFromFile": "Carregar de Arquivo", + "dataManager": "Dados", + "autoSaved": "✅ Progresso salvo automaticamente", + "dataLoaded": "✅ Progresso carregado com sucesso", + "fileSaved": "✅ Salvo em arquivo com sucesso", + "fileLoaded": "✅ Carregado de arquivo com sucesso", + "noSavedData": "❌ Nenhum progresso salvo encontrado", + "savedDataFound": "✅ Progresso salvo encontrado! Carregar para continuar?", + "savedDate": "Salvo em: {date}", + "clickLoadToContinue": "Clique em 'Carregar Progresso' para continuar.", + "fileError": "❌ Erro ao processar arquivo", + "invalidFileFormat": "❌ Formato de arquivo inválido", + "paintingSpeed": "Velocidade de Pintura", + "pixelsPerSecond": "pixels/segundo", + "speedSetting": "Velocidade: {speed} pixels/seg", + "settings": "Configurações", + "botSettings": "Configurações do Bot", + "close": "Fechar", + "language": "Idioma", + "themeSettings": "Configurações de Tema", + "themeSettingsDesc": "Escolha seu tema de cores preferido para a interface.", + "languageSelectDesc": "Selecione seu idioma preferido. As alterações terão efeito imediatamente.", + "autoCaptcha": "Resolvedor de CAPTCHA Automático", + "autoCaptchaDesc": "Tenta resolver o CAPTCHA automaticamente simulando a colocação manual de um pixel quando o token expira.", + "applySettings": "Aplicar Configurações", + "settingsSaved": "✅ Configurações salvas com sucesso!", + "speedOn": "Ligado", + "speedOff": "Desligado", + "cooldownSettings": "Configurações de Cooldown", + "waitCharges": "Aguardar até as cargas atingirem", + "captchaSolving": "🤖 Tentando resolver o CAPTCHA...", + "captchaFailed": "❌ Falha ao resolver CAPTCHA. Pinte um pixel manualmente.", + "automation": "Automação", "noChargesThreshold": "⌛ Aguardando cargas atingirem {threshold}. Atual: {current}. Próxima em {time}...", + "tokenCapturedSuccess": "Token capturado com sucesso! Você pode iniciar o bot agora.", + "notificationsNotSupported": "Notificações não são suportadas neste navegador.", + "chargesReadyNotification": "WPlace — Cargas Prontas", + "chargesReadyMessage": "Cargas prontas: {current} / {max}. Limite: {threshold}.", + "testNotificationTitle": "WPlace — Teste", + "testNotificationMessage": "Esta é uma notificação de teste.", + "showStats": "Mostrar Estatísticas", + "compactMode": "Modo Compacto", + "refreshCharges": "Atualizar Cargas", + "closeStats": "Fechar Estatísticas", + "zoomOut": "Diminuir Zoom", + "zoomIn": "Aumentar Zoom", + "fitToView": "Ajustar à visualização", + "actualSize": "Tamanho real (100%)", + "panMode": "Arrastar (arrastar para mover visualização)", + "clearIgnoredPixels": "Limpar pixels ignorados", + "invertMask": "Inverter máscara", + "waitingSetupComplete": "🔄 Aguardando conclusão da configuração inicial...", + "waitingTokenGenerator": "🔄 Aguardando inicialização do gerador de tokens...", + "uploadImageFirst": "Faça upload de uma imagem primeiro para capturar cores disponíveis", + "pleaseWaitInitialSetup": "🔄 Aguarde a conclusão da configuração inicial antes de carregar o progresso.", + "pleaseWaitFileSetup": "🔄 Aguarde a conclusão da configuração inicial antes de carregar do arquivo.", + "errorSavingProgress": "❌ Erro ao salvar progresso", + "errorLoadingProgress": "❌ Erro ao carregar progresso", + "fileOperationsAvailable": "📂 Operações de arquivo (Carregar/Upload) agora disponíveis!", + "tokenGeneratorReady": "🔑 Gerador de tokens pronto!", + "paintingStats": "Estatísticas de Pintura", + "enablePaintingSpeedLimit": "Ativar limite de velocidade de pintura (controle de tamanho de lote)", + "enableNotifications": "Ativar notificações", + "notifyOnChargesThreshold": "Notificar quando as cargas atingirem o limite", + "onlyWhenNotFocused": "Apenas quando a aba não está em foco", + "repeatEvery": "Repetir a cada", + "minutesPl": "minuto(s)", + "grantPermission": "Conceder Permissão", + "test": "Teste", + "showAllColorsIncluding": "Mostrar Todas as Cores (incluindo indisponíveis)", + "chromaWeight": "Peso da Saturação", + "downloadPreview": "Baixar Pré-visualização", + "apply": "Aplicar", + "cancel": "Cancelar", + "fit": "Ajustar", + "hundred": "100%", + "clear": "Limpar", + "invert": "Inverter", + "reprocessingOverlay": "Reprocessando sobreposição...", + "overlayUpdated": "Sobreposição atualizada!", + "notificationsEnabled": "Notificações ativadas.", + "notificationsPermissionDenied": "Permissão de notificações negada.", + "overlayEnabled": "Sobreposição ativada.", + "overlayDisabled": "Sobreposição desativada.", + "tokenSourceSet": "Fonte de token definida para: {source}", + "batchModeSet": "Modo de lote definido para: {mode}", + "randomRange": "Intervalo Aleatório", + "normalFixedSize": "Tamanho Fixo Normal", + "advancedColorSettingsReset": "Configurações avançadas de cor redefinidas.", + "shiftRowAltColumn": "Shift = Linha • Alt = Coluna", + "hideTurnstileBtn": "Ocultar", + "turnstileInstructions": "Cloudflare Turnstile — complete a verificação se mostrada", + "uploadImageFirstColors": "Faça upload de uma imagem primeiro para capturar cores disponíveis", + "availableColors": "Cores Disponíveis ({count})", + "colorTooltip": "ID: {id}\nRGB: {rgb}", + "expandMode": "Modo Expandir", + "minimize": "Minimizar", + "restore": "Restaurar", + "hideStats": "Ocultar Estatísticas", + "paintOptions": "Opções de pintura", + "paintWhitePixels": "Pintar pixels brancos", + "paintTransparentPixels": "Pintar pixels transparentes" +} \ No newline at end of file diff --git a/lang/ru.json b/lang/ru.json new file mode 100644 index 00000000..1bb8b162 --- /dev/null +++ b/lang/ru.json @@ -0,0 +1,143 @@ +{ + "title": "WPlace Авто-Изображение", + "toggleOverlay": "Toggle Overlay", + "scanColors": "Сканировать цвета", + "uploadImage": "Загрузить изображение", + "resizeImage": "Изменить размер изображения", + "selectPosition": "Выбрать позицию", + "startPainting": "Начать рисование", + "stopPainting": "Остановить рисование", + "checkingColors": "🔍 Проверка доступных цветов...", + "noColorsFound": "❌ Откройте палитру цветов на сайте и попробуйте снова!", + "colorsFound": "✅ Найдено доступных цветов: {count}. Готово к загрузке.", + "loadingImage": "🖼️ Загрузка изображения...", + "imageLoaded": "✅ Изображение загружено, валидных пикселей: {count}", + "imageError": "❌ Ошибка при загрузке изображения", + "selectPositionAlert": "Нарисуйте первый пиксель в месте, откуда начнётся рисунок!", + "waitingPosition": "👆 Ожидание, пока вы нарисуете опорный пиксель...", + "positionSet": "✅ Позиция успешно установлена!", + "positionTimeout": "❌ Время ожидания выбора позиции истекло", + "startPaintingMsg": "🎨 Начинаем рисование...", + "paintingProgress": "🧱 Прогресс: {painted}/{total} пикселей...", + "noCharges": "⌛ Нет зарядов. Ожидание {time}...", + "paintingStopped": "⏹️ Рисование остановлено пользователем", + "paintingComplete": "✅ Рисование завершено! Нарисовано пикселей: {count}.", + "paintingError": "❌ Ошибка во время рисования", + "missingRequirements": "❌ Сначала загрузите изображение и выберите позицию", + "progress": "Прогресс", + "pixels": "Пиксели", + "charges": "Заряды", + "estimatedTime": "Примерное время", + "initMessage": "Нажмите 'Загрузить изображение', чтобы начать", + "waitingInit": "Ожидание инициализации...", + "initializingToken": "🔧 Инициализация генератора Turnstile токенов...", + "tokenReady": "✅ Генератор токенов готов - можете начинать рисование!", + "tokenRetryLater": "⚠️ Генератор токенов повторит попытку при необходимости", + "resizeSuccess": "✅ Изображение изменено до {width}x{height}", + "paintingPaused": "⏸️ Рисование приостановлено на позиции X: {x}, Y: {y}", + "captchaNeeded": "❗ Генерация токена не удалась. Пожалуйста, попробуйте через некоторое время.", + "saveData": "Сохранить прогресс", + "loadData": "Загрузить прогресс", + "saveToFile": "Сохранить в файл", + "loadFromFile": "Загрузить из файла", + "dataManager": "Менеджер данных", + "autoSaved": "✅ Прогресс сохранён автоматически", + "dataLoaded": "✅ Прогресс успешно загружен", + "fileSaved": "✅ Прогресс успешно сохранён в файл", + "fileLoaded": "✅ Прогресс успешно загружен из файла", + "noSavedData": "❌ Сохранённый прогресс не найден", + "savedDataFound": "✅ Найден сохранённый прогресс! Загрузить, чтобы продолжить?", + "savedDate": "Сохранено: {date}", + "clickLoadToContinue": "Нажмите 'Загрузить прогресс', чтобы продолжить.", + "fileError": "❌ Ошибка при обработке файла", + "invalidFileFormat": "❌ Неверный формат файла", + "paintingSpeed": "Скорость рисования", + "pixelsPerSecond": "пикселей/сек", + "speedSetting": "Скорость: {speed} пикс./сек", + "settings": "Настройки", + "botSettings": "Настройки бота", + "close": "Закрыть", + "language": "Язык", + "themeSettings": "Настройки темы", + "themeSettingsDesc": "Выберите предпочтительную цветовую тему интерфейса.", + "languageSelectDesc": "Выберите предпочтительный язык. Изменения вступят в силу немедленно.", + "autoCaptcha": "Авто-решение CAPTCHA (Turnstile)", + "autoCaptchaDesc": "Автоматически генерирует Turnstile токены используя встроенный генератор. Возвращается к автоматизации браузера при необходимости.", + "applySettings": "Применить настройки", + "settingsSaved": "✅ Настройки успешно сохранены!", + "speedOn": "Вкл", + "speedOff": "Выкл", + "cooldownSettings": "Настройки перезарядки", + "waitCharges": "Ждать до накопления зарядов", + "captchaSolving": "🔑 Генерирую Turnstile токен...", + "captchaFailed": "❌ Не удалось сгенерировать Turnstile токен. Пробую резервный метод...", + "automation": "Автоматизация", "noChargesThreshold": "⌛ Ожидание зарядов до {threshold}. Сейчас {current}. Следующий через {time}...", + "tokenCapturedSuccess": "Токен успешно захвачен! Теперь можно запускать бота.", + "notificationsNotSupported": "Уведомления не поддерживаются в этом браузере.", + "chargesReadyNotification": "WPlace — Заряды готовы", + "chargesReadyMessage": "Заряды готовы: {current} / {max}. Порог: {threshold}.", + "testNotificationTitle": "WPlace — Тест", + "testNotificationMessage": "Это тестовое уведомление.", + "showStats": "Показать статистику", + "compactMode": "Компактный режим", + "refreshCharges": "Обновить заряды", + "closeStats": "Закрыть статистику", + "zoomOut": "Уменьшить", + "zoomIn": "Увеличить", + "fitToView": "По размеру экрана", + "actualSize": "Реальный размер (100%)", + "panMode": "Перемещение (перетаскивание для движения)", + "clearIgnoredPixels": "Очистить игнорируемые пиксели", + "invertMask": "Инвертировать маску", + "waitingSetupComplete": "🔄 Ждём завершения начальной настройки...", + "waitingTokenGenerator": "🔄 Ждём инициализации генератора токенов...", + "uploadImageFirst": "Сначала загрузите изображение для захвата доступных цветов", + "pleaseWaitInitialSetup": "🔄 Пожалуйста, подождите завершения начальной настройки перед загрузкой прогресса.", + "pleaseWaitFileSetup": "🔄 Пожалуйста, подождите завершения начальной настройки перед загрузкой из файла.", + "errorSavingProgress": "❌ Ошибка сохранения прогресса", + "errorLoadingProgress": "❌ Ошибка загрузки прогресса", + "fileOperationsAvailable": "📂 Файловые операции (Загрузка/Выгрузка) теперь доступны!", + "tokenGeneratorReady": "🔑 Генератор токенов готов!", + "paintingStats": "Статистика рисования", + "enablePaintingSpeedLimit": "Включить ограничение скорости рисования (контроль размера пакета)", + "enableNotifications": "Включить уведомления", + "notifyOnChargesThreshold": "Уведомлять при достижении порога зарядов", + "onlyWhenNotFocused": "Только когда вкладка не в фокусе", + "repeatEvery": "Повторять каждые", + "minutesPl": "минут(а/ы)", + "grantPermission": "Дать разрешение", + "test": "Тест", + "showAllColorsIncluding": "Показать все цвета (включая недоступные)", + "chromaWeight": "Вес цветности", + "downloadPreview": "Скачать превью", + "apply": "Применить", + "cancel": "Отменить", + "fit": "По размеру", + "hundred": "100%", + "clear": "Очистить", + "invert": "Инвертировать", + "reprocessingOverlay": "Переобработка оверлея...", + "overlayUpdated": "Оверлей обновлён!", + "notificationsEnabled": "Уведомления включены.", + "notificationsPermissionDenied": "В разрешении на уведомления отказано.", + "overlayEnabled": "Оверлей включён.", + "overlayDisabled": "Оверлей отключён.", + "tokenSourceSet": "Источник токенов установлен: {source}", + "batchModeSet": "Режим пакета установлен: {mode}", + "randomRange": "Случайный диапазон", + "normalFixedSize": "Нормальный фиксированный размер", + "advancedColorSettingsReset": "Продвинутые настройки цвета сброшены.", + "shiftRowAltColumn": "Shift = Строка • Alt = Столбец", + "hideTurnstileBtn": "Скрыть", + "turnstileInstructions": "Cloudflare Turnstile — пожалуйста, завершите проверку, если она показана", + "uploadImageFirstColors": "Пожалуйста, сначала загрузите изображение для захвата доступных цветов", + "availableColors": "Доступные цвета ({count})", + "colorTooltip": "ID: {id}\nRGB: {rgb}", + "expandMode": "Режим расширения", + "minimize": "Свернуть", + "restore": "Восстановить", + "hideStats": "Скрыть статистику", + "paintOptions": "Параметры рисования", + "paintWhitePixels": "Рисовать белые пиксели", + "paintTransparentPixels": "Рисовать прозрачные пиксели" +} \ No newline at end of file diff --git a/lang/tr.json b/lang/tr.json new file mode 100644 index 00000000..3a12640e --- /dev/null +++ b/lang/tr.json @@ -0,0 +1,144 @@ +{ + "title": "WPlace Otomatik-Resim", + "toggleOverlay": "Katmanı Aç/Kapat", + "scanColors": "Renkleri Tara", + "uploadImage": "Resim Yükle", + "resizeImage": "Resmi Yeniden Boyutlandır", + "selectPosition": "Konum Seç", + "startPainting": "Boyamayı Başlat", + "stopPainting": "Boyamayı Durdur", + "checkingColors": "🔍 Uygun renkler kontrol ediliyor...", + "noColorsFound": "❌ Sitede renk paletini açın ve tekrar deneyin!", + "colorsFound": "✅ {count} uygun renk bulundu. Yüklemeye hazır.", + "loadingImage": "🖼️ Resim yükleniyor...", + "imageLoaded": "✅ Resim {count} geçerli piksel ile yüklendi", + "imageError": "❌ Resim yüklenirken hata oluştu", + "selectPositionAlert": "Sanatı başlatmak istediğiniz ilk pikseli boyayın!", + "waitingPosition": "👆 Referans pikseli boyamanız bekleniyor...", + "positionSet": "✅ Konum başarıyla ayarlandı!", + "positionTimeout": "❌ Konum seçme süresi doldu", + "startPaintingMsg": "🎨 Boyama başlatılıyor...", + "paintingProgress": "🧱 İlerleme: {painted}/{total} piksel...", + "noCharges": "⌛ Yeterli hak yok. Bekleniyor {time}...", + "paintingStopped": "⏹️ Boyama kullanıcı tarafından durduruldu", + "paintingComplete": "✅ Boyama tamamlandı! {count} piksel boyandı.", + "paintingError": "❌ Boyama sırasında hata oluştu", + "missingRequirements": "❌ Önce resim yükleyip konum seçmelisiniz", + "progress": "İlerleme", + "pixels": "Pikseller", + "charges": "Haklar", + "estimatedTime": "Tahmini süre", + "initMessage": "Başlamak için 'Resim Yükle'ye tıklayın", + "waitingInit": "Başlatma bekleniyor...", + "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", + "resizeSuccess": "✅ Resim {width}x{height} boyutuna yeniden boyutlandırıldı", + "paintingPaused": "⏸️ Boyama duraklatıldı, Konum X: {x}, Y: {y}", + "captchaNeeded": "❗ CAPTCHA gerekli. Devam etmek için bir pikseli manuel olarak boyayın.", + "saveData": "İlerlemeyi Kaydet", + "loadData": "İlerlemeyi Yükle", + "saveToFile": "Dosyaya Kaydet", + "loadFromFile": "Dosyadan Yükle", + "dataManager": "Veri Yöneticisi", + "autoSaved": "✅ İlerleme otomatik olarak kaydedildi", + "dataLoaded": "✅ İlerleme başarıyla yüklendi", + "fileSaved": "✅ İlerleme dosyaya başarıyla kaydedildi", + "fileLoaded": "✅ İlerleme dosyadan başarıyla yüklendi", + "noSavedData": "❌ Kayıtlı ilerleme bulunamadı", + "savedDataFound": "✅ Kayıtlı ilerleme bulundu! Devam etmek için yükleyin.", + "savedDate": "Kaydedilme tarihi: {date}", + "clickLoadToContinue": "Devam etmek için 'İlerlemeyi Yükle'ye tıklayın.", + "fileError": "❌ Dosya işlenirken hata oluştu", + "invalidFileFormat": "❌ Geçersiz dosya formatı", + "paintingSpeed": "Boyama Hızı", + "pixelsPerSecond": "piksel/saniye", + "speedSetting": "Hız: {speed} piksel/sn", + "settings": "Ayarlar", + "botSettings": "Bot Ayarları", + "close": "Kapat", + "language": "Dil", + "themeSettings": "Tema Ayarları", + "themeSettingsDesc": "Arayüz için tercih ettiğiniz renk temasını seçin.", + "languageSelectDesc": "Tercih ettiğiniz dili seçin. Değişiklikler hemen uygulanacaktır.", + "autoCaptcha": "Oto-CAPTCHA Çözücü", + "autoCaptchaDesc": "CAPTCHA süresi dolduğunda manuel piksel yerleştirmeyi taklit ederek otomatik çözmeyi dener.", + "applySettings": "Ayarları Uygula", + "settingsSaved": "✅ Ayarlar başarıyla kaydedildi!", + "speedOn": "Açık", + "speedOff": "Kapalı", + "cooldownSettings": "Bekleme Süresi Ayarları", + "waitCharges": "Haklar şu seviyeye ulaşana kadar bekle", + "captchaSolving": "🤖 CAPTCHA çözülmeye çalışılıyor...", + "captchaFailed": "❌ Oto-CAPTCHA başarısız oldu. Bir pikseli manuel boyayın.", + "automation": "Otomasyon", + "noChargesThreshold": "⌛ Hakların {threshold} seviyesine ulaşması bekleniyor. Şu anda {current}. Sonraki {time} içinde...", + "tokenCapturedSuccess": "Token başarıyla yakalandı! Şimdi botu başlatabilirsiniz.", + "notificationsNotSupported": "Bu tarayıcıda bildirimler desteklenmiyor.", + "chargesReadyNotification": "WPlace — Haklar Hazır", + "chargesReadyMessage": "Haklar hazır: {current} / {max}. Eşik: {threshold}.", + "testNotificationTitle": "WPlace — Test", + "testNotificationMessage": "Bu bir test bildirimidir.", + "showStats": "İstatistikleri Göster", + "compactMode": "Kompakt Mod", + "refreshCharges": "Hakları Yenile", + "closeStats": "İstatistikleri Kapat", + "zoomOut": "Uzaklaştır", + "zoomIn": "Yakınlaştır", + "fitToView": "Görünüme sığdır", + "actualSize": "Gerçek boyut (100%)", + "panMode": "Kaydır (görünümü hareket ettirmek için sürükle)", + "clearIgnoredPixels": "Görmezden gelinen tüm pikselleri temizle", + "invertMask": "Maskeyi ters çevir", + "waitingSetupComplete": "🔄 İlk kurulumun tamamlanması bekleniyor...", + "waitingTokenGenerator": "🔄 Token üretecinin başlatılması bekleniyor...", + "uploadImageFirst": "Uygun renkleri yakalamak için önce bir resim yükleyin", + "pleaseWaitInitialSetup": "🔄 İlerlemeyi yüklemeden önce lütfen ilk kurulumun tamamlanmasını bekleyin.", + "pleaseWaitFileSetup": "🔄 Dosyadan yüklemeden önce lütfen ilk kurulumun tamamlanmasını bekleyin.", + "errorSavingProgress": "❌ İlerlemeyi kaydetme hatası", + "errorLoadingProgress": "❌ İlerlemeyi yükleme hatası", + "fileOperationsAvailable": "📂 Dosya işlemleri (Yükle/Karşıya Yükle) artık mevcut!", + "tokenGeneratorReady": "🔑 Token üreteci hazır!", + "paintingStats": "Boyama İstatistikleri", + "enablePaintingSpeedLimit": "Boyama hız limitini etkinleştir (batch boyut kontrolü)", + "enableNotifications": "Bildirimleri etkinleştir", + "notifyOnChargesThreshold": "Haklar eşiğe ulaştığında bildir", + "onlyWhenNotFocused": "Sadece sekme odaklanmadığında", + "repeatEvery": "Her tekrarla", + "minutesPl": "dakika", + "grantPermission": "İzin Ver", + "test": "Test", + "showAllColorsIncluding": "Tüm Renkleri Göster (kullanılamayanlar dahil)", + "chromaWeight": "Renk Doygunluğu Ağırlığı", + "downloadPreview": "Önizlemeyi İndir", + "apply": "Uygula", + "cancel": "İptal", + "fit": "Sığdır", + "hundred": "100%", + "clear": "Temizle", + "invert": "Ters çevir", + "reprocessingOverlay": "Katman yeniden işleniyor...", + "overlayUpdated": "Katman güncellendi!", + "notificationsEnabled": "Bildirimler etkinleştirildi.", + "notificationsPermissionDenied": "Bildirim izni reddedildi.", + "overlayEnabled": "Katman etkinleştirildi.", + "overlayDisabled": "Katman devre dışı bırakıldı.", + "tokenSourceSet": "Token kaynağı şuna ayarlandı: {source}", + "batchModeSet": "Batch modu şuna ayarlandı: {mode}", + "randomRange": "Rastgele Aralık", + "normalFixedSize": "Normal Sabit Boyut", + "advancedColorSettingsReset": "Gelişmiş renk ayarları sıfırlandı.", + "shiftRowAltColumn": "Shift = Satır • Alt = Sütun", + "hideTurnstileBtn": "Gizle", + "turnstileInstructions": "Cloudflare Turnstile — gösterilirse lütfen kontrolü tamamlayın", + "uploadImageFirstColors": "Uygun renkleri yakalamak için lütfen önce bir resim yükleyin", + "availableColors": "Uygun Renkler ({count})", + "colorTooltip": "ID: {id}\nRGB: {rgb}", + "expandMode": "Genişletme Modu", + "minimize": "Küçült", + "restore": "Geri Yükle", + "hideStats": "İstatistikleri Gizle", + "paintOptions": "Boya Seçenekleri", + "paintWhitePixels": "Beyaz pikselleri boya", + "paintTransparentPixels": "Şeffaf pikselleri boya" +} diff --git a/lang/uk.json b/lang/uk.json new file mode 100644 index 00000000..34171c58 --- /dev/null +++ b/lang/uk.json @@ -0,0 +1,144 @@ +{ + "title": "WPlace Авто-Зображення", + "toggleOverlay": "Перемкнути оверлей", + "scanColors": "Сканувати кольори", + "uploadImage": "Завантажити зображення", + "resizeImage": "Змінити розмір зображення", + "selectPosition": "Вибрати позицію", + "startPainting": "Почати малювання", + "stopPainting": "Зупинити малювання", + "checkingColors": "🔍 Перевірка доступних кольорів...", + "noColorsFound": "❌ Відкрий палітру кольорів на сайті та спробуй ще раз!", + "colorsFound": "✅ Знайдено {count} доступних кольорів. Готово до завантаження.", + "loadingImage": "🖼️ Завантаження зображення...", + "imageLoaded": "✅ Зображення завантажено. Валідних пікселів: {count}", + "imageError": "❌ Помилка завантаження зображення", + "selectPositionAlert": "Намалюй перший піксель у місці, де має починатися арт!", + "waitingPosition": "👆 Очікування на малювання референсного пікселя...", + "positionSet": "✅ Позицію успішно встановлено!", + "positionTimeout": "❌ Час вибору позиції вичерпано", + "startPaintingMsg": "🎨 Початок малювання...", + "paintingProgress": "🧱 Прогрес: {painted}/{total} пікселів...", + "noCharges": "⌛ Немає зарядів. Очікування {time}...", + "paintingStopped": "⏹️ Малювання зупинено користувачем", + "paintingComplete": "✅ Малювання завершено! Намальовано {count} пікселів.", + "paintingError": "❌ Помилка під час малювання", + "missingRequirements": "❌ Спершу завантаж зображення та вибери позицію", + "progress": "Прогрес", + "pixels": "Пікселі", + "charges": "Заряди", + "estimatedTime": "Орієнтовний час", + "initMessage": "Натисни 'Завантажити зображення', щоб почати", + "waitingInit": "Очікування ініціалізації...", + "initializingToken": "🔧 Ініціалізація генератора токенів Turnstile...", + "tokenReady": "✅ Генератор токенів готовий – можна починати малювання!", + "tokenRetryLater": "⚠️ Генератор токенів повторить спробу за потреби", + "resizeSuccess": "✅ Зображення змінено до {width}x{height}", + "paintingPaused": "⏸️ Малювання призупинено на позиції X: {x}, Y: {y}", + "captchaNeeded": "❗ Не вдалося згенерувати токен. Спробуй трохи пізніше.", + "saveData": "Зберегти прогрес", + "loadData": "Завантажити прогрес", + "saveToFile": "Зберегти у файл", + "loadFromFile": "Завантажити з файлу", + "dataManager": "Менеджер даних", + "autoSaved": "✅ Прогрес збережено автоматично", + "dataLoaded": "✅ Прогрес успішно завантажено", + "fileSaved": "✅ Прогрес успішно збережено у файл", + "fileLoaded": "✅ Прогрес успішно завантажено з файлу", + "noSavedData": "❌ Не знайдено збереженого прогресу", + "savedDataFound": "✅ Знайдено збережений прогрес! Завантажити, щоб продовжити?", + "savedDate": "Збережено: {date}", + "clickLoadToContinue": "Натисни 'Завантажити прогрес', щоб продовжити.", + "fileError": "❌ Помилка обробки файлу", + "invalidFileFormat": "❌ Невірний формат файлу", + "paintingSpeed": "Швидкість малювання", + "pixelsPerSecond": "пікселів/секунда", + "speedSetting": "Швидкість: {speed} пікселів/сек", + "settings": "Налаштування", + "botSettings": "Налаштування бота", + "close": "Закрити", + "language": "Мова", + "themeSettings": "Налаштування теми", + "themeSettingsDesc": "Вибери бажану колірну тему для інтерфейсу.", + "languageSelectDesc": "Вибери бажану мову. Зміни набудуть чинності одразу.", + "autoCaptcha": "Авто-CAPTCHA (Turnstile)", + "autoCaptchaDesc": "Автоматично генерує токени Turnstile за допомогою вбудованого генератора. Використовує автоматизацію браузера у разі потреби.", + "applySettings": "Застосувати налаштування", + "settingsSaved": "✅ Налаштування успішно збережено!", + "speedOn": "Увімк", + "speedOff": "Вимк", + "cooldownSettings": "Налаштування відновлення", + "waitCharges": "Очікувати, доки кількість зарядів досягне", + "captchaSolving": "🔑 Генерація токена Turnstile...", + "captchaFailed": "❌ Не вдалося згенерувати токен Turnstile. Використовую запасний метод...", + "automation": "Автоматизація", + "noChargesThreshold": "⌛ Очікування, доки заряди досягнуть {threshold}. Зараз {current}. Наступне через {time}...", + "tokenCapturedSuccess": "Токен успішно захоплено! Можеш запускати бота.", + "notificationsNotSupported": "Сповіщення не підтримуються в цьому браузері.", + "chargesReadyNotification": "WPlace — Заряди готові", + "chargesReadyMessage": "Заряди готові: {current} / {max}. Поріг: {threshold}.", + "testNotificationTitle": "WPlace — Тест", + "testNotificationMessage": "Це тестове сповіщення.", + "showStats": "Показати статистику", + "compactMode": "Компактний режим", + "refreshCharges": "Оновити заряди", + "closeStats": "Закрити статистику", + "zoomOut": "Зменшити", + "zoomIn": "Збільшити", + "fitToView": "Підігнати під вікно", + "actualSize": "Реальний розмір (100%)", + "panMode": "Переміщення (перетягни для руху)", + "clearIgnoredPixels": "Очистити всі ігноровані пікселі", + "invertMask": "Інвертувати маску", + "waitingSetupComplete": "🔄 Очікування завершення початкового налаштування...", + "waitingTokenGenerator": "🔄 Очікування ініціалізації генератора токенів...", + "uploadImageFirst": "Спершу завантаж зображення для захоплення доступних кольорів", + "pleaseWaitInitialSetup": "🔄 Будь ласка, почекай завершення початкового налаштування перед завантаженням прогресу.", + "pleaseWaitFileSetup": "🔄 Будь ласка, почекай завершення початкового налаштування перед завантаженням з файлу.", + "errorSavingProgress": "❌ Помилка збереження прогресу", + "errorLoadingProgress": "❌ Помилка завантаження прогресу", + "fileOperationsAvailable": "📂 Файлові операції (Завантаження/Вивантаження) тепер доступні!", + "tokenGeneratorReady": "🔑 Генератор токенів готовий!", + "paintingStats": "Статистика малювання", + "enablePaintingSpeedLimit": "Увімкнути обмеження швидкості малювання (контроль розміру пакета)", + "enableNotifications": "Увімкнути уведомлення", + "notifyOnChargesThreshold": "Сповіщати при досягненні порогу зарядів", + "onlyWhenNotFocused": "Лише коли вкладка не в фокусі", + "repeatEvery": "Повторювати кожні", + "minutesPl": "хвилин", + "grantPermission": "Надати дозвіл", + "test": "Тест", + "showAllColorsIncluding": "Показати всі кольори (включаючи недоступні)", + "chromaWeight": "Вага насиченості", + "downloadPreview": "Завантажити попередній перегляд", + "apply": "Застосувати", + "cancel": "Скасувати", + "fit": "Підігнати", + "hundred": "100%", + "clear": "Очистити", + "invert": "Інвертувати", + "reprocessingOverlay": "Переобробка оверлея...", + "overlayUpdated": "Оверлей оновлено!", + "notificationsEnabled": "Уведомлення увімкнено.", + "notificationsPermissionDenied": "Дозвіл на уведомлення відхилено.", + "overlayEnabled": "Оверлей увімкнено.", + "overlayDisabled": "Оверлей вимкнено.", + "tokenSourceSet": "Джерело токенів встановлено: {source}", + "batchModeSet": "Пакетний режим встановлено: {mode}", + "randomRange": "Випадковий діапазон", + "normalFixedSize": "Звичайний фіксований розмір", + "advancedColorSettingsReset": "Просунуті налаштування кольорів скинуто.", + "shiftRowAltColumn": "Shift = Рядок • Alt = Стовпець", + "hideTurnstileBtn": "Сховати", + "turnstileInstructions": "Cloudflare Turnstile — будь ласка, заверши перевірку, якщо вона показана", + "uploadImageFirstColors": "Будь ласка, спершу завантаж зображення для захоплення доступних кольорів", + "availableColors": "Доступні кольори ({count})", + "colorTooltip": "ID: {id}\nRGB: {rgb}", + "expandMode": "Режим розширення", + "minimize": "Мінімізувати", + "restore": "Відновити", + "hideStats": "Приховати статистику", + "paintOptions": "Параметри малювання", + "paintWhitePixels": "Малювати білі пікселі", + "paintTransparentPixels": "Малювати прозорі пікселі" +} diff --git a/lang/vi.json b/lang/vi.json new file mode 100644 index 00000000..ea79866f --- /dev/null +++ b/lang/vi.json @@ -0,0 +1,144 @@ +{ + "title": "WPlace Auto-Image", + "toggleOverlay": "Bật/tắt lớp phủ", + "scanColors": "Quét màu", + "uploadImage": "Tải lên hình ảnh", + "resizeImage": "Thay đổi kích thước", + "selectPosition": "Chọn vị trí", + "startPainting": "Bắt đầu vẽ", + "stopPainting": "Dừng vẽ", + "checkingColors": "🔍 Đang kiểm tra màu sắc có sẵn...", + "noColorsFound": "❌ Hãy mở bảng màu trên trang web và thử lại!", + "colorsFound": "✅ Tìm thấy {count} màu. Sẵn sàng để tải lên.", + "loadingImage": "🖼️ Đang tải hình ảnh...", + "imageLoaded": "✅ Đã tải hình ảnh với {count} pixel hợp lệ", + "imageError": "❌ Lỗi khi tải hình ảnh", + "selectPositionAlert": "Vẽ pixel đầu tiên tại vị trí bạn muốn tác phẩm nghệ thuật bắt đầu!", + "waitingPosition": "👆 Đang chờ bạn vẽ pixel tham chiếu...", + "positionSet": "✅ Đã đặt vị trí thành công!", + "positionTimeout": "❌ Hết thời gian chọn vị trí", + "startPaintingMsg": "🎨 Bắt đầu vẽ...", + "paintingProgress": "🧱 Tiến trình: {painted}/{total} pixel...", + "noCharges": "⌛ Không có điện tích. Đang chờ {time}...", + "paintingStopped": "⏹️ Người dùng đã dừng vẽ", + "paintingComplete": "✅ Hoàn thành vẽ! Đã vẽ {count} pixel.", + "paintingError": "❌ Lỗi trong quá trình vẽ", + "missingRequirements": "❌ Hãy tải lên hình ảnh và chọn vị trí trước", + "progress": "Tiến trình", + "pixels": "Pixel", + "charges": "Điện tích", + "estimatedTime": "Thời gian ước tính", + "initMessage": "Nhấp 'Tải lên hình ảnh' để bắt đầu", + "waitingInit": "Đang chờ khởi tạo...", + "initializingToken": "🔧 Đang khởi tạo bộ tạo token Turnstile...", + "tokenReady": "✅ Bộ tạo token đã sẵn sàng - bạn có thể bắt đầu vẽ!", + "tokenRetryLater": "⚠️ Bộ tạo token sẽ thử lại khi cần thiết", + "resizeSuccess": "✅ Đã thay đổi kích thước hình ảnh thành {width}x{height}", + "paintingPaused": "⏸️ Tạm dừng vẽ tại vị trí X: {x}, Y: {y}", + "captchaNeeded": "❗ Tạo token thất bại. Vui lòng thử lại sau.", + "saveData": "Lưu tiến trình", + "loadData": "Tải tiến trình", + "saveToFile": "Lưu vào tệp", + "loadFromFile": "Tải từ tệp", + "dataManager": "Dữ liệu", + "autoSaved": "✅ Đã tự động lưu tiến trình", + "dataLoaded": "✅ Đã tải tiến trình thành công", + "fileSaved": "✅ Đã lưu vào tệp thành công", + "fileLoaded": "✅ Đã tải từ tệp thành công", + "noSavedData": "❌ Không tìm thấy tiến trình đã lưu", + "savedDataFound": "✅ Tìm thấy tiến trình đã lưu! Tải để tiếp tục?", + "savedDate": "Đã lưu vào: {date}", + "clickLoadToContinue": "Nhấp 'Tải tiến trình' để tiếp tục.", + "fileError": "❌ Lỗi khi xử lý tệp", + "invalidFileFormat": "❌ Định dạng tệp không hợp lệ", + "paintingSpeed": "Tốc độ vẽ", + "pixelsPerSecond": "pixel/giây", + "speedSetting": "Tốc độ: {speed} pixel/giây", + "settings": "Cài đặt", + "botSettings": "Cài đặt Bot", + "close": "Đóng", + "language": "Ngôn ngữ", + "themeSettings": "Cài đặt Giao diện", + "themeSettingsDesc": "Chọn chủ đề màu sắc yêu thích cho giao diện.", + "languageSelectDesc": "Chọn ngôn ngữ ưa thích. Thay đổi sẽ có hiệu lực ngay lập tức.", + "autoCaptcha": "Tự động giải CAPTCHA", + "autoCaptchaDesc": "Tự động cố gắng giải CAPTCHA bằng cách mô phỏng việc đặt pixel thủ công khi token hết hạn.", + "applySettings": "Áp dụng cài đặt", + "settingsSaved": "✅ Đã lưu cài đặt thành công!", + "speedOn": "Bật", + "speedOff": "Tắt", + "cooldownSettings": "Cài đặt thời gian chờ", + "waitCharges": "Chờ cho đến khi số lần sạc đạt", + "captchaSolving": "🤖 Đang cố gắng giải CAPTCHA...", + "captchaFailed": "❌ Giải CAPTCHA tự động thất bại. Vui lòng vẽ một pixel thủ công.", + "automation": "Tự động hóa", + "noChargesThreshold": "⌛ Đang chờ số lần sạc đạt {threshold}. Hiện tại {current}. Lần tiếp theo trong {time}...", + "tokenCapturedSuccess": "Đã bắt token thành công! Bạn có thể khởi động bot ngay.", + "notificationsNotSupported": "Thông báo không được hỗ trợ trong trình duyệt này.", + "chargesReadyNotification": "WPlace — Điện tích sẵn sàng", + "chargesReadyMessage": "Điện tích sẵn sàng: {current} / {max}. Ngưỡng: {threshold}.", + "testNotificationTitle": "WPlace — Thử nghiệm", + "testNotificationMessage": "Đây là thông báo thử nghiệm.", + "showStats": "Hiển thị thống kê", + "compactMode": "Chế độ gọn", + "refreshCharges": "Làm mới điện tích", + "closeStats": "Đóng thống kê", + "zoomOut": "Thu nhỏ", + "zoomIn": "Phóng to", + "fitToView": "Vừa màn hình", + "actualSize": "Kích thước thực (100%)", + "panMode": "Di chuyển (kéo để di chuyển)", + "clearIgnoredPixels": "Xóa tất cả pixel bị bỏ qua", + "invertMask": "Đảo ngược mặt nạ", + "waitingSetupComplete": "🔄 Đang chờ thiết lập ban đầu hoàn tất...", + "waitingTokenGenerator": "🔄 Đang chờ bộ tạo token khởi tạo...", + "uploadImageFirst": "Tải lên hình ảnh trước để bắt màu có sẵn", + "pleaseWaitInitialSetup": "🔄 Vui lòng đợi thiết lập ban đầu hoàn tất trước khi tải tiến trình.", + "pleaseWaitFileSetup": "🔄 Vui lòng đợi thiết lập ban đầu hoàn tất trước khi tải từ tệp.", + "errorSavingProgress": "❌ Lỗi khi lưu tiến trình", + "errorLoadingProgress": "❌ Lỗi khi tải tiến trình", + "fileOperationsAvailable": "📂 Các thao tác tệp (Tải/Upload) đã có sẵn!", + "tokenGeneratorReady": "🔑 Bộ tạo token đã sẵn sàng!", + "paintingStats": "Thống kê vẽ", + "enablePaintingSpeedLimit": "Bật giới hạn tốc độ vẽ (kiểm soát kích thước lô)", + "enableNotifications": "Bật thông báo", + "notifyOnChargesThreshold": "Thông báo khi điện tích đạt ngưỡng", + "onlyWhenNotFocused": "Chỉ khi tab không được chọn", + "repeatEvery": "Lặp lại mỗi", + "minutesPl": "phút", + "grantPermission": "Cấp quyền", + "test": "Thử nghiệm", + "showAllColorsIncluding": "Hiển thị tất cả màu (bao gồm không có sẵn)", + "chromaWeight": "Trọng số độ bão hòa", + "downloadPreview": "Tải xuống xem trước", + "apply": "Áp dụng", + "cancel": "Hủy", + "fit": "Vừa khung", + "hundred": "100%", + "clear": "Xóa", + "invert": "Đảo ngược", + "reprocessingOverlay": "Đang xử lý lại lớp phủ...", + "overlayUpdated": "Lớp phủ đã cập nhật!", + "notificationsEnabled": "Đã bật thông báo.", + "notificationsPermissionDenied": "Quyền thông báo bị từ chối.", + "overlayEnabled": "Đã bật lớp phủ.", + "overlayDisabled": "Đã tắt lớp phủ.", + "tokenSourceSet": "Nguồn token được đặt thành: {source}", + "batchModeSet": "Chế độ lô được đặt thành: {mode}", + "randomRange": "Phạm vi ngẫu nhiên", + "normalFixedSize": "Kích thước cố định bình thường", + "advancedColorSettingsReset": "Đã đặt lại cài đặt màu nâng cao.", + "shiftRowAltColumn": "Shift = Hàng • Alt = Cột", + "hideTurnstileBtn": "Ẩn", + "turnstileInstructions": "Cloudflare Turnstile — vui lòng hoàn thành kiểm tra nếu được hiển thị", + "uploadImageFirstColors": "Vui lòng tải lên hình ảnh trước để bắt màu có sẵn", + "availableColors": "Màu có sẵn ({count})", + "colorTooltip": "ID: {id}\nRGB: {rgb}", + "expandMode": "Chế độ mở rộng", + "minimize": "Thu nhỏ", + "restore": "Khôi phục", + "hideStats": "Ẩn thống kê", + "paintOptions": "Tùy chọn vẽ", + "paintWhitePixels": "Vẽ điểm ảnh trắng", + "paintTransparentPixels": "Vẽ điểm ảnh trong suốt" +} diff --git a/lang/zh-CN.json b/lang/zh-CN.json new file mode 100644 index 00000000..9058c372 --- /dev/null +++ b/lang/zh-CN.json @@ -0,0 +1,144 @@ +{ + "title": "WPlace 自动图像", + "toggleOverlay": "切换覆盖层", + "scanColors": "扫描颜色", + "uploadImage": "上传图像", + "resizeImage": "调整大小", + "selectPosition": "选择位置", + "startPainting": "开始绘制", + "stopPainting": "停止绘制", + "checkingColors": "🔍 正在检查可用颜色...", + "noColorsFound": "❌ 请在网站上打开调色板后再试!", + "colorsFound": "✅ 找到 {count} 个可用颜色,准备上传。", + "loadingImage": "🖼️ 正在加载图像...", + "imageLoaded": "✅ 图像已加载,包含 {count} 个有效像素", + "imageError": "❌ 加载图像时出错", + "selectPositionAlert": "请在你想让作品开始的位置绘制第一个像素!", + "waitingPosition": "👆 正在等待你绘制参考像素...", + "positionSet": "✅ 位置设置成功!", + "positionTimeout": "❌ 选择位置超时", + "startPaintingMsg": "🎨 开始绘制...", + "paintingProgress": "🧱 进度: {painted}/{total} 像素...", + "noCharges": "⌛ 无可用次数,等待 {time}...", + "paintingStopped": "⏹️ 已被用户停止", + "paintingComplete": "✅ 绘制完成!共绘制 {count} 个像素。", + "paintingError": "❌ 绘制过程中出错", + "missingRequirements": "❌ 请先加载图像并选择位置", + "progress": "进度", + "pixels": "像素", + "charges": "次数", + "estimatedTime": "预计时间", + "initMessage": "点击\"上传图像\"开始", + "waitingInit": "正在等待初始化...", + "initializingToken": "🔧 正在初始化 Turnstile 令牌生成器...", + "tokenReady": "✅ 令牌生成器已就绪 - 可以开始绘制!", + "tokenRetryLater": "⚠️ 令牌生成器稍后将重试", + "resizeSuccess": "✅ 图像已调整为 {width}x{height}", + "paintingPaused": "⏸️ 在位置 X: {x}, Y: {y} 暂停", + "captchaNeeded": "❗ 令牌生成失败,请稍后再试。", + "saveData": "保存进度", + "loadData": "加载进度", + "saveToFile": "保存到文件", + "loadFromFile": "从文件加载", + "dataManager": "数据管理", + "autoSaved": "✅ 进度已自动保存", + "dataLoaded": "✅ 进度加载成功", + "fileSaved": "✅ 已成功保存到文件", + "fileLoaded": "✅ 已成功从文件加载", + "noSavedData": "❌ 未找到已保存进度", + "savedDataFound": "✅ 找到已保存进度!是否加载继续?", + "savedDate": "保存时间: {date}", + "clickLoadToContinue": "点击\"加载进度\"继续。", + "fileError": "❌ 处理文件时出错", + "invalidFileFormat": "❌ 文件格式无效", + "paintingSpeed": "绘制速度", + "pixelsPerSecond": "像素/秒", + "speedSetting": "速度: {speed} 像素/秒", + "settings": "设置", + "botSettings": "机器人设置", + "close": "关闭", + "language": "语言", + "themeSettings": "主题设置", + "themeSettingsDesc": "为界面选择你喜欢的配色主题。", + "languageSelectDesc": "选择你偏好的语言,变更立即生效。", + "autoCaptcha": "自动 CAPTCHA 解决", + "autoCaptchaDesc": "使用集成的生成器自动生成 Turnstile 令牌,必要时回退到浏览器自动化。", + "applySettings": "应用设置", + "settingsSaved": "✅ 设置保存成功!", + "speedOn": "开启", + "speedOff": "关闭", + "cooldownSettings": "冷却设置", + "waitCharges": "等待次数达到", + "captchaSolving": "🔑 正在生成 Turnstile 令牌...", + "captchaFailed": "❌ 令牌生成失败。尝试回退方法...", + "automation": "自动化", + "noChargesThreshold": "⌛ 等待次数达到 {threshold}。当前 {current}。下次在 {time}...", + "tokenCapturedSuccess": "令牌捕获成功!现在可以启动机器人。", + "notificationsNotSupported": "此浏览器不支持通知。", + "chargesReadyNotification": "WPlace — 次数就绪", + "chargesReadyMessage": "次数就绪:{current} / {max}。阈值:{threshold}。", + "testNotificationTitle": "WPlace — 测试", + "testNotificationMessage": "这是一个测试通知。", + "showStats": "显示统计", + "compactMode": "紧凑模式", + "refreshCharges": "刷新次数", + "closeStats": "关闭统计", + "zoomOut": "缩小", + "zoomIn": "放大", + "fitToView": "适合窗口", + "actualSize": "实际大小 (100%)", + "panMode": "平移(拖动移动视图)", + "clearIgnoredPixels": "清除所有忽略的像素", + "invertMask": "反转蒙版", + "waitingSetupComplete": "🔄 等待初始设置完成...", + "waitingTokenGenerator": "🔄 等待令牌生成器初始化...", + "uploadImageFirst": "请先上传图像以获取可用颜色", + "pleaseWaitInitialSetup": "🔄 请等待初始设置完成后再加载进度。", + "pleaseWaitFileSetup": "🔄 请等待初始设置完成后再从文件加载。", + "errorSavingProgress": "❌ 保存进度时出错", + "errorLoadingProgress": "❌ 加载进度时出错", + "fileOperationsAvailable": "📂 文件操作(加载/上传)现已可用!", + "tokenGeneratorReady": "🔑 令牌生成器准备就绪!", + "paintingStats": "绘制统计", + "enablePaintingSpeedLimit": "启用绘制速度限制(批次大小控制)", + "enableNotifications": "启用通知", + "notifyOnChargesThreshold": "次数达到阈值时通知", + "onlyWhenNotFocused": "仅在标签页未聚焦时", + "repeatEvery": "重复间隔", + "minutesPl": "分钟", + "grantPermission": "授予权限", + "test": "测试", + "showAllColorsIncluding": "显示所有颜色(包括不可用)", + "chromaWeight": "色度权重", + "downloadPreview": "下载预览", + "apply": "应用", + "cancel": "取消", + "fit": "适合", + "hundred": "100%", + "clear": "清除", + "invert": "反转", + "reprocessingOverlay": "重新处理覆盖层...", + "overlayUpdated": "覆盖层已更新!", + "notificationsEnabled": "已启用通知。", + "notificationsPermissionDenied": "通知权限被拒绝。", + "overlayEnabled": "已启用覆盖层。", + "overlayDisabled": "已禁用覆盖层。", + "tokenSourceSet": "令牌源设置为:{source}", + "batchModeSet": "批次模式设置为:{mode}", + "randomRange": "随机范围", + "normalFixedSize": "正常固定大小", + "advancedColorSettingsReset": "已重置高级颜色设置。", + "shiftRowAltColumn": "Shift = 行 • Alt = 列", + "hideTurnstileBtn": "隐藏", + "turnstileInstructions": "Cloudflare Turnstile — 如有显示请完成验证", + "uploadImageFirstColors": "请先上传图像以获取可用颜色", + "availableColors": "可用颜色 ({count})", + "colorTooltip": "ID:{id}\nRGB:{rgb}", + "expandMode": "展开模式", + "minimize": "最小化", + "restore": "恢复", + "hideStats": "隐藏统计", + "paintOptions": "绘图选项", + "paintWhitePixels": "绘制白色像素", + "paintTransparentPixels": "绘制透明像素" +} diff --git a/lang/zh-TW.json b/lang/zh-TW.json new file mode 100644 index 00000000..384886ea --- /dev/null +++ b/lang/zh-TW.json @@ -0,0 +1,144 @@ +{ + "title": "WPlace 自動圖像", + "toggleOverlay": "切換覆蓋層", + "scanColors": "掃描顏色", + "uploadImage": "上傳圖像", + "resizeImage": "調整大小", + "selectPosition": "選擇位置", + "startPainting": "開始繪製", + "stopPainting": "停止繪製", + "checkingColors": "🔍 正在檢查可用顏色...", + "noColorsFound": "❌ 請在網站上打開調色板後再試!", + "colorsFound": "✅ 找到 {count} 個可用顏色,準備上傳。", + "loadingImage": "🖼️ 正在載入圖像...", + "imageLoaded": "✅ 圖像已載入,包含 {count} 個有效像素", + "imageError": "❌ 載入圖像時出錯", + "selectPositionAlert": "請在你想讓作品開始的位置繪製第一個像素!", + "waitingPosition": "👆 正在等待你繪製參考像素...", + "positionSet": "✅ 位置設定成功!", + "positionTimeout": "❌ 選擇位置逾時", + "startPaintingMsg": "🎨 開始繪製...", + "paintingProgress": "🧱 進度: {painted}/{total} 像素...", + "noCharges": "⌛ 無可用次數,等待 {time}...", + "paintingStopped": "⏹️ 已被使用者停止", + "paintingComplete": "✅ 繪製完成!共繪製 {count} 個像素。", + "paintingError": "❌ 繪製過程中出錯", + "missingRequirements": "❌ 請先載入圖像並選擇位置", + "progress": "進度", + "pixels": "像素", + "charges": "次數", + "estimatedTime": "預計時間", + "initMessage": "點擊「上傳圖像」開始", + "waitingInit": "正在等待初始化...", + "initializingToken": "🔧 正在初始化 Turnstile 令牌產生器...", + "tokenReady": "✅ 令牌產生器已就緒 - 可以開始繪製!", + "tokenRetryLater": "⚠️ 令牌產生器稍後將重試", + "resizeSuccess": "✅ 圖像已調整為 {width}x{height}", + "paintingPaused": "⏸️ 在位置 X: {x}, Y: {y} 暫停", + "captchaNeeded": "❗ 令牌產生失敗,請稍後再試。", + "saveData": "儲存進度", + "loadData": "載入進度", + "saveToFile": "儲存至檔案", + "loadFromFile": "從檔案載入", + "dataManager": "資料管理", + "autoSaved": "✅ 進度已自動儲存", + "dataLoaded": "✅ 進度載入成功", + "fileSaved": "✅ 已成功儲存至檔案", + "fileLoaded": "✅ 已成功從檔案載入", + "noSavedData": "❌ 未找到已儲存進度", + "savedDataFound": "✅ 找到已儲存進度!是否載入以繼續?", + "savedDate": "儲存時間: {date}", + "clickLoadToContinue": "點擊「載入進度」繼續。", + "fileError": "❌ 處理檔案時出錯", + "invalidFileFormat": "❌ 檔案格式無效", + "paintingSpeed": "繪製速度", + "pixelsPerSecond": "像素/秒", + "speedSetting": "速度: {speed} 像素/秒", + "settings": "設定", + "botSettings": "機器人設定", + "close": "關閉", + "language": "語言", + "themeSettings": "主題設定", + "themeSettingsDesc": "為介面選擇你喜歡的配色主題。", + "languageSelectDesc": "選擇你偏好的語言,變更立即生效。", + "autoCaptcha": "自動 CAPTCHA 解決", + "autoCaptchaDesc": "使用整合的產生器自動產生 Turnstile 令牌,必要時回退到瀏覽器自動化。", + "applySettings": "套用設定", + "settingsSaved": "✅ 設定儲存成功!", + "speedOn": "開啟", + "speedOff": "關閉", + "cooldownSettings": "冷卻設定", + "waitCharges": "等待次數達到", + "captchaSolving": "🔑 正在產生 Turnstile 令牌...", + "captchaFailed": "❌ 令牌產生失敗。嘗試回退方法...", + "automation": "自動化", + "noChargesThreshold": "⌛ 等待次數達到 {threshold}。目前 {current}。下次在 {time}...", + "tokenCapturedSuccess": "令牌捕獲成功!現在可以啟動機器人。", + "notificationsNotSupported": "此瀏覽器不支持通知。", + "chargesReadyNotification": "WPlace — 次數就緒", + "chargesReadyMessage": "次數就緒:{current} / {max}。閾值:{threshold}。", + "testNotificationTitle": "WPlace — 測試", + "testNotificationMessage": "這是一個測試通知。", + "showStats": "顯示統計", + "compactMode": "緊凑模式", + "refreshCharges": "刷新次數", + "closeStats": "關閉統計", + "zoomOut": "縮小", + "zoomIn": "放大", + "fitToView": "適合視窗", + "actualSize": "實際大小 (100%)", + "panMode": "平移(拖拉移動視圖)", + "clearIgnoredPixels": "清除所有忽略的像素", + "invertMask": "反轉遮罩", + "waitingSetupComplete": "🔄 等待初始設定完成...", + "waitingTokenGenerator": "🔄 等待令牌產生器初始化...", + "uploadImageFirst": "請先上傳圖像以獲取可用顏色", + "pleaseWaitInitialSetup": "🔄 請等待初始設定完成後再載入進度。", + "pleaseWaitFileSetup": "🔄 請等待初始設定完成後再從檔案載入。", + "errorSavingProgress": "❌ 儲存進度時出錯", + "errorLoadingProgress": "❌ 載入進度時出錯", + "fileOperationsAvailable": "📂 檔案操作(載入/上傳)現已可用!", + "tokenGeneratorReady": "🔑 令牌產生器準備就緒!", + "paintingStats": "繪制統計", + "enablePaintingSpeedLimit": "啟用繪制速度限制(批次大小控制)", + "enableNotifications": "啟用通知", + "notifyOnChargesThreshold": "次數達到闾值時通知", + "onlyWhenNotFocused": "僅在標籤頁未聚焦時", + "repeatEvery": "重複間隔", + "minutesPl": "分鐘", + "grantPermission": "授予權限", + "test": "測試", + "showAllColorsIncluding": "顯示所有顏色(包括不可用)", + "chromaWeight": "色度權重", + "downloadPreview": "下載預覽", + "apply": "套用", + "cancel": "取消", + "fit": "適合", + "hundred": "100%", + "clear": "清除", + "invert": "反轉", + "reprocessingOverlay": "重新處理覆蓋層...", + "overlayUpdated": "覆蓋層已更新!", + "notificationsEnabled": "已啟用通知。", + "notificationsPermissionDenied": "通知權限被拒絕。", + "overlayEnabled": "已啟用覆蓋層。", + "overlayDisabled": "已禁用覆蓋層。", + "tokenSourceSet": "令牌來源設定為:{source}", + "batchModeSet": "批次模式設定為:{mode}", + "randomRange": "隨機範圍", + "normalFixedSize": "正常固定大小", + "advancedColorSettingsReset": "已重置進階顏色設定。", + "shiftRowAltColumn": "Shift = 列 • Alt = 行", + "hideTurnstileBtn": "隱藏", + "turnstileInstructions": "Cloudflare Turnstile — 如有顯示請完成驗證", + "uploadImageFirstColors": "請先上傳圖像以獲取可用顏色", + "availableColors": "可用顏色 ({count})", + "colorTooltip": "ID:{id}\nRGB:{rgb}", + "expandMode": "展開模式", + "minimize": "最小化", + "restore": "恢復", + "hideStats": "隱藏統計", + "paintOptions": "繪圖選項", + "paintWhitePixels": "繪製白色像素", + "paintTransparentPixels": "繪製透明像素" +} diff --git a/scripts/RepairTool.js b/scripts/RepairTool.js new file mode 100644 index 00000000..c660d14f --- /dev/null +++ b/scripts/RepairTool.js @@ -0,0 +1,2593 @@ +// eslint-disable-next-line prettier/prettier +; (async () => { + // CONFIGURATION CONSTANTS + const CONFIG = { + COOLDOWN_DEFAULT: 31000, + TRANSPARENCY_THRESHOLD: 100, + WHITE_THRESHOLD: 250, + LOG_INTERVAL: 10, + PAINTING_SPEED: { + MIN: 1, + MAX: 1000, + DEFAULT: 5, + }, + TOKEN_SOURCE: 'generator', // "generator", "manual", or "hybrid" + AUTONOMOUS_MODE: true, // Enable autonomous operation + AUTO_TOKEN_REFRESH: true, // Automatically refresh tokens + TOKEN_PRELOAD_BUFFER: 60000, // Preload tokens 1 minute before expiry + MAX_RETRIES: 10, + RETRY_DELAY_BASE: 1000, + COLOR_MAP: { + 0: { id: 1, name: 'Black', rgb: { r: 0, g: 0, b: 0 } }, + 1: { id: 2, name: 'Dark Gray', rgb: { r: 60, g: 60, b: 60 } }, + 2: { id: 3, name: 'Gray', rgb: { r: 120, g: 120, b: 120 } }, + 3: { id: 4, name: 'Light Gray', rgb: { r: 210, g: 210, b: 210 } }, + 4: { id: 5, name: 'White', rgb: { r: 255, g: 255, b: 255 } }, + 5: { id: 6, name: 'Deep Red', rgb: { r: 96, g: 0, b: 24 } }, + 6: { id: 7, name: 'Red', rgb: { r: 237, g: 28, b: 36 } }, + 7: { id: 8, name: 'Orange', rgb: { r: 255, g: 127, b: 39 } }, + 8: { id: 9, name: 'Gold', rgb: { r: 246, g: 170, b: 9 } }, + 9: { id: 10, name: 'Yellow', rgb: { r: 249, g: 221, b: 59 } }, + 10: { id: 11, name: 'Light Yellow', rgb: { r: 255, g: 250, b: 188 } }, + 11: { id: 12, name: 'Dark Green', rgb: { r: 14, g: 185, b: 104 } }, + 12: { id: 13, name: 'Green', rgb: { r: 19, g: 230, b: 123 } }, + 13: { id: 14, name: 'Light Green', rgb: { r: 135, g: 255, b: 94 } }, + 14: { id: 15, name: 'Dark Teal', rgb: { r: 12, g: 129, b: 110 } }, + 15: { id: 16, name: 'Teal', rgb: { r: 16, g: 174, b: 166 } }, + 16: { id: 17, name: 'Light Teal', rgb: { r: 19, g: 225, b: 190 } }, + 17: { id: 20, name: 'Cyan', rgb: { r: 96, g: 247, b: 242 } }, + 18: { id: 44, name: 'Light Cyan', rgb: { r: 187, g: 250, b: 242 } }, + 19: { id: 18, name: 'Dark Blue', rgb: { r: 40, g: 80, b: 158 } }, + 20: { id: 19, name: 'Blue', rgb: { r: 64, g: 147, b: 228 } }, + 21: { id: 21, name: 'Indigo', rgb: { r: 107, g: 80, b: 246 } }, + 22: { id: 22, name: 'Light Indigo', rgb: { r: 153, g: 177, b: 251 } }, + 23: { id: 23, name: 'Dark Purple', rgb: { r: 120, g: 12, b: 153 } }, + 24: { id: 24, name: 'Purple', rgb: { r: 170, g: 56, b: 185 } }, + 25: { id: 25, name: 'Light Purple', rgb: { r: 224, g: 159, b: 249 } }, + 26: { id: 26, name: 'Dark Pink', rgb: { r: 203, g: 0, b: 122 } }, + 27: { id: 27, name: 'Pink', rgb: { r: 236, g: 31, b: 128 } }, + 28: { id: 28, name: 'Light Pink', rgb: { r: 243, g: 141, b: 169 } }, + 29: { id: 29, name: 'Dark Brown', rgb: { r: 104, g: 70, b: 52 } }, + 30: { id: 30, name: 'Brown', rgb: { r: 149, g: 104, b: 42 } }, + 31: { id: 31, name: 'Beige', rgb: { r: 248, g: 178, b: 119 } }, + 32: { id: 52, name: 'Light Beige', rgb: { r: 255, g: 197, b: 165 } }, + 33: { id: 32, name: 'Medium Gray', rgb: { r: 170, g: 170, b: 170 } }, + 34: { id: 33, name: 'Dark Red', rgb: { r: 165, g: 14, b: 30 } }, + 35: { id: 34, name: 'Light Red', rgb: { r: 250, g: 128, b: 114 } }, + 36: { id: 35, name: 'Dark Orange', rgb: { r: 228, g: 92, b: 26 } }, + 37: { id: 37, name: 'Dark Goldenrod', rgb: { r: 156, g: 132, b: 49 } }, + 38: { id: 38, name: 'Goldenrod', rgb: { r: 197, g: 173, b: 49 } }, + 39: { id: 39, name: 'Light Goldenrod', rgb: { r: 232, g: 212, b: 95 } }, + 40: { id: 40, name: 'Dark Olive', rgb: { r: 74, g: 107, b: 58 } }, + 41: { id: 41, name: 'Olive', rgb: { r: 90, g: 148, b: 74 } }, + 42: { id: 42, name: 'Light Olive', rgb: { r: 132, g: 197, b: 115 } }, + 43: { id: 43, name: 'Dark Cyan', rgb: { r: 15, g: 121, b: 159 } }, + 44: { id: 45, name: 'Light Blue', rgb: { r: 125, g: 199, b: 255 } }, + 45: { id: 46, name: 'Dark Indigo', rgb: { r: 77, g: 49, b: 184 } }, + 46: { id: 47, name: 'Dark Slate Blue', rgb: { r: 74, g: 66, b: 132 } }, + 47: { id: 48, name: 'Slate Blue', rgb: { r: 122, g: 113, b: 196 } }, + 48: { id: 49, name: 'Light Slate Blue', rgb: { r: 181, g: 174, b: 241 } }, + 49: { id: 53, name: 'Dark Peach', rgb: { r: 155, g: 82, b: 73 } }, + 50: { id: 54, name: 'Peach', rgb: { r: 209, g: 128, b: 120 } }, + 51: { id: 55, name: 'Light Peach', rgb: { r: 250, g: 182, b: 164 } }, + 52: { id: 50, name: 'Light Brown', rgb: { r: 219, g: 164, b: 99 } }, + 53: { id: 56, name: 'Dark Tan', rgb: { r: 123, g: 99, b: 82 } }, + 54: { id: 57, name: 'Tan', rgb: { r: 156, g: 132, b: 107 } }, + 55: { id: 36, name: 'Light Tan', rgb: { r: 214, g: 181, b: 148 } }, + 56: { id: 51, name: 'Dark Beige', rgb: { r: 209, g: 128, b: 81 } }, + 57: { id: 61, name: 'Dark Stone', rgb: { r: 109, g: 100, b: 63 } }, + 58: { id: 62, name: 'Stone', rgb: { r: 148, g: 140, b: 107 } }, + 59: { id: 63, name: 'Light Stone', rgb: { r: 205, g: 197, b: 158 } }, + 60: { id: 58, name: 'Dark Slate', rgb: { r: 51, g: 57, b: 65 } }, + 61: { id: 59, name: 'Slate', rgb: { r: 109, g: 117, b: 141 } }, + 62: { id: 60, name: 'Light Slate', rgb: { r: 179, g: 185, b: 209 } }, + 63: { id: 0, name: 'Transparent', rgb: null }, + }, + }; + + // GLOBAL STATE + const state = { + running: false, + imageLoaded: false, + totalPixels: 0, + paintedPixels: 0, + availableColors: [], + displayCharges: 0, + maxCharges: 1, + cooldown: CONFIG.COOLDOWN_DEFAULT, + imageData: null, + stopFlag: false, + startPosition: null, + region: null, + paintWhitePixels: true, + paintTransparentPixels: false, + autoRepairEnabled: false, + autoRepairInterval: 30, + autoRepairTimer: null, + debugLogs: [], + customTransparencyThreshold: CONFIG.TRANSPARENCY_THRESHOLD, + customWhiteThreshold: CONFIG.WHITE_THRESHOLD, + tokenSource: CONFIG.TOKEN_SOURCE, + autonomousMode: CONFIG.AUTONOMOUS_MODE, + autoTokenRefresh: CONFIG.AUTO_TOKEN_REFRESH, + tokenPreloadBuffer: CONFIG.TOKEN_PRELOAD_BUFFER, + retryCount: 0, + tokenRetryTimer: null, + tokenPreloadTimer: null, + windowMinimized: false, + lastAttackState: null, + initialSetupComplete: false, + }; + + // Random string generator + const randStr = (len, chars = 'abcdefghijklmnopqrstuvwxyz0123456789') => + [...Array(len)].map(() => chars[(crypto?.getRandomValues?.(new Uint32Array(1))[0] % chars.length) || Math.floor(Math.random() * chars.length)]).join(''); + + const fpStr32 = randStr(32); + + // Enhanced Turnstile token handling - Actualizado con la lógica de new.txt + let turnstileToken = null; + let tokenExpiryTime = 0; + let tokenGenerationInProgress = false; + let _resolveToken = null; + let tokenPromise = new Promise((resolve) => { + _resolveToken = resolve; + }); + let retryCount = 0; + const MAX_RETRIES = 10; + const MAX_BATCH_RETRIES = 10; + const TOKEN_LIFETIME = 240000; // 4 minutes (tokens typically last 5 min, use 4 for safety) + + function setTurnstileToken(token) { + if (_resolveToken) { + _resolveToken(token); + _resolveToken = null; + } + turnstileToken = token; + tokenExpiryTime = Date.now() + TOKEN_LIFETIME; + console.log('✅ Turnstile token set successfully'); + Utils.addDebugLog('Token cached successfully', 'success'); + } + + function isTokenValid() { + return turnstileToken && Date.now() < tokenExpiryTime; + } + + function invalidateToken() { + turnstileToken = null; + tokenExpiryTime = 0; + console.log('🗑️ Token invalidated, will force fresh generation'); + Utils.addDebugLog('Token invalidated, will force fresh generation', 'warning'); + } + + async function ensureToken(forceRefresh = false) { + // Return cached token if still valid and not forcing refresh + if (isTokenValid() && !forceRefresh) { + return turnstileToken; + } + + // Invalidate token if forcing refresh + if (forceRefresh) invalidateToken(); + + // Avoid multiple simultaneous token generations + if (tokenGenerationInProgress) { + console.log('🔄 Token generation already in progress, waiting...'); + await Utils.sleep(2000); + return isTokenValid() ? turnstileToken : null; + } + + tokenGenerationInProgress = true; + + try { + console.log('🔄 Token expired or missing, generating new one...'); + const token = await handleCaptchaWithRetry(); + if (token && token.length > 20) { + setTurnstileToken(token); + console.log('✅ Token captured and cached successfully'); + return token; + } + + console.log('⚠️ Invisible Turnstile failed, forcing browser automation...'); + const fallbackToken = await handleCaptchaFallback(); + if (fallbackToken && fallbackToken.length > 20) { + setTurnstileToken(fallbackToken); + console.log('✅ Fallback token captured successfully'); + return fallbackToken; + } + + console.log('❌ All token generation methods failed'); + return null; + } finally { + tokenGenerationInProgress = false; + } + } + + async function handleCaptchaWithRetry() { + const startTime = performance.now(); + + try { + const { sitekey, token: preGeneratedToken } = await Utils.obtainSitekeyAndToken(); + + if (!sitekey) { + throw new Error('No valid sitekey found'); + } + + console.log('🔑 Using sitekey:', sitekey); + + if (typeof window !== 'undefined' && window.navigator) { + console.log( + '🧭 UA:', + window.navigator.userAgent.substring(0, 50) + '...', + 'Platform:', + window.navigator.platform + ); + } + + let token = null; + + if ( + preGeneratedToken && + typeof preGeneratedToken === 'string' && + preGeneratedToken.length > 20 + ) { + console.log('♻️ Reusing pre-generated Turnstile token'); + token = preGeneratedToken; + } else { + if (isTokenValid()) { + console.log('♻️ Using existing cached token (from previous session)'); + token = turnstileToken; + } else { + console.log('🔍 Generating new token with executeTurnstile...'); + token = await Utils.executeTurnstile(sitekey, 'paint'); + if (token) setTurnstileToken(token); + } + } + + if (token && typeof token === 'string' && token.length > 20) { + const elapsed = Math.round(performance.now() - startTime); + console.log(`✅ Turnstile token generated successfully in ${elapsed}ms`); + return token; + } else { + throw new Error(`Invalid or empty token received - Length: ${token?.length || 0}`); + } + } catch (error) { + const elapsed = Math.round(performance.now() - startTime); + console.error(`❌ Turnstile token generation failed after ${elapsed}ms:`, error); + throw error; + } + } + + async function handleCaptchaFallback() { + return new Promise(async (resolve, reject) => { + try { + // Ensure we have a fresh promise to await for a new token capture + if (!_resolveToken) { + tokenPromise = new Promise((res) => { + _resolveToken = res; + }); + } + const timeoutPromise = Utils.sleep(20000).then(() => + reject(new Error('Auto-CAPTCHA timed out.')) + ); + + const solvePromise = (async () => { + const mainPaintBtn = await Utils.waitForSelector( + 'button.btn.btn-primary.btn-lg, button.btn-primary.sm\\:btn-xl', + 200, + 10000 + ); + if (!mainPaintBtn) throw new Error('Could not find the main paint button.'); + mainPaintBtn.click(); + await Utils.sleep(500); + + const transBtn = await Utils.waitForSelector('button#color-0', 200, 5000); + if (!transBtn) throw new Error('Could not find the transparent color button.'); + transBtn.click(); + await Utils.sleep(500); + + const canvas = await Utils.waitForSelector('canvas', 200, 5000); + if (!canvas) throw new Error('Could not find the canvas element.'); + + canvas.setAttribute('tabindex', '0'); + canvas.focus(); + const rect = canvas.getBoundingClientRect(); + const centerX = Math.round(rect.left + rect.width / 2); + const centerY = Math.round(rect.top + rect.height / 2); + + canvas.dispatchEvent( + new MouseEvent('mousemove', { + clientX: centerX, + clientY: centerY, + bubbles: true, + }) + ); + canvas.dispatchEvent( + new KeyboardEvent('keydown', { + key: ' ', + code: 'Space', + bubbles: true, + }) + ); + await Utils.sleep(50); + canvas.dispatchEvent( + new KeyboardEvent('keyup', { + key: ' ', + code: 'Space', + bubbles: true, + }) + ); + await Utils.sleep(500); + + // 800ms delay before sending confirmation + await Utils.sleep(800); + + // Keep confirming until token is captured + const confirmLoop = async () => { + while (!turnstileToken) { + let confirmBtn = await Utils.waitForSelector( + 'button.btn.btn-primary.btn-lg, button.btn.btn-primary.sm\\:btn-xl' + ); + if (!confirmBtn) { + const allPrimary = Array.from(document.querySelectorAll('button.btn-primary')); + confirmBtn = allPrimary.length ? allPrimary[allPrimary.length - 1] : null; + } + if (confirmBtn) { + confirmBtn.click(); + } + await Utils.sleep(500); // 500ms delay between confirmation attempts + } + }; + + // Start confirmation loop and wait for token + confirmLoop(); + const token = await tokenPromise; + await Utils.sleep(300); // small delay after token is captured + resolve(token); + })(); + + await Promise.race([solvePromise, timeoutPromise]); + } catch (error) { + console.error('Auto-CAPTCHA process failed:', error); + reject(error); + } + }); + } + + // NUEVA LÓGICA DE INYECCIÓN - ACTUALIZADA DESDE new.txt + function inject(callback) { + const script = document.createElement('script'); + script.textContent = `(${callback})();`; + document.documentElement?.appendChild(script); + script.remove(); + } + + inject(() => { + const fetchedBlobQueue = new Map(); + + window.addEventListener('message', (event) => { + const { source, blobID, blobData } = event.data; + if (source === 'auto-image-overlay' && blobID && blobData) { + const callback = fetchedBlobQueue.get(blobID); + if (typeof callback === 'function') { + callback(blobData); + } + fetchedBlobQueue.delete(blobID); + } + }); + + const originalFetch = window.fetch; + window.fetch = async function (...args) { + const response = await originalFetch.apply(this, args); + const url = args[0] instanceof Request ? args[0].url : args[0]; + + if (typeof url === 'string') { + if (url.includes('https://backend.wplace.live/s0/pixel/')) { + try { + const payload = JSON.parse(args[1].body); + if (payload.t) { + // 📊 Debug log + console.log( + `🔍✅ Turnstile Token Captured - Type: ${typeof payload.t}, Value: ${payload.t + ? typeof payload.t === 'string' + ? payload.t.length > 50 + ? payload.t.substring(0, 50) + '...' + : payload.t + : JSON.stringify(payload.t) + : 'null/undefined' + }, Length: ${payload.t?.length || 0}` + ); + window.postMessage({ source: 'turnstile-capture', token: payload.t }, '*'); + } + } catch (_) { + /* ignore */ + } + } + + const contentType = response.headers.get('content-type') || ''; + if (contentType.includes('image/png') && url.includes('.png')) { + const cloned = response.clone(); + return new Promise(async (resolve) => { + const blobUUID = crypto.randomUUID(); + const originalBlob = await cloned.blob(); + + fetchedBlobQueue.set(blobUUID, (processedBlob) => { + resolve( + new Response(processedBlob, { + headers: cloned.headers, + status: cloned.status, + statusText: cloned.statusText, + }) + ); + }); + + window.postMessage( + { + source: 'auto-image-tile', + endpoint: url, + blobID: blobUUID, + blobData: originalBlob, + }, + '*' + ); + }); + } + } + + return response; + }; + }); + + window.addEventListener('message', (event) => { + const { source, endpoint, blobID, blobData, token } = event.data; + + if (source === 'auto-image-tile' && endpoint && blobID && blobData) { + overlayManager.processAndRespondToTileRequest(event.data); + } + + if (source === 'turnstile-capture' && token) { + setTurnstileToken(token); + Utils.addDebugLog('Token captured from injection system', 'success'); + updateTokenStatus(); + } + }); + + // NUEVO SISTEMA WASM - ACTUALIZADO DESDE new.txt + var pawtect_chunk = null; + + // Find module if pawtect_chunk is null + pawtect_chunk ??= await findTokenModule("pawtect_wasm_bg.wasm"); + + async function createWasmToken(regionX, regionY, payload) { + try { + // Load the Pawtect module and WASM + const mod = await import(new URL('/_app/immutable/chunks/'+pawtect_chunk, location.origin).href); + let wasm; + try { + wasm = await mod._(); + console.log('✅ WASM initialized successfully'); + } catch (wasmError) { + console.error('❌ WASM initialization failed:', wasmError); + return null; + } + try { + try { + const me = await fetch(`https://backend.wplace.live/me`, { credentials: 'include' }).then(r => r.ok ? r.json() : null); + if (me?.id) { + mod.i(me.id); + console.log('✅ user ID set:', me.id); + } + } catch { } + } catch (userIdError) { + console.log('⚠️ Error setting user ID:', userIdError.message); + } + try { + const testUrl = `https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`; + if (mod.r) { + mod.r(testUrl); + console.log('✅ Request URL set:', testUrl); + } else { + console.log('⚠️ request_url function (mod.r) not available'); + } + } catch (urlError) { + console.log('⚠️ Error setting request URL:', urlError.message); + } + + console.log('🔍 payload:', payload); + + // Encode payload + const enc = new TextEncoder(); + const dec = new TextDecoder(); + const bodyStr = JSON.stringify(payload); + const bytes = enc.encode(bodyStr); + console.log('🔍 Payload size:', bytes.length, 'bytes'); + console.log('🔄 Payload string:', bodyStr); + + // Allocate WASM memory with validation + let inPtr; + try { + if (!wasm.__wbindgen_malloc) { + console.error('❌ __wbindgen_malloc function not found'); + return null; + } + + inPtr = wasm.__wbindgen_malloc(bytes.length, 1); + console.log('✅ WASM memory allocated, pointer:', inPtr); + + // Copy data to WASM memory + const wasmBuffer = new Uint8Array(wasm.memory.buffer, inPtr, bytes.length); + wasmBuffer.set(bytes); + console.log('✅ Data copied to WASM memory'); + } catch (memError) { + console.error('❌ Memory allocation error:', memError); + return null; + } + + // Call the WASM function + console.log('🚀 Calling get_pawtected_endpoint_payload...'); + let outPtr, outLen, token; + try { + const result = wasm.get_pawtected_endpoint_payload(inPtr, bytes.length); + console.log('✅ Function called, result type:', typeof result, result); + + if (Array.isArray(result) && result.length === 2) { + [outPtr, outLen] = result; + console.log('✅ Got output pointer:', outPtr, 'length:', outLen); + + // Decode the result + const outputBuffer = new Uint8Array(wasm.memory.buffer, outPtr, outLen); + token = dec.decode(outputBuffer); + console.log('✅ Token decoded successfully'); + } else { + console.error('❌ Unexpected function result format:', result); + return null; + } + } catch (funcError) { + console.error('❌ Function call error:', funcError); + console.error('Stack trace:', funcError.stack); + return null; + } + + // Cleanup memory + try { + if (wasm.__wbindgen_free && outPtr && outLen) { + wasm.__wbindgen_free(outPtr, outLen, 1); + console.log('✅ Output memory freed'); + } + if (wasm.__wbindgen_free && inPtr) { + wasm.__wbindgen_free(inPtr, bytes.length, 1); + console.log('✅ Input memory freed'); + } + } catch (cleanupError) { + console.log('⚠️ Cleanup warning:', cleanupError.message); + } + + console.log('🎉 SUCCESS!'); + console.log('🔑 Full token:', token); + return token; + } catch (error) { + console.error('❌ Failed to generate fp parameter:', error); + return null; + } + } + + async function findTokenModule(str) { + console.log('🔎 Searching for wasm Module...'); + const links = Array.from(document.querySelectorAll('link[rel="modulepreload"][href$=".js"]')); + + for (const link of links) { + try { + const url = new URL(link.getAttribute("href"), location.origin).href; + const code = await fetch(url).then(r => r.text()); + if (code.includes(str)) { + console.log('✅ Found wasm Module...'); + return url.split('/').pop(); + } + } catch { } + } + console.error(`❌ Could not find Pawtect chunk: `, error); + } + + // Audio notification system + function playNotificationSound() { + try { + const audio = new Audio('https://cdn.pixabay.com/download/audio/2025/03/21/audio_9bec51b17f.mp3?filename=glass-break-316720.mp3'); + audio.volume = 0.5; + audio.play().catch(() => { + console.warn('Could not play notification sound'); + }); + } catch (error) { + console.warn('Audio notification failed:', error); + } + } + + // Notification system for attacks and repairs + function showAttackNotification(type, count = 0) { + const existing = document.getElementById('attack-notification'); + if (existing) existing.remove(); + + const notification = document.createElement('div'); + notification.id = 'attack-notification'; + notification.style.cssText = ` + position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); + z-index: 15000; background: rgba(0,0,0,0.9); color: white; + border-radius: 15px; padding: 20px; text-align: center; + box-shadow: 0 10px 30px rgba(0,0,0,0.8); + border: 2px solid ${type === 'attack' ? '#ff4444' : '#44ff44'}; + min-width: 350px; max-width: 450px; + `; + + if (type === 'attack') { + notification.innerHTML = ` +
+ Attack +
+

+ PIXEL ART ATTACKED! +

+

+ Detected ${count} damaged pixels! +

+ `; + } else if (type === 'repaired') { + notification.innerHTML = ` +
+ Repaired +
+

+ COMPLETELY REPAIRED! +

+

+ All pixels have been restored! +

+ `; + } + + document.body.appendChild(notification); + + setTimeout(() => { + if (notification.parentNode) { + notification.style.transition = 'opacity 0.5s ease'; + notification.style.opacity = '0'; + setTimeout(() => { + if (notification.parentNode) notification.remove(); + }, 500); + } + }, 3000); + } + + // Peaceful state display in main window + function showPeacefulState() { + const statusDiv = document.getElementById('status'); + if (statusDiv && !state.running) { + statusDiv.innerHTML = ` +
+ Peaceful + No attacks detected - All secure +
+ `; + } + } + + // Typewriter effect for title + function createTypewriterTitle() { + const titleText = "WPlace Autonomous Repair Tool"; + let currentIndex = 0; + const titleElement = document.getElementById('main-title'); + + if (!titleElement) return; + + function typeNext() { + if (currentIndex < titleText.length) { + titleElement.textContent = titleText.substring(0, currentIndex + 1); + currentIndex++; + setTimeout(typeNext, 100); + } else { + setTimeout(() => { + currentIndex = 0; + titleElement.textContent = ''; + setTimeout(typeNext, 1000); + }, 3000); + } + } + + typeNext(); + } + + // Fallback translations + const TEXTS = { + title: 'WPlace Autonomous Repair Tool', + loadFromFile: 'Load Progress File', + repairPixels: 'Repair Pixels', + enableAutoRepair: 'Enable Auto Repair', + repairInterval: 'Check Interval (seconds)', + debug: 'Debug Console', + clearDebug: 'Clear Debug', + scanningForDamage: 'Scanning for damaged pixels...', + damageDetected: 'Damage detected: {count} pixels', + noDamageDetected: 'No damage found', + repairingPixels: 'Repairing {count} damaged pixels...', + repairComplete: 'Repair completed: {repaired} pixels fixed', + autoRepairStarted: 'Auto repair started (every {interval}s)', + autoRepairStopped: 'Auto repair stopped', + fileLoaded: 'Progress file loaded successfully', + invalidFile: 'Invalid file format', + noImageData: 'No image data found in file', + turnstileInstructions: 'Complete the verification', + hideTurnstileBtn: 'Hide', + tokenCapturedSuccess: 'Token captured successfully', + autonomousModeActive: 'Autonomous mode active', + tokenSystemReady: 'Advanced token system ready', + fileOperationsAvailable: 'File operations available', + initializingToken: 'Initializing token system...', + tokenReady: 'Token system ready', + }; + + // UTILIDADES ACTUALIZADAS CON NUEVA IMPLEMENTACIÓN TURNSTILE + const Utils = { + sleep: (ms) => new Promise((r) => setTimeout(r, ms)), + + t: (key, params = {}) => { + let text = TEXTS[key] || key; + Object.keys(params).forEach((param) => { + text = text.replace(`{${param}}`, params[param]); + }); + return text; + }, + + showAlert: (message, type = 'info') => { + const alertDiv = document.createElement('div'); + alertDiv.style.cssText = ` + position: fixed; top: 20px; right: 20px; z-index: 10000; + padding: 12px 16px; border-radius: 8px; color: white; font-weight: 500; + background: ${type === 'success' ? '#28a745' : type === 'error' ? '#dc3545' : type === 'warning' ? '#ffc107' : '#17a2b8'}; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + max-width: 300px; word-wrap: break-word; + `; + alertDiv.textContent = message; + document.body.appendChild(alertDiv); + + setTimeout(() => { + alertDiv.style.transition = 'opacity 0.3s ease'; + alertDiv.style.opacity = '0'; + setTimeout(() => { + if (alertDiv.parentNode) alertDiv.remove(); + }, 300); + }, 4000); + }, + + addDebugLog: (message, type = 'info') => { + const timestamp = new Date().toLocaleTimeString(); + state.debugLogs.push({ timestamp, message, type }); + + if (state.debugLogs.length > 150) { + state.debugLogs = state.debugLogs.slice(-150); + } + + updateDebugConsole(); + console.log(`[${timestamp}] ${message}`); + }, + + waitForSelector: async (selector, interval = 200, timeout = 5000) => { + const start = Date.now(); + while (Date.now() - start < timeout) { + const el = document.querySelector(selector); + if (el) return el; + await Utils.sleep(interval); + } + return null; + }, + + 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')); + } + }; + reader.onerror = () => reject(new Error('File reading error')); + reader.readAsText(file); + } else { + reject(new Error('No file selected')); + } + }; + input.click(); + }), + + isWhitePixel: (r, g, b) => { + const wt = state.customWhiteThreshold || CONFIG.WHITE_THRESHOLD; + return r >= wt && g >= wt && b >= wt; + }, + + resolveColor(targetRgb, availableColors) { + if (!availableColors || availableColors.length === 0) { + return { id: null, rgb: targetRgb }; + } + + let bestId = availableColors[0].id; + let bestRgb = [...availableColors[0].rgb]; + let bestScore = Infinity; + + for (let i = 0; i < availableColors.length; i++) { + const c = availableColors[i]; + const [r, g, b] = c.rgb; + const rmean = (r + targetRgb[0]) / 2; + const rdiff = r - targetRgb[0]; + const gdiff = g - targetRgb[1]; + const bdiff = b - targetRgb[2]; + const dist = Math.sqrt( + (((512 + rmean) * rdiff * rdiff) >> 8) + + 4 * gdiff * gdiff + + (((767 - rmean) * bdiff * bdiff) >> 8) + ); + if (dist < bestScore) { + bestScore = dist; + bestId = c.id; + bestRgb = [...c.rgb]; + if (dist === 0) break; + } + } + + return { id: bestId, rgb: bestRgb }; + }, + + calculateTileRange( + startRegionX, + startRegionY, + startPixelX, + startPixelY, + width, + height, + tileSize = 1000 + ) { + const endPixelX = startPixelX + width; + const endPixelY = startPixelY + height; + + return { + startTileX: startRegionX + Math.floor(startPixelX / tileSize), + startTileY: startRegionY + Math.floor(startPixelY / tileSize), + endTileX: startRegionX + Math.floor((endPixelX - 1) / tileSize), + endTileY: startRegionY + Math.floor((endPixelY - 1) / tileSize), + }; + }, + + dynamicSleep: async function (tickAndGetRemainingMs) { + let remaining = Math.max(0, await tickAndGetRemainingMs()); + while (remaining > 0) { + const interval = remaining > 5000 ? 2000 : remaining > 1000 ? 500 : 100; + await this.sleep(Math.min(interval, remaining)); + remaining = Math.max(0, await tickAndGetRemainingMs()); + } + }, + + // NUEVA IMPLEMENTACIÓN TURNSTILE COMPLETA - ACTUALIZADA DESDE new.txt + turnstileLoaded: false, + _turnstileContainer: null, + _turnstileOverlay: null, + _turnstileWidgetId: null, + _lastSitekey: null, + _cachedSitekey: null, + + async loadTurnstile() { + if (window.turnstile) { + this.turnstileLoaded = true; + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + if (document.querySelector('script[src^="https://challenges.cloudflare.com/turnstile/v0/api.js"]')) { + const checkReady = () => { + if (window.turnstile) { + this.turnstileLoaded = true; + resolve(); + } else { + setTimeout(checkReady, 100); + } + }; + return checkReady(); + } + + const script = document.createElement('script'); + script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; + script.async = true; + script.defer = true; + script.onload = () => { + this.turnstileLoaded = true; + Utils.addDebugLog('Turnstile script loaded successfully', 'success'); + resolve(); + }; + script.onerror = () => { + Utils.addDebugLog('Failed to load Turnstile script', 'error'); + reject(new Error('Failed to load Turnstile')); + }; + document.head.appendChild(script); + }); + }, + + ensureTurnstileContainer() { + if (!this._turnstileContainer || !document.body.contains(this._turnstileContainer)) { + if (this._turnstileContainer) { + this._turnstileContainer.remove(); + } + + this._turnstileContainer = document.createElement('div'); + this._turnstileContainer.className = 'wplace-turnstile-hidden'; + this._turnstileContainer.setAttribute('aria-hidden', 'true'); + this._turnstileContainer.id = 'turnstile-widget-container'; + document.body.appendChild(this._turnstileContainer); + } + return this._turnstileContainer; + }, + + ensureTurnstileOverlayContainer() { + if (this._turnstileOverlay && document.body.contains(this._turnstileOverlay)) { + return this._turnstileOverlay; + } + + const overlay = document.createElement('div'); + overlay.id = 'turnstile-overlay-container'; + overlay.className = 'wplace-turnstile-overlay wplace-overlay-hidden'; + + const title = document.createElement('div'); + title.textContent = Utils.t('turnstileInstructions'); + title.className = 'wplace-turnstile-title'; + + const host = document.createElement('div'); + host.id = 'turnstile-overlay-host'; + host.className = 'wplace-turnstile-host'; + + const hideBtn = document.createElement('button'); + hideBtn.textContent = Utils.t('hideTurnstileBtn'); + hideBtn.className = 'wplace-turnstile-hide-btn'; + hideBtn.addEventListener('click', () => overlay.remove()); + + overlay.appendChild(title); + overlay.appendChild(host); + overlay.appendChild(hideBtn); + document.body.appendChild(overlay); + + this._turnstileOverlay = overlay; + return overlay; + }, + + async executeTurnstile(sitekey, action = 'paint') { + await this.loadTurnstile(); + + // Try reusing existing widget first if sitekey matches + if (this._turnstileWidgetId && this._lastSitekey === sitekey && window.turnstile?.execute) { + try { + console.log('🔄 Reusing existing Turnstile widget...'); + const token = await Promise.race([ + window.turnstile.execute(this._turnstileWidgetId, { action }), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Execute timeout')), 15000) + ), + ]); + if (token && token.length > 20) { + console.log('✅ Token generated via widget reuse'); + return token; + } + } catch (error) { + console.log(' Widget reuse failed, will create a fresh widget:', error.message); + } + } + + // Try invisible widget first + const invisibleToken = await this.createTurnstileWidget(sitekey, action); + if (invisibleToken && invisibleToken.length > 20) { + return invisibleToken; + } + + console.log(' Falling back to interactive Turnstile (visible).'); + return await this.createTurnstileWidgetInteractive(sitekey, action); + }, + + async createTurnstileWidget(sitekey, action) { + return new Promise((resolve) => { + try { + // Force cleanup of any existing widget + if (this._turnstileWidgetId && window.turnstile?.remove) { + 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, + action, + size: 'invisible', + retry: 'auto', + 'retry-interval': 8000, + callback: (token) => { + console.log('✅ Invisible Turnstile callback'); + resolve(token); + }, + 'error-callback': () => resolve(null), + 'timeout-callback': () => resolve(null), + }); + + this._turnstileWidgetId = widgetId; + this._lastSitekey = sitekey; + + if (!widgetId) { + return resolve(null); + } + + // Execute the widget and race with timeout + Promise.race([ + window.turnstile.execute(widgetId, { action }), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Invisible execute timeout')), 12000) + ), + ]) + .then(resolve) + .catch(() => resolve(null)); + } catch (e) { + console.error('❌ Invisible Turnstile creation failed:', e); + resolve(null); + } + }); + }, + + 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); + } 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, + size: 'normal', + theme: 'light', + callback: (token) => { + clearTimeout(timeout); + 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 { + console.warn('❌ Invalid token from interactive widget'); + resolve(null); + } + }, + 'error-callback': (error) => { + clearTimeout(timeout); + 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); + } else { + console.log('✅ Interactive Turnstile widget created, waiting for user interaction...'); + } + } catch (e) { + console.error('❌ Interactive Turnstile creation failed:', e); + resolve(null); + } + }); + }, + + cleanupTurnstile() { + if (this._turnstileWidgetId && window.turnstile?.remove) { + try { + window.turnstile.remove(this._turnstileWidgetId); + } catch (e) { + console.warn('Failed to cleanup Turnstile widget:', e); + } + } + + if (this._turnstileContainer && document.body.contains(this._turnstileContainer)) { + this._turnstileContainer.remove(); + } + + if (this._turnstileOverlay && document.body.contains(this._turnstileOverlay)) { + this._turnstileOverlay.remove(); + } + + this._turnstileWidgetId = null; + this._turnstileContainer = null; + this._turnstileOverlay = null; + this._lastSitekey = null; + }, + + // DETECCIÓN DE SITEKEY MEJORADA - ACTUALIZADA DESDE new.txt + async obtainSitekeyAndToken(fallback = '0x4AAAAAABpqJe8FO0N84q0F') { + // Cache sitekey to avoid repeated DOM queries + if (this._cachedSitekey) { + console.log('🔍 Using cached sitekey:', this._cachedSitekey); + + return isTokenValid() + ? { + sitekey: this._cachedSitekey, + token: turnstileToken, + } + : { sitekey: this._cachedSitekey, token: null }; + } + + // List of potential sitekeys to try + const potentialSitekeys = [ + '0x4AAAAAABpqJe8FO0N84q0F', // WPlace common sitekey + '0x4AAAAAAAJ7xjKAp6Mt_7zw', // Alternative WPlace sitekey + '0x4AAAAAADm5QWx6Ov2LNF2g', // Another common sitekey + ]; + const trySitekey = async (sitekey, source) => { + if (!sitekey || sitekey.length < 10) return null; + + console.log(`🔍 Testing sitekey from ${source}:`, sitekey); + const token = await this.executeTurnstile(sitekey); + + if (token && token.length >= 20) { + console.log(`✅ Valid token generated from ${source} sitekey`); + setTurnstileToken(token); + this._cachedSitekey = sitekey; + return { sitekey, token }; + } else { + console.log(`❌ Failed to get token from ${source} sitekey`); + return null; + } + }; + + try { + // 1️⃣ data-sitekey attribute + const sitekeySel = document.querySelector('[data-sitekey]'); + if (sitekeySel) { + const sitekey = sitekeySel.getAttribute('data-sitekey'); + const result = await trySitekey(sitekey, 'data attribute'); + if (result) { + return result; + } + } + + // 2️⃣ Turnstile element + const turnstileEl = document.querySelector('.cf-turnstile'); + if (turnstileEl?.dataset?.sitekey) { + const sitekey = turnstileEl.dataset.sitekey; + const result = await trySitekey(sitekey, 'turnstile element'); + if (result) { + return result; + } + } + + // 3️⃣ Meta tags + const metaTags = document.querySelectorAll( + 'meta[name*="turnstile"], meta[property*="turnstile"]' + ); + for (const meta of metaTags) { + const content = meta.getAttribute('content'); + const result = await trySitekey(content, 'meta tag'); + if (result) { + return result; + } + } + + // 4️⃣ Global variable + if (window.__TURNSTILE_SITEKEY) { + const result = await trySitekey(window.__TURNSTILE_SITEKEY, 'global variable'); + if (result) { + return result; + } + } + + // 5️⃣ Script tags + const scripts = document.querySelectorAll('script'); + for (const script of scripts) { + const content = script.textContent || script.innerHTML; + const match = content.match( + /(?:sitekey|data-sitekey)['"\s\[\]:\=\(]*['"]?([0-9a-zA-Z_-]{20,})['"]?/i + ); + if (match && match[1]) { + const extracted = match[1].replace(/['"]/g, ''); + const result = await trySitekey(extracted, 'script content'); + if (result) { + return result; + } + } + } + + // 6️⃣ Known potential sitekeys + console.log('🔍 Testing known potential sitekeys...'); + for (const testSitekey of potentialSitekeys) { + const result = await trySitekey(testSitekey, 'known list'); + if (result) { + return result; + } + } + } catch (error) { + console.warn('⚠️ Error during sitekey detection:', error); + } + + // 7️⃣ Fallback + console.log('🔧 Trying fallback sitekey:', fallback); + const fallbackResult = await trySitekey(fallback, 'fallback'); + if (fallbackResult) { + return fallbackResult; + } + + console.error('❌ No working sitekey or token found.'); + return { sitekey: null, token: null }; + }, + }; + + // Enhanced Overlay Manager for pixel detection with autonomous capabilities + class OverlayManager { + constructor() { + this.originalTiles = new Map(); + this.originalTilesData = new Map(); + this.tileSize = 1000; + this.loadingPromises = new Map(); + this.autonomousMode = state.autonomousMode; + } + + async processAndRespondToTileRequest(eventData) { + const { endpoint, blobID, blobData } = eventData; + + const tileMatch = endpoint.match(/(\d+)\/(\d+)\.png/); + if (tileMatch) { + const tileX = parseInt(tileMatch[1], 10); + const tileY = parseInt(tileMatch[2], 10); + const tileKey = `${tileX},${tileY}`; + + try { + const originalBitmap = await createImageBitmap(blobData); + this.originalTiles.set(tileKey, originalBitmap); + + try { + let canvas, ctx; + if (typeof OffscreenCanvas !== 'undefined') { + canvas = new OffscreenCanvas(originalBitmap.width, originalBitmap.height); + ctx = canvas.getContext('2d'); + } else { + canvas = document.createElement('canvas'); + canvas.width = originalBitmap.width; + canvas.height = originalBitmap.height; + ctx = canvas.getContext('2d'); + } + ctx.imageSmoothingEnabled = false; + ctx.drawImage(originalBitmap, 0, 0); + const imgData = ctx.getImageData(0, 0, originalBitmap.width, originalBitmap.height); + + this.originalTilesData.set(tileKey, { + w: originalBitmap.width, + h: originalBitmap.height, + data: new Uint8ClampedArray(imgData.data), + }); + + if (this.autonomousMode) { + Utils.addDebugLog(`Auto-cached tile: ${tileKey} (${originalBitmap.width}x${originalBitmap.height})`, 'info'); + } + } catch (e) { + Utils.addDebugLog(`Failed to cache tile ImageData: ${tileKey} - ${e.message}`, 'warning'); + } + } catch (e) { + Utils.addDebugLog(`Failed to create tile bitmap: ${tileKey} - ${e.message}`, 'error'); + } + } + + window.postMessage( + { + source: 'auto-image-overlay', + blobID: blobID, + blobData: blobData, + }, + '*' + ); + } + + async getTilePixelColor(tileX, tileY, pixelX, pixelY) { + const tileKey = `${tileX},${tileY}`; + const alphaThresh = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; + + const cached = this.originalTilesData.get(tileKey); + if (cached && cached.data && cached.w > 0 && cached.h > 0) { + const x = Math.max(0, Math.min(cached.w - 1, pixelX)); + const y = Math.max(0, Math.min(cached.h - 1, pixelY)); + const idx = (y * cached.w + x) * 4; + const d = cached.data; + const a = d[idx + 3]; + + if (!state.paintTransparentPixels && a < alphaThresh) { + return null; + } + return [d[idx], d[idx + 1], d[idx + 2], a]; + } + + const bitmap = this.originalTiles.get(tileKey); + if (!bitmap) { + if (this.autonomousMode) { + Utils.addDebugLog(`Tile ${tileKey} not available, requesting load...`, 'warning'); + } + return null; + } + + try { + let canvas, ctx; + if (typeof OffscreenCanvas !== 'undefined') { + canvas = new OffscreenCanvas(bitmap.width, bitmap.height); + ctx = canvas.getContext('2d'); + } else { + canvas = document.createElement('canvas'); + canvas.width = bitmap.width; + canvas.height = bitmap.height; + ctx = canvas.getContext('2d'); + } + ctx.imageSmoothingEnabled = false; + ctx.drawImage(bitmap, 0, 0); + + const x = Math.max(0, Math.min(bitmap.width - 1, pixelX)); + const y = Math.max(0, Math.min(bitmap.height - 1, pixelY)); + const data = ctx.getImageData(x, y, 1, 1).data; + const a = data[3]; + + if (!state.paintTransparentPixels && a < alphaThresh) { + return null; + } + + return [data[0], data[1], data[2], a]; + } catch (e) { + Utils.addDebugLog(`Error reading pixel from tile ${tileKey}: ${e.message}`, 'error'); + return null; + } + } + + async waitForTiles(startRegionX, startRegionY, pixelWidth, pixelHeight, startPixelX = 0, startPixelY = 0, timeoutMs = 15000) { + const { startTileX, startTileY, endTileX, endTileY } = Utils.calculateTileRange( + startRegionX, + startRegionY, + startPixelX, + startPixelY, + pixelWidth, + pixelHeight, + this.tileSize + ); + + const requiredTiles = []; + for (let ty = startTileY; ty <= endTileY; ty++) { + for (let tx = startTileX; tx <= endTileX; tx++) { + requiredTiles.push(`${tx},${ty}`); + } + } + + if (requiredTiles.length === 0) return true; + + Utils.addDebugLog(`Waiting for ${requiredTiles.length} tiles (autonomous: ${this.autonomousMode})...`, 'info'); + + const startTime = Date.now(); + let lastProgress = 0; + + while (Date.now() - startTime < timeoutMs) { + if (state.stopFlag) { + Utils.addDebugLog('waitForTiles: stopped by user', 'warning'); + return false; + } + + const loaded = requiredTiles.filter((k) => this.originalTiles.has(k)).length; + const progress = Math.round((loaded / requiredTiles.length) * 100); + + if (progress !== lastProgress && progress % 20 === 0) { + Utils.addDebugLog(`Tile loading progress: ${loaded}/${requiredTiles.length} (${progress}%)`, 'info'); + lastProgress = progress; + } + + if (loaded === requiredTiles.length) { + Utils.addDebugLog(`All ${requiredTiles.length} required tiles are loaded`, 'success'); + return true; + } + + await Utils.sleep(this.autonomousMode ? 500 : 1000); + } + + const loaded = requiredTiles.filter((k) => this.originalTiles.has(k)).length; + Utils.addDebugLog(`Timeout waiting for tiles: ${loaded}/${requiredTiles.length} loaded`, 'warning'); + + if (this.autonomousMode && loaded > requiredTiles.length * 0.8) { + Utils.addDebugLog(`Autonomous mode: proceeding with ${loaded}/${requiredTiles.length} tiles (80%+ loaded)`, 'warning'); + return true; + } + + return loaded > 0; + } + } + + const overlayManager = new OverlayManager(); + + // Enhanced WPlace API Service with autonomous capabilities - ACTUALIZADO CON NUEVA LÓGICA + const WPlaceService = { + async paintPixelInRegion(regionX, regionY, pixelX, pixelY, color, retryCount = 0) { + try { + await ensureToken(); + if (!turnstileToken) { + Utils.addDebugLog('No valid token available for paint request', 'error'); + return 'token_error'; + } + + const payload = { + coords: [pixelX, pixelY], + colors: [color], + t: turnstileToken, + fp: fpStr32, + }; + + const wasmToken = await createWasmToken(regionX, regionY, payload); + if (!wasmToken) { + Utils.addDebugLog('Failed to generate WASM token', 'error'); + return false; + } + + const res = await fetch(`https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, { + method: 'POST', + headers: { + 'Content-Type': 'text/plain;charset=UTF-8', + 'x-pawtect-token': wasmToken + }, + credentials: 'include', + body: JSON.stringify(payload), + }); + + if (res.status === 403) { + Utils.addDebugLog(`403 Forbidden for pixel (${pixelX},${pixelY}). Token invalid/expired.`, 'error'); + invalidateToken(); + + if (retryCount < 2 && state.autonomousMode) { + Utils.addDebugLog(`Autonomous retry ${retryCount + 1}/2 for pixel (${pixelX},${pixelY})`, 'warning'); + await Utils.sleep(1000); + return await this.paintPixelInRegion(regionX, regionY, pixelX, pixelY, color, retryCount + 1); + } + + return 'token_error'; + } + + if (!res.ok) { + Utils.addDebugLog(`Paint request failed with status ${res.status}`, 'error'); + return false; + } + + const data = await res.json(); + const success = data?.painted === 1; + + if (success) { + Utils.addDebugLog(`Paint SUCCESS for (${pixelX},${pixelY}) with color ${color}`, 'success'); + } else { + Utils.addDebugLog(`Paint FAILED for (${pixelX},${pixelY}) - server response: ${JSON.stringify(data)}`, 'error'); + } + + return success; + } catch (e) { + Utils.addDebugLog(`Paint request error for (${pixelX},${pixelY}): ${e.message}`, 'error'); + return false; + } + }, + + async getCharges() { + try { + const res = await fetch('https://backend.wplace.live/me', { + credentials: 'include', + }); + if (!res.ok) return { charges: 0, max: 1, cooldown: CONFIG.COOLDOWN_DEFAULT }; + const data = await res.json(); + return { + charges: data.charges?.count ?? 0, + max: data.charges?.max ?? 1, + cooldown: data.charges?.cooldownMs ?? CONFIG.COOLDOWN_DEFAULT, + }; + } catch (e) { + Utils.addDebugLog(`Error fetching charges: ${e.message}`, 'warning'); + return { charges: 0, max: 1, cooldown: CONFIG.COOLDOWN_DEFAULT }; + } + }, + }; + + // Anti-grief repair system with autonomous capabilities + async function scanForDamage() { + if (!state.imageData || !state.startPosition || !state.region) { + Utils.addDebugLog('No image data or position for scanning', 'warning'); + return []; + } + + Utils.addDebugLog('Starting autonomous damage scan...', 'info'); + updateStatus(Utils.t('scanningForDamage')); + + const damagedPixels = []; + const { width, height, pixels } = state.imageData; + + const ready = await overlayManager.waitForTiles( + state.region.x, + state.region.y, + width, + height, + state.startPosition.x, + state.startPosition.y, + state.autonomousMode ? 20000 : 15000 + ); + + if (!ready) { + Utils.addDebugLog('Failed to load required tiles for scanning', 'error'); + if (state.autonomousMode) { + Utils.addDebugLog('Autonomous mode: will retry scan in 30 seconds', 'warning'); + setTimeout(() => { + if (state.autoRepairEnabled && !state.stopFlag) { + scanForDamage(); + } + }, 30000); + } + return []; + } + + Utils.addDebugLog(`Scanning ${width}x${height} image for damage (autonomous: ${state.autonomousMode})...`, 'info'); + + let scannedPixels = 0; + let lastProgressUpdate = Date.now(); + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + if (state.stopFlag) break; + + const idx = (y * width + x) * 4; + const originalR = pixels[idx]; + const originalG = pixels[idx + 1]; + const originalB = pixels[idx + 2]; + const originalA = pixels[idx + 3]; + + if (!state.paintTransparentPixels && originalA < CONFIG.TRANSPARENCY_THRESHOLD) { + continue; + } + + if (!state.paintWhitePixels && Utils.isWhitePixel(originalR, originalG, originalB)) { + continue; + } + + scannedPixels++; + + const absX = state.startPosition.x + x; + const absY = state.startPosition.y + y; + const tileX = state.region.x + Math.floor(absX / 1000); + const tileY = state.region.y + Math.floor(absY / 1000); + const pixelX = absX % 1000; + const pixelY = absY % 1000; + + try { + const currentPixel = await overlayManager.getTilePixelColor(tileX, tileY, pixelX, pixelY); + + if (!currentPixel) { + if (!state.autonomousMode) { + Utils.addDebugLog(`No current pixel data for (${x},${y}) - tile ${tileX},${tileY}`, 'warning'); + } + continue; + } + + 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) + }); + + if (!state.autonomousMode || damagedPixels.length <= 10) { + Utils.addDebugLog(`Damage at (${x},${y}): expected color ${targetColor.id}, found color ${currentColor.id}`, 'warning'); + } + } + } catch (e) { + if (!state.autonomousMode) { + Utils.addDebugLog(`Error checking pixel (${x},${y}): ${e.message}`, 'error'); + } + } + } + + if (state.autonomousMode && Date.now() - lastProgressUpdate > 5000) { + Utils.addDebugLog(`Scan progress: ${y}/${height} rows (${Math.round((y / height) * 100)}%), found ${damagedPixels.length} damaged`, 'info'); + lastProgressUpdate = Date.now(); + } else if (!state.autonomousMode && y % 10 === 0) { + Utils.addDebugLog(`Scan progress: ${y}/${height} rows (${Math.round((y / height) * 100)}%)`, 'info'); + } + } + + const logLevel = damagedPixels.length > 0 ? 'warning' : 'success'; + Utils.addDebugLog(`Scan complete. Checked ${scannedPixels} pixels, found ${damagedPixels.length} damaged`, logLevel); + + return damagedPixels; + } + + async function repairDamagedPixels(damagedPixels) { + if (damagedPixels.length === 0) { + updateStatus(Utils.t('noDamageDetected')); + return 0; + } + + Utils.addDebugLog(`Starting autonomous repair of ${damagedPixels.length} pixels`, 'info'); + updateStatus(Utils.t('repairingPixels', { count: damagedPixels.length })); + + let repairedCount = 0; + let consecutiveFailures = 0; + const maxConsecutiveFailures = state.autonomousMode ? 5 : 3; + + for (let i = 0; i < damagedPixels.length; i++) { + const pixel = damagedPixels[i]; + + if (state.stopFlag) { + Utils.addDebugLog('Repair stopped by user request', 'warning'); + break; + } + + // Enhanced charge management for autonomous mode + await updateCharges(); + let chargeWaitAttempts = 0; + const maxChargeWaitAttempts = state.autonomousMode ? 20 : 10; + + while (state.displayCharges < 1 && !state.stopFlag && chargeWaitAttempts < maxChargeWaitAttempts) { + chargeWaitAttempts++; + const waitTime = state.autonomousMode ? Math.min(state.cooldown, 10000) : state.cooldown; + + if (chargeWaitAttempts === 1) { + Utils.addDebugLog(`Waiting for charges... (${state.displayCharges}/${state.maxCharges})`, 'info'); + } + + await Utils.dynamicSleep(() => { + if (state.displayCharges >= 1) return 0; + if (state.stopFlag) return 0; + return waitTime; + }); + await updateCharges(); + } + + if (state.stopFlag) break; + + if (state.displayCharges < 1) { + Utils.addDebugLog(`No charges available after waiting, skipping pixel (${pixel.x},${pixel.y})`, 'warning'); + continue; + } + + const success = await repairSinglePixel(pixel); + if (success) { + repairedCount++; + consecutiveFailures = 0; + if (!state.autonomousMode || repairedCount <= 10 || repairedCount % 10 === 0) { + Utils.addDebugLog(`Repaired pixel (${pixel.x},${pixel.y}) with color ${pixel.originalColor.id} [${repairedCount}/${damagedPixels.length}]`, 'success'); + } + } else { + consecutiveFailures++; + Utils.addDebugLog(`Failed to repair pixel (${pixel.x},${pixel.y}) [${consecutiveFailures}/${maxConsecutiveFailures} consecutive failures]`, 'error'); + + if (consecutiveFailures >= maxConsecutiveFailures) { + Utils.addDebugLog(`Too many consecutive failures (${consecutiveFailures}), pausing repair`, 'error'); + if (state.autonomousMode) { + Utils.addDebugLog('Autonomous mode: will retry repair in 60 seconds', 'warning'); + setTimeout(() => { + if (state.autoRepairEnabled && !state.stopFlag) { + performRepairCheck(); + } + }, 60000); + } + break; + } + } + + await updateCharges(); + + // Dynamic delay based on mode and success rate + const baseDelay = state.autonomousMode ? 100 : 200; + const adaptiveDelay = consecutiveFailures > 0 ? baseDelay * (consecutiveFailures + 1) : baseDelay; + await Utils.sleep(adaptiveDelay); + } + + const message = Utils.t('repairComplete', { repaired: repairedCount }); + updateStatus(message); + Utils.addDebugLog(`${message} (${damagedPixels.length - repairedCount} remaining)`, 'success'); + + return repairedCount; + } + + async function repairSinglePixel(pixel) { + const { x, y, originalColor } = pixel; + + const absX = state.startPosition.x + x; + const absY = state.startPosition.y + y; + const regionX = state.region.x + Math.floor(absX / 1000); + const regionY = state.region.y + Math.floor(absY / 1000); + const pixelX = absX % 1000; + const pixelY = absY % 1000; + + try { + const result = await WPlaceService.paintPixelInRegion( + regionX, + regionY, + pixelX, + pixelY, + originalColor.id + ); + + if (result === 'token_error') { + Utils.addDebugLog('Token error during repair, refreshing token...', 'warning'); + await ensureToken(true); + await Utils.sleep(state.autonomousMode ? 500 : 1000); + + // Retry once with new token + const retryResult = await WPlaceService.paintPixelInRegion(regionX, regionY, pixelX, pixelY, originalColor.id); + return retryResult === true; + } + + return result === true; + } catch (e) { + Utils.addDebugLog(`Error repairing pixel (${x},${y}): ${e.message}`, 'error'); + return false; + } + } + + async function performRepairCheck() { + if (state.running) { + Utils.addDebugLog('Repair check skipped - manual repair in progress', 'info'); + return; + } + + try { + Utils.addDebugLog('Autonomous repair check triggered', 'info'); + + // Ensure we have a valid token before starting + if (!isTokenValid()) { + Utils.addDebugLog('No valid token for autonomous repair, generating...', 'warning'); + await ensureToken(true); + if (!isTokenValid()) { + Utils.addDebugLog('Failed to generate token for autonomous repair, will retry next cycle', 'error'); + return; + } + } + + const damagedPixels = await scanForDamage(); + + if (damagedPixels.length > 0) { + Utils.addDebugLog(`Autonomous repair: Found ${damagedPixels.length} damaged pixels, starting repair`, 'warning'); + updateStatus(Utils.t('damageDetected', { count: damagedPixels.length })); + + // Show attack notification + showAttackNotification('attack', damagedPixels.length); + state.lastAttackState = 'attacked'; + + const repairedCount = await repairDamagedPixels(damagedPixels); + + // Show repair complete notification if all pixels were repaired + if (repairedCount === damagedPixels.length) { + showAttackNotification('repaired'); + state.lastAttackState = 'repaired'; + } + } else { + updateStatus(Utils.t('noDamageDetected')); + + // Show peaceful state if we're not under attack + if (state.lastAttackState !== 'peaceful') { + showPeacefulState(); + state.lastAttackState = 'peaceful'; + } + + if (!state.autonomousMode) { + Utils.addDebugLog('Autonomous repair: No damage detected', 'success'); + } + } + } catch (error) { + Utils.addDebugLog(`Autonomous repair error: ${error.message}`, 'error'); + + if (state.autonomousMode) { + Utils.addDebugLog('Autonomous mode: will retry repair check in 120 seconds due to error', 'warning'); + setTimeout(() => { + if (state.autoRepairEnabled && !state.stopFlag) { + performRepairCheck(); + } + }, 120000); + } + } + } + + function startAutoRepair() { + if (state.autoRepairTimer) { + clearInterval(state.autoRepairTimer); + } + + const intervalMs = state.autoRepairInterval * 1000; + state.autoRepairTimer = setInterval(performRepairCheck, intervalMs); + + const message = Utils.t('autoRepairStarted', { interval: state.autoRepairInterval }); + Utils.addDebugLog(`${message} (autonomous: ${state.autonomousMode})`, 'info'); + Utils.showAlert(message, 'success'); + + // Perform first check immediately + setTimeout(performRepairCheck, 2000); + } + + function stopAutoRepair() { + if (state.autoRepairTimer) { + clearInterval(state.autoRepairTimer); + state.autoRepairTimer = null; + } + + const message = Utils.t('autoRepairStopped'); + Utils.addDebugLog(`${message}`, 'info'); + Utils.showAlert(message, 'info'); + } + + // FUNCIONES DE UI MEJORADAS CON GESTIÓN DE CONFIGURACIÓN INICIAL + function enableFileOperations() { + state.initialSetupComplete = true; + + const loadBtn = document.querySelector('#loadFileBtn'); + if (loadBtn) { + loadBtn.disabled = false; + loadBtn.title = ''; + loadBtn.style.animation = 'pulse 0.6s ease-in-out'; + setTimeout(() => { + if (loadBtn) loadBtn.style.animation = ''; + }, 600); + Utils.addDebugLog('Load Progress button enabled after initial setup', 'success'); + } + + Utils.showAlert(Utils.t('fileOperationsAvailable'), 'success'); + } + + async function initializeTokenGenerator() { + if (isTokenValid()) { + Utils.addDebugLog('Valid token already available, skipping initialization', 'success'); + updateTokenStatus(); + enableFileOperations(); + return; + } + + try { + Utils.addDebugLog('Initializing Turnstile token generator...', 'info'); + updateStatus(Utils.t('initializingToken')); + + await Utils.loadTurnstile(); + Utils.addDebugLog('Turnstile script loaded successfully', 'success'); + + if (state.autoTokenRefresh) { + Utils.addDebugLog('Pre-generating token for autonomous operations...', 'info'); + await ensureToken(); + } + + state.initialSetupComplete = true; + Utils.addDebugLog('Enhanced token generator initialization complete', 'success'); + updateStatus(Utils.t('tokenReady')); + enableFileOperations(); + + } catch (error) { + Utils.addDebugLog('Token system initialization failed: ' + error.message, 'warning'); + Utils.addDebugLog('Manual token generation will be required', 'info'); + enableFileOperations(); + } + } + + function updateStatus(message) { + const statusEl = document.getElementById('status'); + if (statusEl) { + if (typeof message === 'string') { + statusEl.innerHTML = message; + } else { + statusEl.textContent = message; + } + } + } + + function updateDebugConsole() { + const debugConsole = document.getElementById('debugConsole'); + if (!debugConsole) return; + + const logsHtml = state.debugLogs.map(log => { + const color = log.type === 'error' ? '#ff6b6b' : + log.type === 'warning' ? '#ffa726' : + log.type === 'success' ? '#66bb6a' : '#64b5f6'; + + return `
+ [${log.timestamp}] ${log.message} +
`; + }).join(''); + + debugConsole.innerHTML = logsHtml; + debugConsole.scrollTop = debugConsole.scrollHeight; + } + + async function updateCharges() { + try { + const { charges, max, cooldown } = await WPlaceService.getCharges(); + state.displayCharges = Math.floor(charges); + state.maxCharges = Math.max(1, Math.floor(max)); + state.cooldown = cooldown; + + updateChargesDisplay(); + } catch (error) { + if (!state.autonomousMode) { + Utils.addDebugLog(`Error updating charges: ${error.message}`, 'error'); + } + } + } + + function updateChargesDisplay() { + const chargesEl = document.getElementById('chargesInfo'); + if (chargesEl) { + chargesEl.textContent = `Charges: ${state.displayCharges}/${state.maxCharges} (cooldown: ${Math.round(state.cooldown / 1000)}s)`; + } + } + + function updateTokenStatus() { + const tokenEl = document.getElementById('tokenInfo'); + if (tokenEl) { + if (isTokenValid()) { + const remaining = Math.round((tokenExpiryTime - Date.now()) / 1000); + tokenEl.textContent = `Token: Valid (expires in ${remaining}s)`; + tokenEl.style.color = '#81c784'; + } else { + tokenEl.textContent = 'Token: Not generated or expired'; + tokenEl.style.color = '#ffab91'; + } + } + } + + function updateSystemStatus() { + const systemEl = document.getElementById('systemInfo'); + if (systemEl) { + const autonomousStatus = state.autonomousMode ? 'AUTONOMOUS' : 'MANUAL'; + const tokenMode = state.tokenSource.toUpperCase(); + systemEl.textContent = `System: ${autonomousStatus} | Token: ${tokenMode} | Auto-refresh: ${state.autoTokenRefresh ? 'ON' : 'OFF'}`; + } + } + + // Window dragging functionality + function makeWindowDraggable() { + const container = document.getElementById('wplace-repair-tool'); + const header = document.getElementById('window-header'); + + let isDragging = false; + let currentX; + let currentY; + let initialX; + let initialY; + let xOffset = 0; + let yOffset = 0; + + function dragStart(e) { + if (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') { + return; + } + + initialX = e.clientX - xOffset; + initialY = e.clientY - yOffset; + + if (e.target === header || header.contains(e.target)) { + isDragging = true; + container.style.cursor = 'grabbing'; + } + } + + function dragEnd() { + initialX = currentX; + initialY = currentY; + isDragging = false; + container.style.cursor = 'default'; + } + + function drag(e) { + if (isDragging) { + e.preventDefault(); + currentX = e.clientX - initialX; + currentY = e.clientY - initialY; + xOffset = currentX; + yOffset = currentY; + + container.style.transform = `translate(${currentX}px, ${currentY}px)`; + } + } + + header.addEventListener('mousedown', dragStart); + document.addEventListener('mouseup', dragEnd); + document.addEventListener('mousemove', drag); + } + + // Enhanced UI Creation with NEW IMPLEMENTATION + function createUI() { + const existing = document.getElementById('wplace-repair-tool'); + if (existing) existing.remove(); + + const container = document.createElement('div'); + container.id = 'wplace-repair-tool'; + container.style.cssText = ` + position: fixed; top: 20px; left: 20px; z-index: 10000; + background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 12px; min-width: 520px; max-width: 600px; + box-shadow: 0 8px 32px rgba(0,0,0,0.6); + color: white; font-family: 'Segoe UI', Arial, sans-serif; + backdrop-filter: blur(10px); + resize: both; overflow: hidden; + `; + + container.innerHTML = ` +
+

+ WPlace Autonomous Repair Tool +

+
+ + +
+
+ +
+
+
+ ${Utils.t('autonomousModeActive')} +
+
+ Charges: 0/1 (cooldown: 31s) +
+
+ Token: Initializing... +
+
+ System: AUTONOMOUS | Token: GENERATOR | Auto-refresh: ON +
+
+ +
+ + + + + +
+ +
+
+ +
+ +
+ + + seconds +
+ +
+ + +
+ +
+
Image: Not loaded
+
Position: Not set
+
Colors: 0 available
+
+
+ +
+
+

📋 ${Utils.t('debug')} (Enhanced System)

+ +
+ +
+
[Ready] WPlace Autonomous Repair Tool v3.0
+
[Info] NEW Turnstile implementation active
+
[Info] Enhanced token management with preloading
+
[Info] ${Utils.t('tokenSystemReady')}
+
+
+
+ `; + + document.body.appendChild(container); + makeWindowDraggable(); + setupEventListeners(); + + // Start typewriter effect + setTimeout(() => { + createTypewriterTitle(); + }, 1000); + + // Start autonomous systems + initializeAutonomousSystems(); + } + + function initializeAutonomousSystems() { + // Update charges periodically + updateCharges(); + setInterval(updateCharges, 10000); + + // Update token status + updateTokenStatus(); + setInterval(updateTokenStatus, 5000); + + // Update system status + updateSystemStatus(); + setInterval(updateSystemStatus, 15000); + + // Initialize token generator + initializeTokenGenerator(); + } + + function setupEventListeners() { + // Window controls + document.getElementById('minimizeBtn').addEventListener('click', () => { + const content = document.getElementById('window-content'); + state.windowMinimized = !state.windowMinimized; + + if (state.windowMinimized) { + content.style.display = 'none'; + document.getElementById('minimizeBtn').textContent = '+'; + } else { + content.style.display = 'block'; + document.getElementById('minimizeBtn').textContent = '−'; + } + }); + + document.getElementById('closeBtn').addEventListener('click', () => { + const container = document.getElementById('wplace-repair-tool'); + if (container) { + container.style.transition = 'opacity 0.3s ease, transform 0.3s ease'; + container.style.opacity = '0'; + container.style.transform = 'scale(0.8)'; + setTimeout(() => { + container.remove(); + cleanup(); + }, 300); + } + }); + + // Load file + document.getElementById('loadFileBtn').addEventListener('click', async () => { + try { + Utils.addDebugLog('Loading progress file...', 'info'); + const data = await Utils.createFileUploader(); + + if (!data || !data.state || !data.imageData) { + throw new Error(Utils.t('invalidFile')); + } + + const migratedData = migrateProgressData(data); + + Object.assign(state, migratedData.state); + state.imageData = { + ...migratedData.imageData, + pixels: new Uint8ClampedArray(migratedData.imageData.pixels), + }; + + state.imageLoaded = true; + document.getElementById('repairBtn').disabled = false; + + // Update info displays + document.getElementById('imageInfo').textContent = `${state.imageData.width}x${state.imageData.height}`; + document.getElementById('positionInfo').textContent = state.startPosition ? + `(${state.startPosition.x}, ${state.startPosition.y}) in region (${state.region.x}, ${state.region.y})` : 'Not set'; + document.getElementById('colorsInfo').textContent = `${state.availableColors?.length || 0} available`; + + updateStatus(Utils.t('fileLoaded')); + Utils.addDebugLog(`Loaded image: ${state.imageData.width}x${state.imageData.height}`, 'success'); + Utils.addDebugLog(`Position: (${state.startPosition?.x}, ${state.startPosition?.y})`, 'info'); + Utils.addDebugLog(`Region: (${state.region?.x}, ${state.region?.y})`, 'info'); + Utils.addDebugLog(`Available colors: ${state.availableColors?.length || 0}`, 'info'); + + if (migratedData.state.paintWhitePixels !== undefined) { + state.paintWhitePixels = migratedData.state.paintWhitePixels; + Utils.addDebugLog(`Paint white pixels: ${state.paintWhitePixels}`, 'info'); + } + if (migratedData.state.paintTransparentPixels !== undefined) { + state.paintTransparentPixels = migratedData.state.paintTransparentPixels; + Utils.addDebugLog(`Paint transparent pixels: ${state.paintTransparentPixels}`, 'info'); + } + + Utils.showAlert(Utils.t('fileLoaded'), 'success'); + + // If autonomous mode and auto repair is enabled, start it + if (state.autonomousMode && document.getElementById('autoRepairEnabled').checked) { + Utils.addDebugLog('Autonomous mode: starting auto repair after file load', 'info'); + setTimeout(() => { + state.autoRepairEnabled = true; + startAutoRepair(); + }, 5000); + } + + } catch (error) { + Utils.addDebugLog(`Load error: ${error.message}`, 'error'); + Utils.showAlert(error.message, 'error'); + } + }); + + // Generate Token button + document.getElementById('generateTokenBtn').addEventListener('click', async () => { + const btn = document.getElementById('generateTokenBtn'); + const originalText = btn.textContent; + + try { + btn.disabled = true; + btn.textContent = '🔄 Generating...'; + btn.style.background = 'linear-gradient(135deg, #ffa726 0%, #ff9800 100%)'; + + Utils.addDebugLog('Manual token generation requested', 'info'); + + const token = await ensureToken(true); + + if (token) { + Utils.addDebugLog('Token generation succeeded!', 'success'); + Utils.showAlert('Token generated successfully!', 'success'); + updateTokenStatus(); + } else { + throw new Error('Token generation failed'); + } + } catch (error) { + Utils.addDebugLog(`Token generation failed: ${error.message}`, 'error'); + Utils.showAlert('Token generation failed', 'error'); + playNotificationSound(); + } finally { + btn.disabled = false; + btn.textContent = originalText; + btn.style.background = 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)'; + } + }); + + // Manual repair + let repairRunning = false; + document.getElementById('repairBtn').addEventListener('click', async () => { + if (!state.imageLoaded) { + Utils.showAlert('Please load a progress file first', 'warning'); + return; + } + + if (repairRunning) { + state.stopFlag = true; + Utils.addDebugLog('Manual repair stop requested', 'warning'); + return; + } + + repairRunning = true; + state.running = true; + state.stopFlag = false; + const repairBtn = document.getElementById('repairBtn'); + repairBtn.textContent = '⏸️ Stop Repair'; + repairBtn.style.background = 'linear-gradient(135deg, #ff6b35 0%, #f7931e 100%)'; + + try { + Utils.addDebugLog('Starting manual repair with enhanced autonomous system...', 'info'); + + if (!isTokenValid()) { + Utils.addDebugLog('No valid token, generating...', 'info'); + await ensureToken(true); + } + + const damagedPixels = await scanForDamage(); + + if (damagedPixels.length > 0) { + updateStatus(Utils.t('damageDetected', { count: damagedPixels.length })); + showAttackNotification('attack', damagedPixels.length); + + const repairedCount = await repairDamagedPixels(damagedPixels); + + if (repairedCount === damagedPixels.length) { + showAttackNotification('repaired'); + } + } else { + updateStatus(Utils.t('noDamageDetected')); + showPeacefulState(); + } + } catch (error) { + Utils.addDebugLog(`Repair error: ${error.message}`, 'error'); + Utils.showAlert('Repair failed', 'error'); + } finally { + repairRunning = false; + state.running = false; + state.stopFlag = false; + repairBtn.textContent = '🔧 ' + Utils.t('repairPixels'); + repairBtn.style.background = 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)'; + } + }); + + // Auto repair toggle + document.getElementById('autoRepairEnabled').addEventListener('change', (e) => { + state.autoRepairEnabled = e.target.checked; + + if (state.autoRepairEnabled) { + if (!state.imageLoaded) { + Utils.showAlert('Please load a progress file first', 'warning'); + e.target.checked = false; + state.autoRepairEnabled = false; + return; + } + startAutoRepair(); + } else { + stopAutoRepair(); + } + }); + + // Repair interval change + document.getElementById('repairInterval').addEventListener('change', (e) => { + const value = parseInt(e.target.value) || 30; + state.autoRepairInterval = Math.max(10, Math.min(3600, value)); + e.target.value = state.autoRepairInterval; + + Utils.addDebugLog(`Repair interval changed to ${state.autoRepairInterval} seconds`, 'info'); + + if (state.autoRepairEnabled && state.imageLoaded) { + Utils.addDebugLog('Restarting auto repair with new interval', 'info'); + startAutoRepair(); + } + }); + + // Auto token refresh toggle + document.getElementById('autoTokenRefresh').addEventListener('change', (e) => { + state.autoTokenRefresh = e.target.checked; + Utils.addDebugLog(`Auto token refresh ${state.autoTokenRefresh ? 'enabled' : 'disabled'}`, 'info'); + updateSystemStatus(); + + if (state.autoTokenRefresh && state.autonomousMode && !isTokenValid()) { + ensureToken(); + } + }); + + // Token source selection + document.getElementById('tokenSourceSelect').addEventListener('change', (e) => { + state.tokenSource = e.target.value; + Utils.addDebugLog(`Token source changed to: ${state.tokenSource}`, 'info'); + updateSystemStatus(); + + // Invalidate current token to force new generation with new method + if (isTokenValid()) { + Utils.addDebugLog('Invalidating current token due to source change', 'info'); + invalidateToken(); + } + }); + + // Clear debug + document.getElementById('clearDebugBtn').addEventListener('click', () => { + state.debugLogs = []; + updateDebugConsole(); + Utils.addDebugLog('Debug console cleared', 'info'); + Utils.addDebugLog('Enhanced WPlace Autonomous Repair Tool ready', 'info'); + }); + } + + // Enhanced data migration helper + function migrateProgressData(data) { + if (!data.version || data.version < '2.0') { + Utils.addDebugLog('Migrating old progress data format...', 'info'); + + if (!data.state) data.state = {}; + if (!data.imageData) data.imageData = {}; + + data.state.paintWhitePixels = data.state.paintWhitePixels ?? true; + data.state.paintTransparentPixels = data.state.paintTransparentPixels ?? false; + + data.version = '2.0'; + } + + // Additional migrations for autonomous features + if (data.version === '2.0') { + data.state.autonomousMode = data.state.autonomousMode ?? CONFIG.AUTONOMOUS_MODE; + data.state.autoTokenRefresh = data.state.autoTokenRefresh ?? CONFIG.AUTO_TOKEN_REFRESH; + data.state.tokenSource = data.state.tokenSource ?? CONFIG.TOKEN_SOURCE; + data.version = '3.0'; + Utils.addDebugLog('Migrated to autonomous features v3.0', 'info'); + } + + return data; + } + + // Main initialization function + async function initialize() { + Utils.addDebugLog('Starting WPlace Autonomous Repair Tool v3.0...', 'info'); + + // Create UI first + createUI(); + + Utils.addDebugLog('NEW Enhanced Features Initialized:', 'info'); + Utils.addDebugLog('• NEW Turnstile implementation with widget reuse', 'info'); + Utils.addDebugLog('• Enhanced sitekey detection from multiple sources', 'info'); + Utils.addDebugLog('• Sophisticated token management with preloading', 'info'); + Utils.addDebugLog('• Autonomous operation with smart retry logic', 'info'); + Utils.addDebugLog('• Enhanced tile interception and caching', 'info'); + Utils.addDebugLog('• Adaptive repair algorithms with failure handling', 'info'); + Utils.addDebugLog('• Visual notifications and audio alerts', 'info'); + Utils.addDebugLog('• Draggable window with minimize/close controls', 'info'); + + if (state.autonomousMode) { + Utils.addDebugLog('AUTONOMOUS MODE ACTIVE - Tool will operate independently', 'success'); + Utils.addDebugLog('Instructions for autonomous operation:', 'info'); + Utils.addDebugLog('1. Wait for initial setup to complete', 'info'); + Utils.addDebugLog('2. Load a progress file using "Load Progress File"', 'info'); + Utils.addDebugLog('3. Enable "Auto Repair" to start autonomous monitoring', 'info'); + Utils.addDebugLog('4. Tool will automatically scan and repair damage', 'info'); + Utils.addDebugLog('5. Tokens will be generated and refreshed automatically', 'info'); + Utils.addDebugLog('6. Monitor debug console for autonomous operation logs', 'info'); + } else { + Utils.addDebugLog('Manual mode active - use controls for manual operation', 'info'); + } + + // Show autonomous mode status + updateStatus(state.autonomousMode ? Utils.t('autonomousModeActive') : 'Manual mode ready - Load file and enable auto repair to start'); + + Utils.addDebugLog('WPlace Autonomous Repair Tool ready for operation', 'success'); + } + + // Enhanced cleanup function + function cleanup() { + Utils.addDebugLog('Cleaning up autonomous systems...', 'info'); + + // Clear all timers + if (state.autoRepairTimer) { + clearInterval(state.autoRepairTimer); + state.autoRepairTimer = null; + } + + if (state.tokenRetryTimer) { + clearTimeout(state.tokenRetryTimer); + state.tokenRetryTimer = null; + } + + if (state.tokenPreloadTimer) { + clearTimeout(state.tokenPreloadTimer); + state.tokenPreloadTimer = null; + } + + // Cleanup Turnstile + Utils.cleanupTurnstile(); + + // Set stop flag + state.stopFlag = true; + + Utils.addDebugLog('Cleanup completed', 'info'); + } + + // Add cleanup on page unload + window.addEventListener('beforeunload', cleanup); + + // Handle page visibility changes for autonomous mode + document.addEventListener('visibilitychange', () => { + if (state.autonomousMode) { + if (document.hidden) { + Utils.addDebugLog('Page hidden - autonomous systems continue running', 'info'); + } else { + Utils.addDebugLog('Page visible - autonomous systems active', 'info'); + // Refresh status when page becomes visible + updateCharges(); + updateTokenStatus(); + } + } + }); + + // Enhanced error handling for autonomous mode + window.addEventListener('error', (event) => { + if (state.autonomousMode) { + Utils.addDebugLog(`Global error in autonomous mode: ${event.error?.message || 'Unknown error'}`, 'error'); + } + }); + + window.addEventListener('unhandledrejection', (event) => { + if (state.autonomousMode) { + Utils.addDebugLog(`Unhandled promise rejection in autonomous mode: ${event.reason}`, 'error'); + } + }); + + // Start the enhanced autonomous application + initialize().catch(error => { + console.error('Failed to initialize autonomous repair tool:', error); + Utils.addDebugLog(`Initialization failed: ${error.message}`, 'error'); + }); + +})(); diff --git a/themes/classic-light.css b/themes/classic-light.css new file mode 100644 index 00000000..b8079fc3 --- /dev/null +++ b/themes/classic-light.css @@ -0,0 +1,598 @@ +/* WPlace Auto-Image Bot - Classic Light Theme */ + +/* Clean, bright theme based on classic design with light backgrounds */ + +.wplace-theme-classic-light { + /* === CORE COLORS === */ + --wplace-primary: #fff; /* Clean white */ + --wplace-secondary: #f8f9fa; /* Light gray */ + --wplace-accent: #e9ecef; /* Lighter gray */ + --wplace-text: #212529; /* Dark text */ + --wplace-highlight: #6f42c1; /* Purple highlight */ + --wplace-success: #28a745; /* Bootstrap green */ + --wplace-error: #dc3545; /* Bootstrap red */ + --wplace-warning: #ffc107; /* Bootstrap yellow */ + + /* === UI PROPERTIES === */ + --wplace-radius: 8px; + --wplace-btn-radius: 12px; + --wplace-border-style: solid; + --wplace-border-width: 1px; + --wplace-border-color: #415a77; + --wplace-shadow: 0 8px 32px rgb(0 0 0 / 15%), 0 0 0 1px rgb(0 0 0 / 8%); + --wplace-backdrop: blur(10px); + --wplace-font: 'Segoe UI', roboto, sans-serif; + + /* === EXTENDED COLOR PALETTE === */ + + /* Icon colors */ + --wplace-icon-primary: #4facfe; /* Light blue */ + --wplace-icon-secondary: #00f2fe; /* Cyan */ + --wplace-icon-palette: #f093fb; /* Pink */ + + /* Additional UI colors */ + --wplace-danger: #dc3545; /* Bootstrap red */ + --wplace-danger-dark: #c82333; /* Darker red */ + --wplace-muted-text: rgb(33 37 41 / 70%); + --wplace-highlight-secondary: #d3a4ff; + + /* Text variants */ + --wplace-text-secondary: rgb(33 37 41 / 90%); + --wplace-text-muted: rgb(33 37 41 / 70%); + --wplace-text-dim: rgb(33 37 41 / 60%); + --wplace-text-faded: rgb(33 37 41 / 80%); + + /* Background variants */ + --wplace-bg-input: #f3f3f3; /* neutral-100 */ + --wplace-bg-subtle: #f3f3f3; /* neutral-100 — hover */ + --wplace-bg-faint: #e0e0e0; /* neutral-200 */ + --wplace-bg-ghost: #d1d1d1; /* neutral-300 */ + --wplace-bg-whisper: #e0e0e0; /* can be used as whisper on hover */ + + /* Border variants */ + --wplace-border-subtle: #d1d1d1; /* neutral-300 */ + --wplace-border-faint: #e0e0e0; /* neutral-200 */ + --wplace-border-ghost: #e0e0e0; /* neutral-200 */ + --wplace-border-ultra-faint: #e0e0e0; /* neutral-200 — very faint */ + + /* Shadow variants */ + --wplace-shadow-drag: 0 12px 40px rgb(0 0 0 / 20%), 0 0 0 2px rgb(111 66 193 / 30%); + --wplace-shadow-notification: 0 4px 12px rgb(0 0 0 / 15%); + --wplace-shadow-slider-thumb: 0 3px 6px rgb(0 0 0 / 20%), 0 0 0 2px var(--wplace-icon-primary); + --wplace-shadow-slider-hover: 0 4px 8px rgb(0 0 0 / 25%), 0 0 0 3px var(--wplace-icon-primary); + + /* Animation colors */ + --wplace-pulse-start: rgb(40 167 69 / 70%); + --wplace-pulse-mid: rgb(40 167 69 / 0%); + --wplace-pulse-end: rgb(40 167 69 / 0%); + + /* Slider colors */ + --wplace-slider-thumb-bg: #6f42c1; + --wplace-slider-track-bg: linear-gradient(to right, #4facfe 0%, #00f2fe 100%); +} + +/* === COMPONENT STYLING === */ + +/* Main container with clean light styling */ +.wplace-theme-classic-light #wplace-image-bot-container { + background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%) !important; + color: var(--wplace-text) !important; + border: 1px solid rgb(0 0 0 / 15%) !important; + border-radius: 12px !important; + box-shadow: 0 8px 32px rgb(0 0 0 / 15%), 0 0 0 1px rgb(0 0 0 / 8%) !important; + font-family: 'Segoe UI', Roboto, sans-serif !important; + backdrop-filter: blur(10px) !important; +} + + +/* Stats container with proper contrast */ +.wplace-theme-classic-light #wplace-stats-container { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important; + border: 1px solid rgb(0 0 0 / 20%) !important; + border-radius: 12px !important; + box-shadow: 0 8px 32px rgb(0 0 0 / 15%) !important; + font-family: 'Segoe UI', Roboto, sans-serif !important; + color: var(--wplace-text) !important; + position: fixed !important; + overflow: hidden !important; + z-index: 9998 !important; +} + +/* Headers with light gradient */ +.wplace-theme-classic-light .wplace-header { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important; + color: var(--wplace-highlight) !important; + border-bottom: 1px solid rgb(0 0 0 / 15%) !important; + text-shadow: none !important; +} + +/* Stats header styling */ +.wplace-theme-classic-light #wplace-stats-container .wplace-header { + background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%) !important; + color: var(--wplace-text) !important; + border-bottom: 1px solid rgb(0 0 0 / 20%) !important; + text-shadow: none !important; + font-weight: 600 !important; +} + +/* Stats title specific styling */ +.wplace-theme-classic-light #wplace-stats-container .wplace-header .wplace-stats-title { + color: var(--wplace-text) !important; + text-shadow: none !important; +} + +/* Comprehensive text and element styling for light theme - scoped to bot containers only */ + +.wplace-theme-classic-light .wplace-status, +.wplace-theme-classic-light .wplace-stats, +.wplace-theme-classic-light .wplace-section, +.wplace-theme-classic-light .wplace-controls, +.wplace-theme-classic-light .wplace-data-management, +.wplace-theme-classic-light .wplace-cooldown-settings { + color: var(--wplace-text) !important; +} + +/* Buttons with light styling */ +.wplace-theme-classic-light .wplace-btn { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important; + border: 1px solid rgb(0 0 0 / 20%) !important; + border-radius: 12px !important; + color: var(--wplace-text) !important; + font-family: 'Segoe UI', Roboto, sans-serif !important; + font-weight: 500 !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !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 rgb(0 0 0 / 20%) !important; + color: #212529 !important; + font-weight: 500 !important; +} + +.wplace-theme-classic-light .wplace-btn:hover:not(:disabled) { + background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%) !important; + box-shadow: 0 4px 12px rgb(0 0 0 / 15%) !important; + transform: translateY(-1px) !important; +} + +.wplace-theme-classic-light .wplace-data-management .wplace-btn:hover:not(:disabled) { + background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%) !important; + box-shadow: 0 4px 12px rgb(0 0 0 / 15%) !important; +} + +/* Settings dialog */ +.wplace-theme-classic-light #wplace-settings-container { + background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%) !important; + border: 1px solid rgb(0 0 0 / 15%) !important; + border-radius: 12px !important; + box-shadow: 0 16px 48px rgb(0 0 0 / 20%), 0 0 0 1px rgb(0 0 0 / 8%) !important; + font-family: 'Segoe UI', Roboto, sans-serif !important; + backdrop-filter: blur(10px) !important; + color: var(--wplace-text) !important; +} + +/* Settings dialog text elements */ +.wplace-theme-classic-light .wplace-stat-colors-grid * { + color: var(--wplace-text) !important; + font-weight: 500 !important; +} + +.wplace-theme-classic-light #wplace-settings-container * { + color: var(--wplace-text) !important; +} + +.wplace-theme-classic-light #wplace-settings-container .wplace-settings-section-wrapper * { + color: var(--wplace-text) !important; +} + +/* Color palette text labels */ +.wplace-theme-classic-light .wplace-color-label { + color: var(--wplace-text) !important; + font-weight: 500 !important; + text-shadow: none !important; +} + +.wplace-theme-classic-light .wplace-color-name { + color: var(--wplace-text) !important; + font-weight: 500 !important; +} + +.wplace-theme-classic-light .wplace-color-item-name { + color: #000 !important; + font-weight: 600 !important; + text-shadow: none !important; +} + +/* Resize dialog */ +.wplace-theme-classic-light .resize-container { + background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%) !important; + border: 1px solid rgb(0 0 0 / 15%) !important; + border-radius: 12px !important; + box-shadow: 0 16px 48px rgb(0 0 0 / 20%), 0 0 0 1px rgb(0 0 0 / 8%) !important; + font-family: 'Segoe UI', Roboto, sans-serif !important; + backdrop-filter: blur(10px) !important; +} + +/* Settings header */ +.wplace-theme-classic-light .wplace-settings-header { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important; + border-bottom: 1px solid rgb(0 0 0 / 15%) !important; +} + +.wplace-theme-classic-light .wplace-settings-title { + color: var(--wplace-text) !important; + text-shadow: none !important; +} + +.wplace-theme-classic-light .wplace-settings-close-btn { + background: rgb(0 0 0 / 5%) !important; + border: 1px solid rgb(0 0 0 / 20%) !important; + border-radius: 50% !important; + color: var(--wplace-text) !important; + transition: all 0.3s ease !important; +} + +.wplace-theme-classic-light .wplace-settings-close-btn:hover { + background: rgb(220 53 69 / 10%) !important; + border-color: var(--wplace-error) !important; + box-shadow: 0 0 12px rgb(220 53 69 / 30%) !important; +} + +/* Section titles */ +.wplace-theme-classic-light .wplace-section-title { + color: var(--wplace-highlight) !important; + text-shadow: none !important; + font-weight: 600 !important; +} + +/* Button variants with classic light colors */ +.wplace-theme-classic-light .wplace-btn-start { + background: linear-gradient(135deg, #28a745 0%, #20c997 100%) !important; + color: white !important; + font-weight: 600 !important; +} + +.wplace-theme-classic-light .wplace-btn-start:hover:not(:disabled) { + box-shadow: 0 4px 12px rgb(40 167 69 / 40%) !important; +} + +.wplace-theme-classic-light .wplace-btn-stop { + background: linear-gradient(135deg, #dc3545 0%, #c82333 100%) !important; + color: white !important; + font-weight: 600 !important; +} + +.wplace-theme-classic-light .wplace-btn-stop:hover:not(:disabled) { + box-shadow: 0 4px 12px rgb(220 53 69 / 40%) !important; +} + +.wplace-theme-classic-light .wplace-btn-upload { + background: rgb(111 66 193 / 10%) !important; + border: 2px dashed var(--wplace-highlight) !important; + color: var(--wplace-highlight) !important; +} + +.wplace-theme-classic-light .wplace-btn-upload:hover:not(:disabled) { + background: rgb(111 66 193 / 15%) !important; + box-shadow: 0 4px 12px rgb(111 66 193 / 20%) !important; +} + +/* Progress bars with clean light styling */ +.wplace-theme-classic-light .wplace-progress { + background: rgb(0 0 0 / 10%) !important; + border: 1px solid rgb(0 0 0 / 15%) !important; + border-radius: 12px !important; +} + +.wplace-theme-classic-light .wplace-progress-bar { + background: linear-gradient(135deg, #6f42c1 0%, #9370db 100%) !important; + box-shadow: none !important; +} + +.wplace-theme-classic-light .wplace-progress-bar::after { + background: linear-gradient(90deg, transparent, rgb(255 255 255 / 30%), transparent) !important; +} + +/* Status indicators with light colors */ +.wplace-theme-classic-light .status-success { + background: rgb(40 167 69 / 10%) !important; + border-color: var(--wplace-success) !important; + color: var(--wplace-success) !important; + box-shadow: 0 0 15px rgb(40 167 69 / 20%) !important; + text-shadow: none !important; +} + +.wplace-theme-classic-light .status-error { + background: rgb(220 53 69 / 10%) !important; + border-color: var(--wplace-error) !important; + color: var(--wplace-error) !important; + box-shadow: 0 0 15px rgb(220 53 69 / 20%) !important; + text-shadow: none !important; +} + +.wplace-theme-classic-light .status-default { + background: rgb(111 66 193 / 10%) !important; + border-color: var(--wplace-highlight) !important; + color: var(--wplace-highlight) !important; + text-shadow: none !important; +} + +.wplace-theme-classic-light .wplace-stat-label { + color: var(--wplace-text) !important; + text-shadow: none !important; + font-weight: 500 !important; +} + +.wplace-theme-classic-light .wplace-stat-value { + color: var(--wplace-highlight) !important; + text-shadow: none !important; + font-weight: 600 !important; +} + +/* Sections with light styling */ +.wplace-theme-classic-light .wplace-section { + background: rgb(0 0 0 / 3%) !important; + border: 1px solid rgb(0 0 0 / 10%) !important; + border-radius: 12px !important; +} + +.wplace-theme-classic-light .wplace-status-section { + background: rgb(0 0 0 / 3%) !important; + border: 1px solid rgb(0 0 0 / 10%) !important; + border-radius: 12px !important; +} + +.wplace-theme-classic-light .wplace-settings-section-wrapper { + background: rgb(0 0 0 / 3%) !important; + border: 1px solid rgb(0 0 0 / 10%) !important; + border-radius: 12px !important; +} + +/* Form controls with light styling */ +.wplace-theme-classic-light .wplace-settings-select { + background: #fff !important; + border: 1px solid rgb(0 0 0 / 20%) !important; + border-radius: 8px !important; + color: var(--wplace-text) !important; + font-family: 'Segoe UI', Roboto, sans-serif !important; + box-shadow: 0 2px 8px rgb(0 0 0 / 10%) !important; +} + +.wplace-theme-classic-light .wplace-settings-select:focus { + border-color: var(--wplace-highlight) !important; + box-shadow: 0 0 0 2px rgb(111 66 193 / 30%) !important; +} + +/* Dropdown menu options */ +.wplace-theme-classic-light .wplace-settings-select option { + background: #fff !important; + color: var(--wplace-text) !important; +} + +.wplace-theme-classic-light .wplace-settings-option { + background: #fff !important; + color: var(--wplace-text) !important; +} + +/* Sliders with classic gradient */ +.wplace-theme-classic-light .wplace-speed-slider { + background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%) !important; + border-radius: 4px !important; +} + +.wplace-theme-classic-light .wplace-overlay-opacity-slider { + background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%) !important; + border-radius: 4px !important; +} + +.wplace-theme-classic-light .wplace-slider { + background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%) !important; + 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 rgb(0 0 0 / 20%); + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgb(0 0 0 / 20%); +} + +.wplace-theme-classic-light .wplace-speed-value { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%) !important; + border-radius: 8px !important; + color: white !important; + font-weight: 600 !important; + box-shadow: 0 3px 10px rgb(79 172 254 / 30%) !important; + border: 1px solid rgb(0 0 0 / 10%) !important; +} + +/* Settings labels */ +.wplace-theme-classic-light .wplace-settings-section-label { + color: var(--wplace-highlight) !important; + text-shadow: none !important; + font-family: 'Segoe UI', Roboto, sans-serif !important; + font-weight: 600 !important; +} + +/* Icon colors for classic light theme */ +.wplace-theme-classic-light .wplace-icon-key { color: #4facfe; } +.wplace-theme-classic-light .wplace-icon-robot { color: #4facfe; } +.wplace-theme-classic-light .wplace-icon-speed { color: #4facfe; } +.wplace-theme-classic-light .wplace-icon-bell { color: #ffc107; } +.wplace-theme-classic-light .wplace-icon-palette { color: #f093fb; } +.wplace-theme-classic-light .wplace-icon-globe { color: #ffeaa7; } +.wplace-theme-classic-light .wplace-icon-paint { color: #4facfe; } +.wplace-theme-classic-light .wplace-icon-eye { color: #6f42c1; } + +/* Clean light theme animations */ +@keyframes light-shimmer { + 0% { + transform: translateX(-100%); + } + + 100% { + transform: translateX(200%); + } +} + +/* Turnstile/CF checkbox overlay - CRITICAL FIX */ +.wplace-theme-classic-light .wplace-turnstile-overlay { + background: rgb(255 255 255 / 98%) !important; + border-radius: 12px !important; + box-shadow: 0 8px 32px rgb(0 0 0 / 30%) !important; + backdrop-filter: blur(10px) !important; + border: 1px solid rgb(0 0 0 / 20%) !important; + color: var(--wplace-text) !important; + font-family: 'Segoe UI', Roboto, sans-serif !important; +} + +.wplace-theme-classic-light .wplace-turnstile-title { + color: var(--wplace-text) !important; +} + +.wplace-theme-classic-light .wplace-turnstile-hide-btn { + color: var(--wplace-text) !important; + border: 1px solid rgb(0 0 0 / 20%) !important; + border-radius: 6px !important; + background: rgb(0 0 0 / 5%) !important; +} + +.wplace-theme-classic-light .wplace-turnstile-hide-btn:hover { + background: rgb(0 0 0 / 10%) !important; +} + +/* Dual control layout styling for classic light theme */ +.wplace-theme-classic-light .wplace-dual-control-compact { + display: flex; + flex-direction: row; + align-items: center; + gap: 12px; + margin: 8px 0; + flex-wrap: wrap; +} + +.wplace-theme-classic-light .wplace-slider-container-compact { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + min-width: 160px; +} + +.wplace-theme-classic-light .wplace-speed-slider-container-compact { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + min-width: 160px; +} + +.wplace-theme-classic-light .wplace-input-group-compact { + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; +} + +.wplace-theme-classic-light .wplace-input-btn-compact { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + color: white; + border: none; + border-radius: 4px; + width: 22px; + height: 22px; + cursor: pointer; + font-weight: bold; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); +} + +.wplace-theme-classic-light .wplace-input-btn-compact:hover { + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0,0,0,0.3); + background: linear-gradient(135deg, #00f2fe 0%, #4facfe 100%); +} + +.wplace-theme-classic-light .wplace-input-btn-compact:active { + transform: translateY(0); + box-shadow: 0 2px 4px rgba(0,0,0,0.2); +} + +.wplace-theme-classic-light .wplace-number-input-compact { + background: #fff; + border: 1px solid rgb(0 0 0 / 20%); + border-radius: 4px; + color: var(--wplace-text); + padding: 4px 8px; + font-size: 12px; + width: 50px; + text-align: center; + transition: all 0.2s ease; + height: 22px; +} + +.wplace-theme-classic-light .wplace-number-input-compact:focus { + outline: none; + border-color: #4facfe; + box-shadow: 0 0 0 3px rgba(79, 172, 254, 0.3); + background: #f8f9fa; +} + +.wplace-theme-classic-light .wplace-input-label-compact { + color: var(--wplace-text); + font-size: 11px; + margin-left: 4px; + white-space: nowrap; +} + +.wplace-theme-classic-light .wplace-batch-size-header { + margin-bottom: 8px; +} + +.wplace-theme-classic-light .wplace-batch-size-label { + color: var(--wplace-highlight); + font-weight: 500; + font-size: 13px; +} + +/* Cooldown control styling for classic light theme */ +.wplace-theme-classic-light .wplace-cooldown-control { + margin: 12px 0; +} + +.wplace-theme-classic-light .wplace-cooldown-control label { + color: var(--wplace-highlight); + font-weight: 500; + font-size: 13px; + margin-bottom: 8px; + display: block; +} + +/* Color swatches with light styling */ +.wplace-theme-classic-light .wplace-color-swatch { + border: 1px solid rgb(0 0 0 / 20%) !important; + border-radius: 4px !important; + box-shadow: 0 2px 8px rgb(0 0 0 / 10%) !important; +} + +.wplace-theme-classic-light .wplace-color-swatch:hover { + box-shadow: 0 4px 16px rgb(111 66 193 / 30%) !important; + transform: translateY(-1px); +} + +.wplace-theme-classic-light .wplace-stat-colors-grid { + background: rgb(0 0 0 / 5%) !important; + border-radius: 8px !important; + border: 1px solid rgb(0 0 0 / 10%) !important; +} diff --git a/themes/classic.css b/themes/classic.css new file mode 100644 index 00000000..857ce6e6 --- /dev/null +++ b/themes/classic.css @@ -0,0 +1,725 @@ +/* WPlace Auto-Image Bot - Classic Theme */ +/* Clean, modern UI with gradients and subtle effects */ + +.wplace-theme-classic { + --wplace-primary: linear-gradient(135deg, #000 0%, #1a1a1a 100%); + --wplace-secondary: linear-gradient(135deg, #111 0%, #2a2a2a 100%); + --wplace-accent: #222; + --wplace-text: #fff; + --wplace-highlight: #775ce3; + --wplace-success: #0f0; + --wplace-error: #f00; + --wplace-warning: #fa0; + --wplace-radius: 12px; + --wplace-btn-radius: 16px; + --wplace-shadow: 0 8px 32px rgb(0 0 0 / 60%), 0 0 0 1px rgb(255 255 255 / 10%); + --wplace-backdrop: blur(10px); + --wplace-font: 'Segoe UI', roboto, sans-serif; + --wplace-scanline: 0; + --wplace-pixel-blink: 0; + + /* Icon colors */ + --wplace-icon-primary: #4facfe; + --wplace-icon-secondary: #00f2fe; + --wplace-icon-palette: #f093fb; + + /* Additional UI colors */ + --wplace-danger: #ff6a6a; + --wplace-danger-dark: #ff4757; + --wplace-muted-text: #fffB; + --wplace-highlight-secondary: #d3a4ff; + + /* Slider colors */ + --wplace-slider-track-bg: linear-gradient(to right, #4facfe 0%, #00f2fe 100%); +} + +/* Classic theme container styling */ +:root #wplace-image-bot-container, +.wplace-theme-classic #wplace-image-bot-container { + background: var(--wplace-primary); + color: var(--wplace-text); + border-radius: var(--wplace-radius); + box-shadow: var(--wplace-shadow); + font-family: var(--wplace-font); + backdrop-filter: var(--wplace-backdrop); + border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color); +} + +:root #wplace-stats-container, +.wplace-theme-classic #wplace-stats-container { + background: var(--wplace-primary); + color: var(--wplace-text); + border-radius: var(--wplace-radius); + box-shadow: var(--wplace-shadow); + font-family: var(--wplace-font); + backdrop-filter: var(--wplace-backdrop); + border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color); +} + +:root #wplace-settings-container, +.wplace-theme-classic #wplace-settings-container { + background: var(--wplace-primary); + color: var(--wplace-text); + border-radius: var(--wplace-radius); + box-shadow: var(--wplace-shadow); + font-family: var(--wplace-font); + backdrop-filter: var(--wplace-backdrop); + border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color); +} + +:root .wplace-header, +.wplace-theme-classic .wplace-header { + background: var(--wplace-secondary); + color: var(--wplace-highlight); + text-shadow: 0 1px 2px rgb(0 0 0 / 50%); +} + +:root .wplace-section-title, +.wplace-theme-classic .wplace-section-title { + color: var(--wplace-highlight); +} + +:root .wplace-stat-value, +.wplace-theme-classic .wplace-stat-value { + color: var(--wplace-highlight); +} + +:root .wplace-status-section, +.wplace-theme-classic .wplace-status-section { + border-radius: var(--wplace-radius); + background: rgb(255 255 255 / 3%); + border: 1px solid rgb(255 255 255 / 10%); +} + +:root .wplace-section, +.wplace-theme-classic .wplace-section { + border-radius: var(--wplace-radius); + background: rgb(255 255 255 / 3%); +} + +:root .wplace-stats, +.wplace-theme-classic .wplace-stats { + border-radius: var(--wplace-radius); + background: rgb(255 255 255 / 3%); + border: 1px solid rgb(255 255 255 / 10%); +} + +:root .wplace-status, +.wplace-theme-classic .wplace-status { + border-radius: var(--wplace-radius); +} + +:root .wplace-alert-base, +.wplace-theme-classic .wplace-alert-base { + border-radius: var(--wplace-radius); + font-family: var(--wplace-font); +} + +:root .wplace-settings-container-base, +.wplace-theme-classic .wplace-settings-container-base { + font-family: var(--wplace-font); + border-radius: var(--wplace-radius); + background: var(--wplace-primary); + color: var(--wplace-text); + box-shadow: var(--wplace-shadow); + backdrop-filter: var(--wplace-backdrop); +} + +/* Button styling for classic theme */ +:root .wplace-btn, +.wplace-theme-classic .wplace-btn { + background: linear-gradient(135deg, #222 0%, #4a4a4a 100%); + border: 1px solid rgb(255 255 255 / 10%); + color: var(--wplace-text); + border-radius: var(--wplace-btn-radius); + font-family: var(--wplace-font); +} + +:root .wplace-btn::before, +.wplace-theme-classic .wplace-btn::before { + background: linear-gradient(90deg, transparent, rgb(255 255 255 / 10%), transparent); +} + +:root .wplace-btn:disabled, +.wplace-theme-classic .wplace-btn:disabled { + box-shadow: none !important; +} + +:root .wplace-btn:hover:not(:disabled), +.wplace-theme-classic .wplace-btn:hover:not(:disabled) { + box-shadow: 0 4px 12px rgb(0 0 0 / 40%); +} + +:root .wplace-btn-overlay.active, +.wplace-theme-classic .wplace-btn-overlay.active { + background: linear-gradient(135deg, #29b6f6 0%, #8e2de2 100%); + box-shadow: 0 0 15px #8e2de2; +} + +/* Button variants - exact upstream main colors */ +:root .wplace-btn-primary, +.wplace-theme-classic .wplace-btn-primary { + background: linear-gradient(135deg, #222 0%, #6a5acd 100%); + color: var(--wplace-text); +} + +:root .wplace-btn-upload, +.wplace-theme-classic .wplace-btn-upload { + background: linear-gradient(135deg, #111 0%, #4a4a4a 100%); + color: var(--wplace-text); + border: 1px dashed var(--wplace-highlight) !important; +} + +:root .wplace-btn-start, +.wplace-theme-classic .wplace-btn-start { + background: linear-gradient(135deg, var(--wplace-success) 0%, #228b22 100%); + color: white; +} + +:root .wplace-btn-stop, +.wplace-theme-classic .wplace-btn-stop { + background: linear-gradient(135deg, var(--wplace-error) 0%, #dc143c 100%); + color: white; +} + +:root .wplace-btn-select, +.wplace-theme-classic .wplace-btn-select { + background: linear-gradient(135deg, var(--wplace-highlight) 0%, #9370db 100%); + color: white; +} + +:root .wplace-btn-file, +.wplace-theme-classic .wplace-btn-file { + background: linear-gradient(135deg, #ff8c00 0%, #ff7f50 100%); + color: white; +} + +:root .wplace-btn.active, +:root .wplace-btn[aria-pressed="true"], +.wplace-theme-classic .wplace-btn.active, +.wplace-theme-classic .wplace-btn[aria-pressed="true"] { + background: var(--wplace-highlight) !important; + color: #000 !important; + border-color: var(--wplace-text) !important; + box-shadow: 0 0 8px rgb(0 0 0 / 25%) inset, 0 0 6px rgb(0 0 0 / 20%) !important; +} + +:root .wplace-btn.active i, +:root .wplace-btn[aria-pressed="true"] i, +.wplace-theme-classic .wplace-btn.active i, +.wplace-theme-classic .wplace-btn[aria-pressed="true"] i { + filter: drop-shadow(0 0 3px #000); +} + +:root .mask-mode-group .wplace-btn.active, +:root .mask-mode-group .wplace-btn[aria-pressed="true"], +.wplace-theme-classic .mask-mode-group .wplace-btn.active, +.wplace-theme-classic .mask-mode-group .wplace-btn[aria-pressed="true"] { + background: var(--wplace-highlight); + color: #000; + border-color: var(--wplace-text); + box-shadow: 0 0 8px rgb(0 0 0 / 25%) inset, 0 0 6px rgb(0 0 0 / 20%); +} + +/* Status styling for classic theme */ +:root .status-default, +.wplace-theme-classic .status-default { + background: rgb(255 255 255 / 10%); + border-color: var(--wplace-text); + color: var(--wplace-text); +} + +:root .status-success, +.wplace-theme-classic .status-success { + background: rgb(0 255 0 / 10%); + border-color: var(--wplace-success); + color: var(--wplace-success); + box-shadow: 0 0 15px var(--wplace-success); +} + +:root .status-error, +.wplace-theme-classic .status-error { + background: rgb(255 0 0 / 10%); + border-color: var(--wplace-error); + color: var(--wplace-error); + box-shadow: 0 0 15px var(--wplace-error); +} + +:root .status-warning, +.wplace-theme-classic .status-warning { + background: rgb(255 165 0 / 10%); + border-color: var(--wplace-warning); + color: var(--wplace-warning); + box-shadow: 0 0 15px var(--wplace-warning); +} + +:root .wplace-status.status-default, +.wplace-theme-classic .wplace-status.status-default { + color: var(--wplace-text); + background: rgb(255 255 255 / 10%); + border-color: var(--wplace-text); +} + +/* Alert styling for classic theme */ +:root .wplace-alert-info, +.wplace-theme-classic .wplace-alert-info { + background: linear-gradient(135deg, #3498db, #2980b9); + box-shadow: 0 4px 12px rgb(52 152 219 / 30%); +} + +:root .wplace-alert-success, +.wplace-theme-classic .wplace-alert-success { + background: linear-gradient(135deg, #27ae60, #229954); + box-shadow: 0 4px 12px rgb(39 174 96 / 30%); +} + +:root .wplace-alert-warning, +.wplace-theme-classic .wplace-alert-warning { + background: linear-gradient(135deg, #f39c12, #e67e22); + box-shadow: 0 4px 12px rgb(243 156 18 / 30%); +} + +:root .wplace-alert-error, +.wplace-theme-classic .wplace-alert-error { + background: linear-gradient(135deg, #e74c3c, #c0392b); + box-shadow: 0 4px 12px rgb(231 76 60 / 30%); +} + +/* Progress bar styling for classic theme */ +:root .wplace-progress, +.wplace-theme-classic .wplace-progress { + background: rgb(0 0 0 / 30%); + border: 1px solid rgb(255 255 255 / 10%); + border-radius: var(--wplace-radius); +} + +:root .wplace-progress-bar, +.wplace-theme-classic .wplace-progress-bar { + background: linear-gradient(135deg, var(--wplace-highlight) 0%, #9370db 100%); +} + +:root .wplace-progress-bar::after, +.wplace-theme-classic .wplace-progress-bar::after { + background: linear-gradient(90deg, transparent, rgb(255 255 255 / 30%), transparent); +} + +/* Header and sections styling for classic theme */ +:root .wplace-header-btn, +.wplace-theme-classic .wplace-header-btn { + background: rgb(255 255 255 / 10%); + color: var(--wplace-highlight); + border-radius: 4px; + font-family: var(--wplace-font); +} + +:root .wplace-header-btn:hover, +.wplace-theme-classic .wplace-header-btn:hover { + background: #222; + color: var(--wplace-text); +} + +/* Font and typography for classic theme */ +:root .wplace-turnstile-overlay, +.wplace-theme-classic .wplace-turnstile-overlay { + background: rgb(0 0 0 / 90%) !important; + border-radius: var(--wplace-radius) !important; + box-shadow: 0 8px 32px rgb(0 0 0 / 40%) !important; + backdrop-filter: var(--wplace-backdrop) !important; + border: 1px solid rgb(255 255 255 / 20%) !important; + color: var(--wplace-text) !important; + font-family: var(--wplace-font) !important; +} + +/* Auto light/dark support for classic theme */ +@media (prefers-color-scheme: light) { + :root .theme-auto, + .wplace-theme-classic .theme-auto { + --wplace-primary: #fff; + --wplace-secondary: #f5f5f5; + --wplace-accent: #007acc; + --wplace-text: #333; + } +} + +@media (prefers-color-scheme: dark) { + :root .theme-auto, + .wplace-theme-classic .theme-auto { + --wplace-primary: #1e1e1e; + --wplace-secondary: #2d2d30; + --wplace-accent: var(--wplace-highlight); + --wplace-text: #fff; + } +} + +/* Border and color styling for classic theme */ +:root .wplace-color-item-name, +.wplace-theme-classic .wplace-color-item-name { + color: #ccc; +} + +:root .wplace-color-swatch, +.wplace-theme-classic .wplace-color-swatch { + border: 1px solid rgb(255 255 255 / 20%); + border-radius: 4px; +} + +:root .wplace-stat-color-swatch, +.wplace-theme-classic .wplace-stat-color-swatch { + border-radius: 3px; + border: 1px solid rgb(255 255 255 / 10%); + box-shadow: inset 0 0 2px rgb(0 0 0 / 50%); +} + +:root .resize-tools button, +.wplace-theme-classic .resize-tools button { + border-radius: 6px; + border: 1px solid rgb(255 255 255 / 20%); + background: rgb(255 255 255 / 6%); + color: #fff; +} + +:root .resize-slider, +.wplace-theme-classic .resize-slider { + background: #ccc; + border-radius: var(--wplace-radius); +} + +/* Text effects and filters for classic theme */ +:root .wplace-color-swatch:not(.active), +.wplace-theme-classic .wplace-color-swatch:not(.active) { + filter: grayscale(80%); +} + +:root .wplace-color-swatch.unavailable, +.wplace-theme-classic .wplace-color-swatch.unavailable { + border-color: #666; +} + +:root .wplace-color-swatch.unavailable:not(.active), +.wplace-theme-classic .wplace-color-swatch.unavailable:not(.active) { + filter: grayscale(90%); +} + +:root .wplace-color-swatch.active::after, +.wplace-theme-classic .wplace-color-swatch.active::after { + text-shadow: 0 0 3px black; +} + +/* Icon colors for classic theme */ +:root .wplace-icon-key, +.wplace-theme-classic .wplace-icon-key { + color: #4facfe; +} + +:root .wplace-icon-robot, +.wplace-theme-classic .wplace-icon-robot { + color: #4facfe; +} + +:root .wplace-icon-speed, +.wplace-theme-classic .wplace-icon-speed { + color: #4facfe; +} + +:root .wplace-icon-bell, +.wplace-theme-classic .wplace-icon-bell { + color: #ffd166; +} + +:root .wplace-icon-palette, +.wplace-theme-classic .wplace-icon-palette { + color: #f093fb; +} + +:root .wplace-icon-globe, +.wplace-theme-classic .wplace-icon-globe { + color: #ffeaa7; +} + +:root .wplace-icon-paint, +.wplace-theme-classic .wplace-icon-paint { + color: #4facfe; +} + +:root .wplace-icon-eye, +.wplace-theme-classic .wplace-icon-eye { + color: var(--wplace-highlight); +} + +/* Form controls and sliders for classic theme */ +:root .wplace-slider, +.wplace-theme-classic .wplace-slider { + background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%); + border-radius: 4px; +} + +:root .wplace-slider::-webkit-slider-thumb, +.wplace-theme-classic .wplace-slider::-webkit-slider-thumb { + -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, +.wplace-theme-classic .wplace-speed-slider { + background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%); + border-radius: 4px; +} + +:root .wplace-speed-slider::-webkit-slider-thumb, +.wplace-theme-classic .wplace-speed-slider::-webkit-slider-thumb { + border-radius: 50%; + border: 2px solid; +} + +:root .wplace-speed-slider::-moz-range-thumb, +.wplace-theme-classic .wplace-speed-slider::-moz-range-thumb { + border-radius: 50%; + border: 2px solid; +} + +:root .wplace-turnstile-title, +.wplace-theme-classic .wplace-turnstile-title { + color: var(--wplace-text) !important; +} + +:root .wplace-turnstile-hide-btn, +.wplace-theme-classic .wplace-turnstile-hide-btn { + color: var(--wplace-text) !important; + border: 1px solid rgb(255 255 255 / 20%) !important; + border-radius: 6px !important; +} + +:root .wplace-turnstile-hide-btn:hover, +.wplace-theme-classic .wplace-turnstile-hide-btn:hover { + background: rgb(255 255 255 / 10%) !important; +} + +:root .wplace-settings-select, +.wplace-theme-classic .wplace-settings-select { + background: rgb(255 255 255 / 15%); + color: white; + border: 1px solid rgb(255 255 255 / 20%); + border-radius: 8px; + box-shadow: 0 3px 10px rgb(0 0 0 / 10%); +} + +:root .wplace-settings-option, +.wplace-theme-classic .wplace-settings-option { + background: #2d3748; + color: white; +} + +:root .wplace-stat-colors-grid, +.wplace-theme-classic .wplace-stat-colors-grid { + background: rgb(0 0 0 / 20%); + border-radius: 4px; +} + +:root .resize-overlay, +.wplace-theme-classic .resize-overlay { + background: rgb(0 0 0 / 80%); +} + +:root .wplace-overlay, +.wplace-theme-classic .wplace-overlay { + background: rgb(0 0 0 / 80%); +} + +:root .wplace-settings-error, +.wplace-theme-classic .wplace-settings-error { + background: rgb(255 0 0 / 40%) !important; +} + +/* Settings sections and batch controls for classic theme */ +:root .wplace-settings-section-wrapper, +.wplace-theme-classic .wplace-settings-section-wrapper { + background: rgb(255 255 255 / 10%); + border-radius: var(--wplace-radius); + border: 1px solid rgb(255 255 255 / 10%); +} + +:root .wplace-batch-controls, +.wplace-theme-classic .wplace-batch-controls { + background: rgb(255 255 255 / 10%); + border-radius: var(--wplace-radius); + border: 1px solid rgb(255 255 255 / 10%); +} + +:root .wplace-overlay-opacity-slider, +.wplace-theme-classic .wplace-overlay-opacity-slider { + background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%); + border-radius: 4px; +} + +:root .wplace-speed-value, +.wplace-theme-classic .wplace-speed-value { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + border-radius: 8px; + color: white; + box-shadow: 0 3px 10px rgb(79 172 254 / 30%); + border: 1px solid rgb(255 255 255 / 20%); +} + +/* Additional elements for classic theme */ +:root .wplace-color-divider, +.wplace-theme-classic .wplace-color-divider { + background: rgb(255 255 255 / 10%); +} + +:root .wplace-settings-select option, +.wplace-theme-classic .wplace-settings-select option { + background: #2d3748; + color: white; +} + +:root .wplace-settings-description, +.wplace-theme-classic .wplace-settings-description { + color: rgb(255 255 255 / 70%); +} + +:root .wplace-settings-apply-btn, +.wplace-theme-classic .wplace-settings-apply-btn { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + color: white; + border-radius: 8px; +} + +/* Dual control layout styling for classic theme */ +:root .wplace-dual-control-compact, +.wplace-theme-classic .wplace-dual-control-compact { + display: flex; + flex-direction: row; + align-items: center; + gap: 12px; + margin: 8px 0; + flex-wrap: wrap; +} + +:root .wplace-slider-container-compact, +.wplace-theme-classic .wplace-slider-container-compact { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + min-width: 160px; +} + +:root .wplace-speed-slider-container-compact, +.wplace-theme-classic .wplace-speed-slider-container-compact { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + min-width: 160px; +} + +:root .wplace-input-group-compact, +.wplace-theme-classic .wplace-input-group-compact { + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; +} + +:root .wplace-input-btn-compact, +.wplace-theme-classic .wplace-input-btn-compact { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + color: white; + border: none; + border-radius: 4px; + width: 22px; + height: 22px; + cursor: pointer; + font-weight: bold; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); +} + +:root .wplace-input-btn-compact:hover, +.wplace-theme-classic .wplace-input-btn-compact:hover { + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0,0,0,0.3); + background: linear-gradient(135deg, #00f2fe 0%, #4facfe 100%); +} + +:root .wplace-input-btn-compact:active, +.wplace-theme-classic .wplace-input-btn-compact:active { + transform: translateY(0); + box-shadow: 0 2px 4px rgba(0,0,0,0.2); +} + +:root .wplace-number-input-compact, +.wplace-theme-classic .wplace-number-input-compact { + background: rgba(255,255,255,0.1); + border: 1px solid rgba(255,255,255,0.2); + border-radius: 4px; + color: white; + padding: 4px 8px; + font-size: 12px; + width: 50px; + text-align: center; + transition: all 0.2s ease; + height: 22px; +} + +:root .wplace-number-input-compact:focus, +.wplace-theme-classic .wplace-number-input-compact:focus { + outline: none; + border-color: #4facfe; + box-shadow: 0 0 0 3px rgba(79, 172, 254, 0.3); + background: rgba(255,255,255,0.15); +} + +:root .wplace-input-label-compact, +.wplace-theme-classic .wplace-input-label-compact { + color: rgba(255,255,255,0.8); + font-size: 11px; + margin-left: 4px; + white-space: nowrap; +} + +:root .wplace-batch-size-header, +.wplace-theme-classic .wplace-batch-size-header { + margin-bottom: 8px; +} + +:root .wplace-batch-size-label, +.wplace-theme-classic .wplace-batch-size-label { + color: var(--wplace-highlight); + font-weight: 500; + font-size: 13px; +} + +/* Cooldown control styling for classic theme */ +:root .wplace-cooldown-control, +.wplace-theme-classic .wplace-cooldown-control { + margin: 12px 0; +} + +:root .wplace-cooldown-control label, +.wplace-theme-classic .wplace-cooldown-control label { + color: var(--wplace-highlight); + font-weight: 500; + font-size: 13px; + margin-bottom: 8px; + display: block; +} + +/* Resize dialog styling for classic theme */ +:root .resize-container, +.wplace-theme-classic .resize-container { + background: #000; + border: 1px solid #fff; + border-radius: 12px; + box-shadow: 0 0 20px rgb(0 0 0 / 50%); +} diff --git a/themes/neon.css b/themes/neon.css new file mode 100644 index 00000000..1daa02b2 --- /dev/null +++ b/themes/neon.css @@ -0,0 +1,519 @@ +/* WPlace Auto-Image Bot - Neon Theme */ +/* Retro cyberpunk aesthetic with green neon glow effects */ + +.wplace-theme-neon { + /* Neon theme colors - matching upstream main neon theme */ + --wplace-primary: #1a1a2e; + --wplace-secondary: #16213e; + --wplace-accent: #0f3460; + --wplace-text: #00ff41; + --wplace-highlight: #ff6b35; + --wplace-success: #39ff14; + --wplace-error: #ff073a; + --wplace-warning: #ff0; + + /* Neon UI properties */ + --wplace-radius: 0; + --wplace-border-style: solid; + --wplace-border-width: 2px; + --wplace-border-color: #00ff41; + --wplace-shadow: 0 0 20px rgb(0 255 65 / 30%), inset 0 0 20px rgb(0 255 65 / 10%); + --wplace-backdrop: none; + --wplace-font: 'Press Start 2P', monospace, 'Courier New'; + + /* Z-index layers */ + --wplace-z-overlay: 10000; + --wplace-z-alert: 10002; + --wplace-z-settings: 10002; + + /* Feature toggles */ + --wplace-scanline: 1; + --wplace-pixel-blink: 1; + + /* Icon colors - neon variants */ + --wplace-icon-primary: #00ff41; + --wplace-icon-secondary: #39ff14; + --wplace-icon-palette: #ff6b35; + + /* Additional UI colors - neon variants */ + --wplace-danger: #ff073a; + --wplace-danger-dark: #cc0531; + --wplace-muted-text: #00ff4180; + --wplace-highlight-secondary: #fa0; + + /* Text variants - neon style */ + --wplace-text-secondary: #00ff41dd; + --wplace-text-muted: #00ff41bb; + --wplace-text-dim: #00ff4199; + --wplace-text-faded: #00ff41cc; + + /* Background variants - neon style */ + --wplace-bg-input: rgb(0 255 65 / 15%); + --wplace-bg-subtle: rgb(0 255 65 / 8%); + --wplace-bg-faint: rgb(0 255 65 / 5%); + --wplace-bg-ghost: rgb(0 255 65 / 3%); + --wplace-bg-whisper: rgb(0 255 65 / 2%); + + /* Border variants - neon style */ + --wplace-border-subtle: rgb(0 255 65 / 40%); + --wplace-border-faint: rgb(0 255 65 / 25%); + --wplace-border-ghost: rgb(0 255 65 / 15%); + --wplace-border-ultra-faint: rgb(0 255 65 / 8%); + + /* Shadow variants - neon style */ + --wplace-shadow-drag: 0 12px 40px rgb(0 255 65 / 60%), 0 0 0 2px rgb(0 255 65 / 80%), 0 0 20px rgb(0 255 65 / 30%); + --wplace-shadow-notification: 0 4px 12px rgb(0 255 65 / 40%), 0 0 15px rgb(0 255 65 / 20%); + --wplace-shadow-slider-thumb: 0 3px 6px rgb(0 255 65 / 50%), 0 0 0 2px var(--wplace-icon-primary), 0 0 10px rgb(0 255 65 / 30%); + --wplace-shadow-slider-hover: 0 4px 8px rgb(0 255 65 / 60%), 0 0 0 3px var(--wplace-icon-primary), 0 0 15px rgb(0 255 65 / 40%); + + /* Animation colors - neon style */ + --wplace-pulse-start: rgb(0 255 65 / 80%); + --wplace-pulse-mid: rgb(0 255 65 / 0%); + --wplace-pulse-end: rgb(0 255 65 / 0%); + + /* Slider colors - neon style */ + --wplace-slider-thumb-bg: #00ff41; +} + +/* Neon-specific styling overrides */ +.wplace-theme-neon #wplace-image-bot-container { + background: var(--wplace-primary) !important; + border: 2px solid var(--wplace-text) !important; + border-radius: 0 !important; + box-shadow: 0 0 20px var(--wplace-text), inset 0 0 20px rgb(0 255 65 / 10%) !important; + font-family: var(--wplace-font) !important; +} + +.wplace-theme-neon #wplace-stats-container { + background: var(--wplace-primary) !important; + border: 2px solid var(--wplace-text) !important; + border-radius: 0 !important; + box-shadow: 0 0 20px var(--wplace-text), inset 0 0 20px rgb(0 255 65 / 10%) !important; + font-family: var(--wplace-font) !important; + position: fixed !important; + overflow: hidden !important; + z-index: 9998 !important; +} + +.wplace-theme-neon .wplace-header { + background: var(--wplace-secondary) !important; + border-bottom: 1px solid var(--wplace-text) !important; + color: var(--wplace-text) !important; + text-shadow: 0 0 10px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; +} + +.wplace-theme-neon .wplace-section { + background: rgb(22 33 62 / 50%) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; +} + +.wplace-theme-neon .wplace-status-section { + background: rgb(22 33 62 / 50%) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; +} + +.wplace-theme-neon .wplace-section-title { + color: var(--wplace-text) !important; + text-shadow: 0 0 8px var(--wplace-text) !important; + text-transform: uppercase !important; + font-family: var(--wplace-font) !important; + font-size: 10px !important; +} + +.wplace-theme-neon .wplace-btn { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; + text-shadow: 0 0 5px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + font-size: 9px !important; + text-transform: uppercase !important; +} + +/* Pixel blinking and neon glow animation for buttons */ +.wplace-theme-neon .wplace-btn:hover:not(:disabled) { + box-shadow: 0 0 15px var(--wplace-text), inset 0 0 15px rgb(0 255 65 / 10%) !important; + animation: pixel-blink 0.5s infinite, neon-glow 1s ease-in-out infinite alternate !important; +} + +.wplace-theme-neon .wplace-btn-start { + background: var(--wplace-secondary) !important; + border-color: var(--wplace-success) !important; + color: var(--wplace-success) !important; + text-shadow: 0 0 8px var(--wplace-success) !important; +} + +.wplace-theme-neon .wplace-btn-stop { + background: var(--wplace-secondary) !important; + border-color: var(--wplace-error) !important; + color: var(--wplace-error) !important; + text-shadow: 0 0 8px var(--wplace-error) !important; +} + +.wplace-theme-neon .wplace-btn-upload { + background: var(--wplace-secondary) !important; + border: 1px dashed var(--wplace-highlight) !important; + color: var(--wplace-highlight) !important; + text-shadow: 0 0 8px var(--wplace-highlight) !important; +} + +.wplace-theme-neon .wplace-btn-select { + background: var(--wplace-secondary) !important; + border-color: var(--wplace-highlight) !important; + color: var(--wplace-highlight) !important; + text-shadow: 0 0 8px var(--wplace-highlight) !important; +} + +.wplace-theme-neon .wplace-btn-file { + background: var(--wplace-secondary) !important; + border-color: var(--wplace-warning) !important; + color: var(--wplace-warning) !important; + text-shadow: 0 0 8px var(--wplace-warning) !important; +} + +.wplace-theme-neon .wplace-progress { + background: rgb(0 0 0 / 80%) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; +} + +.wplace-theme-neon .wplace-progress-bar { + background: linear-gradient(90deg, var(--wplace-success) 0%, var(--wplace-text) 100%) !important; + box-shadow: 0 0 10px var(--wplace-success) !important; +} + +.wplace-theme-neon .wplace-stat-value { + color: var(--wplace-text) !important; + text-shadow: 0 0 5px var(--wplace-text) !important; +} + +.wplace-theme-neon .status-success { + background: rgb(57 255 20 / 10%) !important; + border-color: var(--wplace-success) !important; + color: var(--wplace-success) !important; + box-shadow: 0 0 15px var(--wplace-success) !important; + text-shadow: 0 0 8px var(--wplace-success) !important; +} + +.wplace-theme-neon .status-error { + background: rgb(255 7 58 / 10%) !important; + border-color: var(--wplace-error) !important; + color: var(--wplace-error) !important; + box-shadow: 0 0 15px var(--wplace-error) !important; + text-shadow: 0 0 8px var(--wplace-error) !important; +} + +.wplace-theme-neon .status-default { + background: rgb(0 255 65 / 10%) !important; + border-color: var(--wplace-text) !important; + color: var(--wplace-text) !important; + text-shadow: 0 0 5px var(--wplace-text) !important; +} + +/* Settings dialog neon styling */ +.wplace-theme-neon #wplace-settings-container { + background: var(--wplace-primary) !important; + border: 2px solid var(--wplace-text) !important; + border-radius: 0 !important; + box-shadow: 0 0 30px var(--wplace-text), inset 0 0 30px rgb(0 255 65 / 10%) !important; + font-family: var(--wplace-font) !important; +} + +.wplace-theme-neon .wplace-settings-header { + background: var(--wplace-secondary) !important; + border-bottom: 1px solid var(--wplace-text) !important; +} + +.wplace-theme-neon .wplace-settings-header h3 { + color: var(--wplace-text) !important; + text-shadow: 0 0 10px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + font-size: 16px !important; + text-transform: uppercase !important; +} + +.wplace-theme-neon .wplace-settings-close-btn { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; +} + +.wplace-theme-neon .wplace-settings-close-btn:hover { + background: var(--wplace-error) !important; + box-shadow: 0 0 15px var(--wplace-error) !important; +} + +.wplace-theme-neon .wplace-settings-section-wrapper { + background: rgb(22 33 62 / 30%) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; +} + +.wplace-theme-neon .wplace-settings-select { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + font-size: 11px !important; +} + +.wplace-theme-neon .wplace-settings-section-label { + color: var(--wplace-text) !important; + text-shadow: 0 0 8px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + font-size: 12px !important; + text-transform: uppercase !important; +} + +.wplace-theme-neon .wplace-speed-value { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; + text-shadow: 0 0 8px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + box-shadow: 0 0 10px rgb(0 255 65 / 30%) !important; +} + +.wplace-theme-neon .wplace-overlay-opacity-value { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; + text-shadow: 0 0 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 rgb(0 255 65 / 30%) !important; +} + +.wplace-theme-neon .wplace-slider::-webkit-slider-thumb, +.wplace-theme-neon .wplace-speed-slider::-webkit-slider-thumb, +.wplace-theme-neon .wplace-overlay-opacity-slider::-webkit-slider-thumb { + -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 rgb(0 255 65 / 50%) !important; +} + +.wplace-theme-neon .wplace-slider::-webkit-slider-thumb:hover, +.wplace-theme-neon .wplace-speed-slider::-webkit-slider-thumb:hover, +.wplace-theme-neon .wplace-overlay-opacity-slider::-webkit-slider-thumb:hover { + 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: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent, var(--wplace-text), transparent); + z-index: 1; + pointer-events: none; + animation: scanline 3s linear infinite; + opacity: 0.7; +} + + +.wplace-theme-neon #wplace-stats-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent, var(--wplace-text), transparent); + z-index: 1; + pointer-events: none; + animation: scanline-stats 4s linear infinite; + opacity: 0.7; +} + + +/* Text glow animations */ +@keyframes neon-glow { + 0%, 100% { + text-shadow: 0 0 5px currentcolor, 0 0 10px currentcolor, 0 0 15px currentcolor; + } + + 50% { + text-shadow: 0 0 2px currentcolor, 0 0 5px currentcolor, 0 0 8px currentcolor; + } +} + +@keyframes pixel-blink { + 0%, 50% { + opacity: 1; + } + + 51%, 100% { + opacity: 0.7; + } +} + +@keyframes scanline { + 0% { + transform: translateY(-100%); + } + + 100% { + transform: translateY(400px); + } +} + +@keyframes scanline-stats { + 0% { + transform: translateY(-100%); + } + + 100% { + transform: translateY(300px); + } +} + +/* Dual control layout styling for neon theme */ +.wplace-theme-neon .wplace-dual-control-compact { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + margin: 6px 0; + flex-wrap: wrap; +} + +.wplace-theme-neon .wplace-slider-container-compact { + display: flex; + align-items: center; + gap: 6px; + flex: 1; + min-width: 160px; +} + +.wplace-theme-neon .wplace-speed-slider-container-compact { + display: flex; + align-items: center; + gap: 6px; + flex: 1; + min-width: 160px; +} + +.wplace-theme-neon .wplace-input-group-compact { + display: flex; + align-items: center; + gap: 3px; + flex-shrink: 0; +} + +.wplace-theme-neon .wplace-input-btn-compact { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; + width: 18px; + height: 18px; + cursor: pointer; + font-weight: bold; + font-size: 10px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + text-shadow: 0 0 5px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; +} + +.wplace-theme-neon .wplace-input-btn-compact:hover { + box-shadow: 0 0 10px var(--wplace-text) !important; + animation: pixel-blink 0.5s infinite !important; +} + +.wplace-theme-neon .wplace-number-input-compact { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + color: var(--wplace-text) !important; + padding: 2px 4px; + font-size: 10px; + width: 40px; + text-align: center; + transition: all 0.2s ease; + height: 18px; + font-family: var(--wplace-font) !important; + text-shadow: 0 0 5px var(--wplace-text) !important; +} + +.wplace-theme-neon .wplace-number-input-compact:focus { + outline: none; + box-shadow: 0 0 10px var(--wplace-text) !important; + background: rgb(0 255 65 / 15%) !important; +} + +.wplace-theme-neon .wplace-input-label-compact { + color: var(--wplace-text) !important; + font-size: 9px; + margin-left: 3px; + white-space: nowrap; + text-shadow: 0 0 5px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + text-transform: uppercase; +} + +.wplace-theme-neon .wplace-batch-size-header { + margin-bottom: 6px; +} + +.wplace-theme-neon .wplace-batch-size-label { + color: var(--wplace-text) !important; + font-weight: 500; + font-size: 11px; + text-shadow: 0 0 8px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + text-transform: uppercase; +} + +/* Cooldown control styling for neon theme */ +.wplace-theme-neon .wplace-cooldown-control { + margin: 8px 0; +} + +.wplace-theme-neon .wplace-cooldown-control label { + color: var(--wplace-text) !important; + font-weight: 500; + font-size: 11px; + text-shadow: 0 0 8px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + text-transform: uppercase; + margin-bottom: 6px; + display: block; +} + +/* Resize dialog styling for neon theme */ +.wplace-theme-neon .resize-container { + background: #1a1a2e !important; + border: 3px solid #00ff41 !important; + border-radius: 0 !important; + box-shadow: 0 0 30px rgb(0 255 65 / 50%) !important; + font-family: 'Press Start 2P', monospace !important; +} +