- uses: peaceiris/actions-gh-pages@v4
+ # Minimal index page
+ cat > public/index.html <<'HTML'
+
+ BranchesBranches
+ HTML
+
+ for BR in "${BRANCHES[@]}"; do
+ SAFE=$(echo "$BR" | tr '/:@ ' '----' | sed 's/[^A-Za-z0-9._-]/-/g')
+ mkdir -p "public/$SAFE"
+
+ # Fetch that branch tip and export without checkout
+ git fetch --depth=1 origin "$BR"
+ git archive --format=tar FETCH_HEAD | tar -x -C "public/$SAFE"
+
+ # Keep only static assets; drop heavy/unneeded 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
+
+ echo "- $BR
" >> public/index.html
+ done
+ echo "
" >> public/index.html
+ echo "Built public/ for ${#BRANCHES[@]} branches."
+
+ - name: Upload Pages artifact
+ uses: actions/upload-pages-artifact@v4
with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
- publish_branch: gh-pages
- publish_dir: _site
- destination_dir: ${{ steps.br.outputs.safe }}
- keep_files: true
- commit_message: Deploy ${{ github.ref_name }}
+ path: public
+
+ - name: Deploy to GitHub Pages
+ id: deploy
+ uses: actions/deploy-pages@v4
From 5e13c567275475f410f6d306c2a90d368f72b287 Mon Sep 17 00:00:00 2001
From: Staninna <26458805+Staninna@users.noreply.github.com>
Date: Wed, 27 Aug 2025 16:12:03 +0200
Subject: [PATCH 09/19] test commit to check if ci gets triggered when i push
another branch
---
themes/neon.css | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/themes/neon.css b/themes/neon.css
index af3a0d8..3b69283 100644
--- a/themes/neon.css
+++ b/themes/neon.css
@@ -1,5 +1,5 @@
/* WPlace Auto-Image Bot - Neon Theme */
-/* Retro cyberpunk aesthetic with green neon glow effects */
+/* Retro cyberpunk aesthetic with green neon glow effects :) */
.wplace-theme--neon {
/* Neon theme colors - matching upstream main neon theme */
From be340b799caed0c4c046f826765e8e6ab1d08d03 Mon Sep 17 00:00:00 2001
From: Staninna <26458805+Staninna@users.noreply.github.com>
Date: Wed, 27 Aug 2025 16:27:25 +0200
Subject: [PATCH 10/19] cd should run
---
themes/neon.css | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/themes/neon.css b/themes/neon.css
index 3b69283..af3a0d8 100644
--- a/themes/neon.css
+++ b/themes/neon.css
@@ -1,5 +1,5 @@
/* WPlace Auto-Image Bot - Neon Theme */
-/* Retro cyberpunk aesthetic with green neon glow effects :) */
+/* Retro cyberpunk aesthetic with green neon glow effects */
.wplace-theme--neon {
/* Neon theme colors - matching upstream main neon theme */
From a5e82fe408931a7dfbc36c5ac2c5c3f7f2209d78 Mon Sep 17 00:00:00 2001
From: Staninna <26458805+Staninna@users.noreply.github.com>
Date: Wed, 27 Aug 2025 16:37:45 +0200
Subject: [PATCH 11/19] update cd.yml to be better??? test run
---
.github/workflows/cd.yml | 122 ++++++++++++++++++++++++++++++++++-----
1 file changed, 106 insertions(+), 16 deletions(-)
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
index c77e2e1..ebc3c81 100644
--- a/.github/workflows/cd.yml
+++ b/.github/workflows/cd.yml
@@ -29,42 +29,132 @@ jobs:
- name: Configure Pages
uses: actions/configure-pages@v5
- - name: Aggregate files from all branches into ./public
+ - name: Aggregate and build recursive directory pages
shell: bash
run: |
set -euo pipefail
+
+ OWNER="${GITHUB_REPOSITORY%/*}"
+ REPO="${GITHUB_REPOSITORY#*/}"
+ SITE_ROOT="/${REPO}/"
+
mkdir -p public
+ : > public/.nojekyll
- # List remote branches (exclude HEAD pointer)
- mapfile -t BRANCHES < <(git ls-remote --heads origin \
- | awk '{print $2}' | sed 's@refs/heads/@@')
+ # List remote branches
+ mapfile -t BRANCHES < <(
+ git ls-remote --heads origin | awk '{print $2}' \
+ | sed 's@refs/heads/@@'
+ )
- # Minimal index page
+ # Top-level landing page
cat > public/index.html <<'HTML'
-
- BranchesBranches
" >> public/index.html
- echo "Built public/ for ${#BRANCHES[@]} branches."
+
+ # Finish landing page with JS to render branch cards
+ cat >> public/index.html <<'HTML'
+
+ HTML
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v4
@@ -73,4 +163,4 @@ jobs:
- name: Deploy to GitHub Pages
id: deploy
- uses: actions/deploy-pages@v4
+ uses: actions/deploy-pages@v4
\ No newline at end of file
From e20d2818bed3a5b81c02a601abc648d808f6e25b Mon Sep 17 00:00:00 2001
From: Staninna <26458805+Staninna@users.noreply.github.com>
Date: Wed, 27 Aug 2025 16:55:03 +0200
Subject: [PATCH 12/19] update cd.yml to be better??? test run
---
.github/workflows/cd.yml | 36 +++++++++++++++++++++++-------------
1 file changed, 23 insertions(+), 13 deletions(-)
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
index ebc3c81..1c26237 100644
--- a/.github/workflows/cd.yml
+++ b/.github/workflows/cd.yml
@@ -20,6 +20,7 @@ jobs:
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
+
steps:
- name: Checkout (shallow)
uses: actions/checkout@v4
@@ -34,6 +35,12 @@ jobs:
run: |
set -euo pipefail
+ # Ensure jq is available (usually preinstalled)
+ if ! command -v jq >/dev/null 2>&1; then
+ sudo apt-get update -y
+ sudo apt-get install -y jq
+ fi
+
OWNER="${GITHUB_REPOSITORY%/*}"
REPO="${GITHUB_REPOSITORY#*/}"
SITE_ROOT="/${REPO}/"
@@ -41,13 +48,14 @@ jobs:
mkdir -p public
: > public/.nojekyll
- # List remote branches
+ # List remote branches (exclude symbolic HEAD)
mapfile -t BRANCHES < <(
- git ls-remote --heads origin | awk '{print $2}' \
+ git ls-remote --heads origin \
+ | awk '{print $2}' \
| sed 's@refs/heads/@@'
)
- # Top-level landing page
+ # Landing page shell
cat > public/index.html <<'HTML'
@@ -66,17 +74,16 @@ jobs:
HTML
- # Helper to create an index.html for every directory
+ # Function to emit an index.html for a directory
gen_index_dir() {
local DIR="$1" ROOT="$2" BR="$3" SAFE="$4"
local rel="${DIR#$ROOT}" # "" or "/sub/dir"
local title="Branch: ${BR}${rel:-/}"
- # Collect entries
mapfile -t SUBDIRS < <(find "$DIR" -mindepth 1 -maxdepth 1 -type d \
-printf '%f\n' | sort)
mapfile -t FILES < <(find "$DIR" -mindepth 1 -maxdepth 1 -type f \
@@ -108,16 +115,16 @@ jobs:
} > "$DIR/index.html"
}
- # Process each branch
+ # Build trees and index entries per branch
for BR in "${BRANCHES[@]}"; do
SAFE="$(echo "$BR" | tr '/:@ ' '----' | sed 's/[^A-Za-z0-9._-]/-/g')"
mkdir -p "public/$SAFE"
- # Fetch and extract content without checkout
+ # Export branch contents without checkout
git fetch --depth=1 origin "$BR"
git archive --format=tar FETCH_HEAD | tar -x -C "public/$SAFE"
- # Keep only static assets, then remove empty dirs
+ # Keep only static assets, then prune empties
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 \
@@ -125,17 +132,20 @@ jobs:
-name '*.svg' \) -delete
find "public/$SAFE" -type d -empty -delete
- # Generate index.html for every directory (recursive)
+ # Generate index.html for every directory in this branch
ROOT_DIR="public/$SAFE"
while IFS= read -r -d '' d; do
gen_index_dir "$d" "$ROOT_DIR" "$BR" "$SAFE"
done < <(find "$ROOT_DIR" -type d -print0)
- # Add branch to landing page list
- echo "" >> public/index.html
+ # Append to landing page using JSON-escaped values
+ SAFE_JSON=$(printf '%s' "$SAFE" | jq -Rr @json)
+ BR_JSON=$(printf '%s' "$BR" | jq -Rr @json)
+ echo "" \
+ >> public/index.html
done
- # Finish landing page with JS to render branch cards
+ # Finish landing page
cat >> public/index.html <<'HTML'
+
HTML
- # Function to emit an index.html for a directory
+ # Emit index.html for a directory
gen_index_dir() {
- local DIR="$1" ROOT="$2" BR="$3" SAFE="$4"
- local rel="${DIR#$ROOT}" # "" or "/sub/dir"
+ local DIR="$1" ROOT="$2" BR="$3"
+ local rel="${DIR#$ROOT}"
local title="Branch: ${BR}${rel:-/}"
- mapfile -t SUBDIRS < <(find "$DIR" -mindepth 1 -maxdepth 1 -type d \
- -printf '%f\n' | sort)
- mapfile -t FILES < <(find "$DIR" -mindepth 1 -maxdepth 1 -type f \
- ! -name 'index.html' -printf '%f\n' | sort)
-
{
echo ""
echo ""
+ echo ""
echo "${title}"
echo ""
echo "${title}
"
@@ -101,68 +98,82 @@ jobs:
echo "All branches
"
fi
echo "| Name | Size | Modified |
"
- for d in "${SUBDIRS[@]}"; do
- echo "| ๐ $d/ | โ | โ |
"
- done
- for f in "${FILES[@]}"; do
- local size=$(stat -c %s "$DIR/$f" 2>/dev/null || echo 0)
- local size_h=$(numfmt --to=iec --suffix=B "$size" 2>/dev/null || echo "$size")
- local mod=$(date -r "$DIR/$f" "+%Y-%m-%d %H:%M" 2>/dev/null || echo "")
- echo "| ๐ $f | $size_h | $mod |
"
- done
+
+ # Subdirectories
+ while IFS= read -r d; do
+ [[ -n "$d" ]] || continue
+ printf "| ๐ %s/ | โ | โ |
\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 "| ๐ %s | %s | %s |
\n" "$f" "$f" "$size_h" "$mod"
+ done < <(find "$DIR" -mindepth 1 -maxdepth 1 -type f ! -name 'index.html' -printf '%f\n' | sort)
+
echo "
"
echo "Generated $(date -u '+%Y-%m-%d %H:%M UTC')
"
} > "$DIR/index.html"
}
- # Build trees and index entries per branch
+ # 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"
- # Export branch contents without checkout
git fetch --depth=1 origin "$BR"
git archive --format=tar FETCH_HEAD | tar -x -C "public/$SAFE"
- # Keep only static assets, then prune empties
- rm -rf "public/$SAFE/.git" "public/$SAFE/.github" "public/$SAFE/node_modules"
+ # 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
+ \( -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
- # Generate index.html for every directory in this branch
ROOT_DIR="public/$SAFE"
while IFS= read -r -d '' d; do
- gen_index_dir "$d" "$ROOT_DIR" "$BR" "$SAFE"
+ gen_index_dir "$d" "$ROOT_DIR" "$BR"
done < <(find "$ROOT_DIR" -type d -print0)
- # Append to landing page using JSON-escaped values
+ # Append to landing page (JSON-escaped)
SAFE_JSON=$(printf '%s' "$SAFE" | jq -Rr @json)
BR_JSON=$(printf '%s' "$BR" | jq -Rr @json)
- echo "" \
+ echo "" \
>> public/index.html
done
# Finish landing page
cat >> public/index.html <<'HTML'
HTML
From 9165c5b7509f89c527d5362ec81acecb29c69452 Mon Sep 17 00:00:00 2001
From: Staninna <26458805+Staninna@users.noreply.github.com>
Date: Wed, 27 Aug 2025 19:24:59 +0200
Subject: [PATCH 14/19] Are we revived? Restored all the mysteriously
disappeared code
---
Auto-Image.js | 2801 +++++++++++------------------------------
auto-image-styles.css | 1966 +++++++++++++++++++++++++++++
2 files changed, 2715 insertions(+), 2052 deletions(-)
create mode 100644 auto-image-styles.css
diff --git a/Auto-Image.js b/Auto-Image.js
index c967534..546cb66 100644
--- a/Auto-Image.js
+++ b/Auto-Image.js
@@ -21,10 +21,10 @@
COOLDOWN_CHARGE_THRESHOLD: 1, // Default wait threshold
// Desktop Notifications (defaults)
NOTIFICATIONS: {
- ENABLED: true,
- ON_CHARGES_REACHED: true,
- ONLY_WHEN_UNFOCUSED: true,
- REPEAT_MINUTES: 5, // repeat reminder while threshold condition holds
+ ENABLED: true,
+ ON_CHARGES_REACHED: true,
+ ONLY_WHEN_UNFOCUSED: true,
+ REPEAT_MINUTES: 5, // repeat reminder while threshold condition holds
},
OVERLAY: {
OPACITY_DEFAULT: 0.6,
@@ -146,6 +146,27 @@
pixelBlink: false,
},
},
+ "Classic Light": {
+ primary: "#ffffff",
+ secondary: "#f8f9fa",
+ accent: "#e9ecef",
+ text: "#212529",
+ highlight: "#6f42c1",
+ success: "#28a745",
+ error: "#dc3545",
+ warning: "#ffc107",
+ fontFamily: "'Segoe UI', Roboto, sans-serif",
+ borderRadius: "12px",
+ borderStyle: "solid",
+ borderWidth: "1px",
+ boxShadow: "0 8px 32px rgba(0,0,0,0.15), 0 0 0 1px rgba(0,0,0,0.08)",
+ backdropFilter: "blur(10px)",
+ animations: {
+ glow: false,
+ scanline: false,
+ pixelBlink: false,
+ },
+ },
"Neon Retro": {
primary: "#1a1a2e",
secondary: "#16213e",
@@ -177,20 +198,56 @@
const getCurrentTheme = () => CONFIG.THEMES[CONFIG.currentTheme]
const switchTheme = (themeName) => {
- if (CONFIG.THEMES[themeName]) {
- CONFIG.currentTheme = themeName
- saveThemePreference()
+ if (CONFIG.THEMES[themeName]) {
+ CONFIG.currentTheme = themeName
+ saveThemePreference()
- // Remove existing theme styles
- const existingStyle = document.querySelector('style[data-wplace-theme="true"]')
- if (existingStyle) {
- existingStyle.remove()
- }
+ // APPLY THEME VARS/CLASS (new)
+ applyTheme()
- // Recreate UI with new theme (cleanup is handled in createUI)
- createUI()
- }
+ // Recreate UI (kept for now)
+ createUI()
}
+}
+
+// Add this helper (place it after getCurrentTheme/switchTheme definitions)
+function applyTheme() {
+ const theme = getCurrentTheme();
+ // Toggle theme class on documentElement so CSS vars cascade to our UI
+ document.documentElement.classList.remove('wplace-theme--classic', 'wplace-theme--classic-light', 'wplace-theme--neon');
+
+ let themeClass = 'wplace-theme--classic'; // default
+ if (CONFIG.currentTheme === 'Neon Retro') {
+ themeClass = 'wplace-theme--neon';
+ } else if (CONFIG.currentTheme === 'Classic Light') {
+ themeClass = 'wplace-theme--classic-light';
+ }
+
+ document.documentElement.classList.add(themeClass);
+
+ // Also set CSS variables explicitly in case you want runtime overrides
+ const root = document.documentElement;
+ const setVar = (k, v) => { try { root.style.setProperty(k, v); } catch {} };
+
+ setVar('--wplace-primary', theme.primary);
+ setVar('--wplace-secondary', theme.secondary);
+ setVar('--wplace-accent', theme.accent);
+ setVar('--wplace-text', theme.text);
+ setVar('--wplace-highlight', theme.highlight);
+ setVar('--wplace-success', theme.success);
+ setVar('--wplace-error', theme.error);
+ setVar('--wplace-warning', theme.warning);
+
+ // Typography + look
+ setVar('--wplace-font', theme.fontFamily || "'Segoe UI', Roboto, sans-serif");
+ setVar('--wplace-radius', ('' + (theme.borderRadius || '12px')));
+ setVar('--wplace-border-style', (('' + (theme.borderStyle || 'solid'))));
+ setVar('--wplace-border-width', (('' + (theme.borderWidth || '1px'))));
+ setVar('--wplace-backdrop', (('' + (theme.backdropFilter || 'blur(10px)'))));
+ setVar('--wplace-border-color', 'rgba(255,255,255,0.1)');
+}
+
+
const saveThemePreference = () => {
try {
@@ -212,19 +269,13 @@
}
const loadLanguagePreference = () => {
- const savedLanguage = localStorage.getItem("wplace_language")
-
- const browserLocale = navigator.language
- const browserLanguage = browserLocale.split("-")[0]
-
- if (TEXT[savedLanguage]) { // trying to load saved language
- state.language = savedLanguage
- } else if (TEXT[browserLocale]) { // trying to load full locale match (e.g. "zh-CN", "zh-TW" etc)
- state.language = browserLocale
- localStorage.setItem("wplace_language", browserLocale)
- } else if (TEXT[browserLanguage]) { // trying to load base language match (e.g. "en" for "en-US" or "en-GB" etc)
- state.language = browserLanguage
- localStorage.setItem("wplace_language", browserLanguage)
+ try {
+ const saved = localStorage.getItem("wplace_language")
+ if (saved && TEXT[saved]) {
+ state.language = saved
+ }
+ } catch (e) {
+ console.warn("Could not load language preference:", e)
}
}
@@ -740,7 +791,7 @@
automation: "Otomasyon",
noChargesThreshold: "โ Haklarฤฑn {threshold} seviyesine ulaลmasฤฑ bekleniyor. ลu anda {current}. Sonraki {time} iรงinde...",
},
- "zh-CN": {
+ zh: {
title: "WPlace ่ชๅจๅพๅ",
toggleOverlay: "ๅๆข่ฆ็ๅฑ",
scanColors: "ๆซๆ้ข่ฒ",
@@ -816,7 +867,7 @@
automation: "่ชๅจๅ",
noChargesThreshold: "โ ็ญๅพ
ๆฌกๆฐ่พพๅฐ {threshold}ใๅฝๅ {current}ใไธๆฌกๅจ {time}...",
},
- "zh-TW": {
+ "zh-tw": {
title: "WPlace ่ชๅๅๅ",
toggleOverlay: "ๅๆ่ฆ่ๅฑค",
scanColors: "ๆๆ้ก่ฒ",
@@ -1152,28 +1203,28 @@
initialSetupComplete: false, // Track if initial startup setup is complete (only happens once)
overlayOpacity: CONFIG.OVERLAY.OPACITY_DEFAULT,
blueMarbleEnabled: CONFIG.OVERLAY.BLUE_MARBLE_DEFAULT,
- ditheringEnabled: true,
- // Advanced color matching settings
- colorMatchingAlgorithm: 'lab',
- enableChromaPenalty: true,
- chromaPenaltyWeight: 0.15,
- customTransparencyThreshold: CONFIG.TRANSPARENCY_THRESHOLD,
- customWhiteThreshold: CONFIG.WHITE_THRESHOLD,
- resizeSettings: null,
- originalImage: null,
- resizeIgnoreMask: null,
- // Notification prefs and runtime bookkeeping
- notificationsEnabled: CONFIG.NOTIFICATIONS.ENABLED,
- notifyOnChargesReached: CONFIG.NOTIFICATIONS.ON_CHARGES_REACHED,
- notifyOnlyWhenUnfocused: CONFIG.NOTIFICATIONS.ONLY_WHEN_UNFOCUSED,
- notificationIntervalMinutes: CONFIG.NOTIFICATIONS.REPEAT_MINUTES,
- _lastChargesNotifyAt: 0,
- _lastChargesBelow: true,
- // Smart save tracking
- _lastSavePixelCount: 0,
- _lastSaveTime: 0,
- _saveInProgress: false,
- paintedMap: null,
+ ditheringEnabled: true,
+ // Advanced color matching settings
+ colorMatchingAlgorithm: 'lab',
+ enableChromaPenalty: true,
+ chromaPenaltyWeight: 0.15,
+ customTransparencyThreshold: CONFIG.TRANSPARENCY_THRESHOLD,
+ customWhiteThreshold: CONFIG.WHITE_THRESHOLD,
+ resizeSettings: null,
+ originalImage: null,
+ resizeIgnoreMask: null,
+ // Notification prefs and runtime bookkeeping
+ notificationsEnabled: CONFIG.NOTIFICATIONS.ENABLED,
+ notifyOnChargesReached: CONFIG.NOTIFICATIONS.ON_CHARGES_REACHED,
+ notifyOnlyWhenUnfocused: CONFIG.NOTIFICATIONS.ONLY_WHEN_UNFOCUSED,
+ notificationIntervalMinutes: CONFIG.NOTIFICATIONS.REPEAT_MINUTES,
+ _lastChargesNotifyAt: 0,
+ _lastChargesBelow: true,
+ // Smart save tracking
+ _lastSavePixelCount: 0,
+ _lastSaveTime: 0,
+ _saveInProgress: false,
+ paintedMap: null,
}
let _updateResizePreview = () => { };
@@ -1206,8 +1257,8 @@
this.disable();
this.imageBitmap = null;
this.chunkedTiles.clear();
- this.originalTiles.clear();
- this.originalTilesData.clear();
+ this.originalTiles.clear();
+ this.originalTilesData.clear();
this.lastProcessedHash = null;
if (this.processPromise) {
this.processPromise = null;
@@ -1274,7 +1325,7 @@
async _doProcessImageIntoChunks() {
const startTime = performance.now();
this.chunkedTiles.clear();
-
+
const { width: imageWidth, height: imageHeight } = this.imageBitmap;
const { x: startPixelX, y: startPixelY } = this.startCoords.pixel;
const { x: startRegionX, y: startRegionY } = this.startCoords.region;
@@ -1303,7 +1354,7 @@
// Process tiles in batches with yielding
for (let i = 0; i < tilesToProcess.length; i += batchSize) {
const batch = tilesToProcess.slice(i, i + batchSize);
-
+
await Promise.all(batch.map(async ({ tx, ty }) => {
const tileKey = `${tx},${ty}`;
const chunkBitmap = await this._processTile(tx, ty, imageWidth, imageHeight, startPixelX, startPixelY, startRegionX, startRegionY);
@@ -1351,18 +1402,18 @@
if (state.blueMarbleEnabled) {
const imageData = chunkCtx.getImageData(dX, dY, sW, sH);
const data = imageData.data;
-
+
// Faster pixel manipulation using typed arrays
for (let i = 0; i < data.length; i += 4) {
const pixelIndex = i / 4;
const pixelY = Math.floor(pixelIndex / sW);
const pixelX = pixelIndex % sW;
-
+
if ((pixelX + pixelY) % 2 === 0 && data[i + 3] > 0) {
data[i + 3] = 0; // Set alpha to 0
}
}
-
+
chunkCtx.putImageData(imageData, dX, dY);
}
@@ -1491,7 +1542,7 @@
const originalBitmap = await createImageBitmap(originalBlob);
const canvas = new OffscreenCanvas(originalBitmap.width, originalBitmap.height);
const ctx = canvas.getContext('2d');
-
+
// Disable antialiasing for pixel-perfect rendering
ctx.imageSmoothingEnabled = false;
@@ -1504,7 +1555,7 @@
ctx.drawImage(overlayBitmap, 0, 0);
// Use faster blob conversion with compression settings
- return await canvas.convertToBlob({
+ return await canvas.convertToBlob({
type: 'image/png',
quality: 0.95 // Slight compression for faster processing
});
@@ -1561,7 +1612,7 @@
}
tokenGenerationInProgress = true;
-
+
try {
console.log("๐ Token expired or missing, generating new one...");
const token = await handleCaptchaWithRetry();
@@ -1700,6 +1751,18 @@
}
});
+ async function detectLanguage() {
+ try {
+ const response = await fetch("https://backend.wplace.live/me", {
+ credentials: "include",
+ })
+ const data = await response.json()
+ state.language = data.language === "pt" ? "pt" : "en"
+ } catch {
+ state.language = navigator.language.startsWith("pt") ? "pt" : "en"
+ }
+ }
+
// UTILITY FUNCTIONS
const Utils = {
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
@@ -1727,7 +1790,7 @@
this.turnstileLoaded = true;
return Promise.resolve();
}
-
+
return new Promise((resolve, reject) => {
// Avoid adding the script twice
if (document.querySelector('script[src^="https://challenges.cloudflare.com/turnstile/v0/api.js"]')) {
@@ -1741,7 +1804,7 @@
};
return checkReady();
}
-
+
const script = document.createElement('script');
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
script.async = true;
@@ -1766,20 +1829,9 @@
if (this._turnstileContainer) {
this._turnstileContainer.remove();
}
-
+
this._turnstileContainer = document.createElement('div');
- this._turnstileContainer.style.cssText = `
- position: fixed !important;
- left: -99999px !important;
- top: -99999px !important;
- width: 1px !important;
- height: 1px !important;
- pointer-events: none !important;
- opacity: 0 !important;
- visibility: hidden !important;
- z-index: -99999 !important;
- overflow: hidden !important;
- `;
+ this._turnstileContainer.className = 'wplace-turnstile-hidden';
this._turnstileContainer.setAttribute('aria-hidden', 'true');
this._turnstileContainer.id = 'turnstile-widget-container';
document.body.appendChild(this._turnstileContainer);
@@ -1795,35 +1847,19 @@
const overlay = document.createElement('div');
overlay.id = 'turnstile-overlay-container';
- overlay.style.cssText = `
- position: fixed !important;
- bottom: 20px !important;
- right: 20px !important;
- z-index: 99999 !important;
- background: rgba(0,0,0,0.9) !important;
- border-radius: 12px !important;
- padding: 20px !important;
- box-shadow: 0 8px 32px rgba(0,0,0,0.4) !important;
- backdrop-filter: blur(10px) !important;
- border: 1px solid rgba(255,255,255,0.2) !important;
- color: white !important;
- font-family: 'Segoe UI', sans-serif !important;
- display: none !important;
- max-width: 350px !important;
- min-width: 300px !important;
- `;
+ overlay.className = 'wplace-turnstile-overlay wplace-overlay-hidden';
const title = document.createElement('div');
title.textContent = 'Cloudflare Turnstile โ please complete the check if shown';
- title.style.cssText = 'font: 600 12px/1.3 "Segoe UI",sans-serif; margin-bottom: 8px; opacity: 0.9;';
+ title.className = 'wplace-turnstile-title';
const host = document.createElement('div');
host.id = 'turnstile-overlay-host';
- host.style.cssText = 'width: 100%; min-height: 70px;';
+ host.className = 'wplace-turnstile-host';
const hideBtn = document.createElement('button');
hideBtn.textContent = 'Hide';
- hideBtn.style.cssText = 'position:absolute; top:6px; right:6px; font-size:11px; background:transparent; color:#fff; border:1px solid rgba(255,255,255,0.2); border-radius:6px; padding:2px 6px; cursor:pointer;';
+ hideBtn.className = 'wplace-turnstile-hide-btn';
hideBtn.addEventListener('click', () => overlay.remove());
overlay.appendChild(title);
@@ -1870,24 +1906,24 @@
try {
// Force cleanup of any existing widget
if (this._turnstileWidgetId && window.turnstile?.remove) {
- try {
- window.turnstile.remove(this._turnstileWidgetId);
+ try {
+ window.turnstile.remove(this._turnstileWidgetId);
console.log('๐งน Cleaned up existing Turnstile widget');
} catch (e) {
console.warn('โ ๏ธ Widget cleanup warning:', e.message);
}
}
-
+
const container = this.ensureTurnstileContainer();
container.innerHTML = '';
-
+
// Verify Turnstile is available
if (!window.turnstile?.render) {
console.error('โ Turnstile not available for rendering');
resolve(null);
return;
}
-
+
console.log('๐ง Creating invisible Turnstile widget...');
const widgetId = window.turnstile.render(container, {
sitekey,
@@ -1902,10 +1938,10 @@
'error-callback': () => resolve(null),
'timeout-callback': () => resolve(null)
});
-
+
this._turnstileWidgetId = widgetId;
this._lastSitekey = sitekey;
-
+
if (!widgetId) {
return resolve(null);
}
@@ -1925,31 +1961,33 @@
async createTurnstileWidgetInteractive(sitekey, action) {
// Create a visible widget that users can interact with if needed
console.log('๐ Creating interactive Turnstile widget (visible)');
-
+
return new Promise((resolve) => {
try {
// Force cleanup of any existing widget
if (this._turnstileWidgetId && window.turnstile?.remove) {
- try {
- window.turnstile.remove(this._turnstileWidgetId);
+ try {
+ window.turnstile.remove(this._turnstileWidgetId);
} catch (e) {
console.warn('โ ๏ธ Widget cleanup warning:', e.message);
}
}
-
+
const overlay = this.ensureTurnstileOverlayContainer();
+ overlay.classList.remove('wplace-overlay-hidden');
overlay.style.display = 'block';
-
+
const host = overlay.querySelector('#turnstile-overlay-host');
host.innerHTML = '';
-
+
// Set a timeout for interactive mode
const timeout = setTimeout(() => {
console.warn('โฐ Interactive Turnstile widget timeout');
+ overlay.classList.add('wplace-overlay-hidden');
overlay.style.display = 'none';
resolve(null);
}, 60000); // 60 seconds for user interaction
-
+
const widgetId = window.turnstile.render(host, {
sitekey,
action,
@@ -1957,9 +1995,10 @@
theme: 'light',
callback: (token) => {
clearTimeout(timeout);
- overlay.style.display = 'none';
+ overlay.classList.add('wplace-overlay-hidden');
+ overlay.style.display = 'none';
console.log('โ
Interactive Turnstile completed successfully');
-
+
if (typeof token === 'string' && token.length > 20) {
resolve(token);
} else {
@@ -1969,17 +2008,19 @@
},
'error-callback': (error) => {
clearTimeout(timeout);
- overlay.style.display = 'none';
+ overlay.classList.add('wplace-overlay-hidden');
+ overlay.style.display = 'none';
console.warn('โ Interactive Turnstile error:', error);
resolve(null);
},
});
-
+
this._turnstileWidgetId = widgetId;
this._lastSitekey = sitekey;
-
+
if (!widgetId) {
clearTimeout(timeout);
+ overlay.classList.add('wplace-overlay-hidden');
overlay.style.display = 'none';
console.warn('โ Failed to create interactive Turnstile widget');
resolve(null);
@@ -2006,15 +2047,15 @@
console.warn('Failed to cleanup Turnstile widget:', e);
}
}
-
+
if (this._turnstileContainer && document.body.contains(this._turnstileContainer)) {
this._turnstileContainer.remove();
}
-
+
if (this._turnstileOverlay && document.body.contains(this._turnstileOverlay)) {
this._turnstileOverlay.remove();
}
-
+
this._turnstileWidgetId = null;
this._turnstileContainer = null;
this._turnstileOverlay = null;
@@ -2092,11 +2133,11 @@
this._cachedSitekey = testSitekey;
return testSitekey;
}
-
+
} catch (error) {
console.warn('Error detecting sitekey:', error);
}
-
+
console.log("๐ Using fallback sitekey:", fallback);
this._cachedSitekey = fallback;
return fallback;
@@ -2152,40 +2193,7 @@
showAlert: (message, type = "info") => {
const alertDiv = document.createElement("div")
- alertDiv.style.cssText = `
- position: fixed;
- top: 20px;
- left: 50%;
- transform: translateX(-50%);
- padding: 12px 20px;
- border-radius: 8px;
- color: white;
- font-weight: 600;
- z-index: 10001;
- max-width: 400px;
- text-align: center;
- box-shadow: 0 4px 12px rgba(0,0,0,0.3);
- animation: slideDown 0.3s ease-out;
- font-family: 'Segoe UI', sans-serif;
- `
-
- const colors = {
- info: "background: linear-gradient(135deg, #3498db, #2980b9);",
- success: "background: linear-gradient(135deg, #27ae60, #229954);",
- warning: "background: linear-gradient(135deg, #f39c12, #e67e22);",
- error: "background: linear-gradient(135deg, #e74c3c, #c0392b);",
- }
-
- alertDiv.style.cssText += colors[type] || colors.info
-
- const style = document.createElement("style")
- style.textContent = `
- @keyframes slideDown {
- from { transform: translateX(-50%) translateY(-20px); opacity: 0; }
- to { transform: translateX(-50%) translateY(0); opacity: 1; }
- }
- `
- document.head.appendChild(style)
+ alertDiv.className = `wplace-alert-base wplace-alert-${type}`
alertDiv.textContent = message
document.body.appendChild(alertDiv)
@@ -2194,7 +2202,6 @@
alertDiv.style.animation = "slideDown 0.3s ease-out reverse"
setTimeout(() => {
document.body.removeChild(alertDiv)
- document.head.removeChild(style)
}, 300)
}, 4000)
},
@@ -2236,7 +2243,7 @@
// Use provided palette or derive from COLOR_MAP
if (!palette || palette.length === 0) {
palette = Object.values(CONFIG.COLOR_MAP)
- .filter(c => c.rgb)
+ .filter(c => c.rgb)
.map(c => [c.rgb.r, c.rgb.g, c.rgb.b]);
}
if (state.colorMatchingAlgorithm === 'legacy') {
@@ -2290,17 +2297,17 @@
},
createImageUploader: () =>
- new Promise((resolve) => {
- const input = document.createElement("input")
- input.type = "file"
- input.accept = "image/png,image/jpeg"
- input.onchange = () => {
- const fr = new FileReader()
- fr.onload = () => resolve(fr.result)
- fr.readAsDataURL(input.files[0])
- }
- input.click()
- }),
+ new Promise((resolve) => {
+ const input = document.createElement("input")
+ input.type = "file"
+ input.accept = "image/png,image/jpeg"
+ input.onchange = () => {
+ const fr = new FileReader()
+ fr.onload = () => resolve(fr.result)
+ fr.readAsDataURL(input.files[0])
+ }
+ input.click()
+ }),
createFileDownloader: (data, filename) => {
const blob = new Blob([data], { type: "application/json" })
@@ -2315,30 +2322,30 @@
},
createFileUploader: () =>
- new Promise((resolve, reject) => {
- const input = document.createElement("input")
- input.type = "file"
- input.accept = ".json"
- input.onchange = (e) => {
- const file = e.target.files[0]
- if (file) {
- const reader = new FileReader()
- reader.onload = () => {
- try {
- const data = JSON.parse(reader.result)
- resolve(data)
- } catch (error) {
- reject(new Error("Invalid JSON file"))
- }
+ new Promise((resolve, reject) => {
+ const input = document.createElement("input")
+ input.type = "file"
+ input.accept = ".json"
+ input.onchange = (e) => {
+ const file = e.target.files[0]
+ if (file) {
+ const reader = new FileReader()
+ reader.onload = () => {
+ try {
+ const data = JSON.parse(reader.result)
+ resolve(data)
+ } catch (error) {
+ reject(new Error("Invalid JSON file"))
}
- reader.onerror = () => reject(new Error("File reading error"))
- reader.readAsText(file)
- } else {
- reject(new Error("No file selected"))
}
+ reader.onerror = () => reject(new Error("File reading error"))
+ reader.readAsText(file)
+ } else {
+ reject(new Error("No file selected"))
}
- input.click()
- }),
+ }
+ input.click()
+ }),
extractAvailableColors: () => {
const colorElements = document.querySelectorAll('[id^="color-"]')
@@ -2431,8 +2438,8 @@
markPixelPainted: (x, y, regionX = 0, regionY = 0) => {
const actualX = x + regionX;
const actualY = y + regionY;
-
- if (state.paintedMap && state.paintedMap[actualY] &&
+
+ if (state.paintedMap && state.paintedMap[actualY] &&
actualX >= 0 && actualX < state.paintedMap[actualY].length) {
state.paintedMap[actualY][actualX] = true;
}
@@ -2441,8 +2448,8 @@
isPixelPainted: (x, y, regionX = 0, regionY = 0) => {
const actualX = x + regionX;
const actualY = y + regionY;
-
- if (state.paintedMap && state.paintedMap[actualY] &&
+
+ if (state.paintedMap && state.paintedMap[actualY] &&
actualX >= 0 && actualX < state.paintedMap[actualY].length) {
return state.paintedMap[actualY][actualX];
}
@@ -2454,28 +2461,28 @@
const now = Date.now();
const pixelsSinceLastSave = state.paintedPixels - state._lastSavePixelCount;
const timeSinceLastSave = now - state._lastSaveTime;
-
+
// Save conditions:
// 1. Every 25 pixels (reduced from 50 for more frequent saves)
// 2. At least 30 seconds since last save (prevent spam)
// 3. Not already saving
- return !state._saveInProgress &&
- pixelsSinceLastSave >= 25 &&
- timeSinceLastSave >= 30000;
+ return !state._saveInProgress &&
+ pixelsSinceLastSave >= 25 &&
+ timeSinceLastSave >= 30000;
},
performSmartSave: () => {
if (!Utils.shouldAutoSave()) return false;
-
+
state._saveInProgress = true;
const success = Utils.saveProgress();
-
+
if (success) {
state._lastSavePixelCount = state.paintedPixels;
state._lastSaveTime = Date.now();
console.log(`๐พ Auto-saved at ${state.paintedPixels} pixels`);
}
-
+
state._saveInProgress = false;
return success;
},
@@ -2529,7 +2536,7 @@
migrateProgressToV2: (saved) => {
if (!saved) return saved;
const isV1 = !saved.version || saved.version === '1' || saved.version === '1.0' || saved.version === '1.1';
- if (!isV1) return saved;
+ if (!isV1) return saved;
try {
const migrated = { ...saved };
@@ -2574,7 +2581,7 @@
}
},
- saveProgress: () => {
+ saveProgress: () => {
try {
// Pack painted map if available
let paintedMapPacked = null;
@@ -2603,13 +2610,13 @@
availableColors: state.availableColors,
},
imageData: state.imageData
- ? {
- width: state.imageData.width,
- height: state.imageData.height,
- pixels: Array.from(state.imageData.pixels),
- totalPixels: state.imageData.totalPixels,
- }
- : null,
+ ? {
+ width: state.imageData.width,
+ height: state.imageData.height,
+ pixels: Array.from(state.imageData.pixels),
+ totalPixels: state.imageData.totalPixels,
+ }
+ : null,
paintedMapPacked: paintedMapPacked,
}
@@ -2679,7 +2686,7 @@
const imageData = new ImageData(state.imageData.pixels, state.imageData.width, state.imageData.height);
ctx.putImageData(imageData, 0, 0);
const proc = new ImageProcessor('');
- proc.img = canvas;
+ proc.img = canvas;
proc.canvas = canvas;
proc.ctx = ctx;
state.imageData.processor = proc;
@@ -2703,7 +2710,7 @@
}
},
- saveProgressToFile: () => {
+ saveProgressToFile: () => {
try {
// Pack painted map if available
let paintedMapPacked = null;
@@ -2732,13 +2739,13 @@
availableColors: state.availableColors,
},
imageData: state.imageData
- ? {
- width: state.imageData.width,
- height: state.imageData.height,
- pixels: Array.from(state.imageData.pixels),
- totalPixels: state.imageData.totalPixels,
- }
- : null,
+ ? {
+ width: state.imageData.width,
+ height: state.imageData.height,
+ pixels: Array.from(state.imageData.pixels),
+ totalPixels: state.imageData.totalPixels,
+ }
+ : null,
paintedMapPacked: paintedMapPacked,
}
@@ -2751,7 +2758,7 @@
}
},
- loadProgressFromFile: async () => {
+ loadProgressFromFile: async () => {
try {
const data = await Utils.createFileUploader()
if (!data || !data.state) {
@@ -2765,7 +2772,7 @@
} else {
migrated = Utils.migrateProgressToV21(data) || data;
}
- const success = Utils.restoreProgress(migrated)
+ const success = Utils.restoreProgress(migrated)
return success
} catch (error) {
console.error("Error loading from file:", error)
@@ -2782,9 +2789,9 @@
try {
// Recreate ImageBitmap from loaded pixel data
const imageData = new ImageData(
- state.imageData.pixels,
- state.imageData.width,
- state.imageData.height
+ state.imageData.pixels,
+ state.imageData.width,
+ state.imageData.height
);
const canvas = new OffscreenCanvas(state.imageData.width, state.imageData.height);
@@ -2933,96 +2940,96 @@
// Desktop Notification Manager
const NotificationManager = {
- pollTimer: null,
- pollIntervalMs: 60_000,
- icon() {
- const link = document.querySelector("link[rel~='icon']");
- return link?.href || (location.origin + "/favicon.ico");
- },
- async requestPermission() {
- if (!("Notification" in window)) {
- Utils.showAlert("Notifications are not supported in this browser.", "warning");
- return "denied";
- }
- if (Notification.permission === "granted") return "granted";
- try {
- const perm = await Notification.requestPermission();
- return perm;
- } catch {
- return Notification.permission;
- }
- },
- canNotify() {
- return state.notificationsEnabled &&
- typeof Notification !== "undefined" &&
- Notification.permission === "granted";
- },
- notify(title, body, tag = "wplace-charges", force = false) {
- if (!this.canNotify()) return false;
- if (!force && state.notifyOnlyWhenUnfocused && document.hasFocus()) return false;
- try {
- new Notification(title, {
- body,
- tag,
- renotify: true,
- icon: this.icon(),
- badge: this.icon(),
- silent: false,
- });
- return true;
- } catch {
- // Graceful fallback
- Utils.showAlert(body, "info");
- return false;
- }
- },
- resetEdgeTracking() {
- state._lastChargesBelow = state.currentCharges < state.cooldownChargeThreshold;
- state._lastChargesNotifyAt = 0;
- },
- maybeNotifyChargesReached(force = false) {
- if (!state.notificationsEnabled || !state.notifyOnChargesReached) return;
- const reached = state.currentCharges >= state.cooldownChargeThreshold;
- const now = Date.now();
- const repeatMs = Math.max(1, Number(state.notificationIntervalMinutes || 5)) * 60_000;
- if (reached) {
- const shouldEdge = state._lastChargesBelow || force;
- const shouldRepeat = now - (state._lastChargesNotifyAt || 0) >= repeatMs;
- if (shouldEdge || shouldRepeat) {
- const msg = `Charges ready: ${Math.floor(state.currentCharges)} / ${state.maxCharges}. Threshold: ${state.cooldownChargeThreshold}.`;
- this.notify("WPlace โ Charges Ready", msg, "wplace-notify-charges");
- state._lastChargesNotifyAt = now;
- }
- state._lastChargesBelow = false;
- } else {
- state._lastChargesBelow = true;
- }
- },
- startPolling() {
- this.stopPolling();
- if (!state.notificationsEnabled || !state.notifyOnChargesReached) return;
- // lightweight background polling
- this.pollTimer = setInterval(async () => {
- try {
- const { charges, cooldown, max } = await WPlaceService.getCharges();
- state.currentCharges = Math.floor(charges);
- state.cooldown = cooldown;
- state.maxCharges = Math.max(1, Math.floor(max));
- this.maybeNotifyChargesReached();
- } catch { /* ignore */ }
- }, this.pollIntervalMs);
- },
- stopPolling() {
- if (this.pollTimer) {
- clearInterval(this.pollTimer);
- this.pollTimer = null;
- }
- },
- syncFromState() {
- this.resetEdgeTracking();
- if (state.notificationsEnabled && state.notifyOnChargesReached) this.startPolling();
- else this.stopPolling();
- },
+ pollTimer: null,
+ pollIntervalMs: 60_000,
+ icon() {
+ const link = document.querySelector("link[rel~='icon']");
+ return link?.href || (location.origin + "/favicon.ico");
+ },
+ async requestPermission() {
+ if (!("Notification" in window)) {
+ Utils.showAlert("Notifications are not supported in this browser.", "warning");
+ return "denied";
+ }
+ if (Notification.permission === "granted") return "granted";
+ try {
+ const perm = await Notification.requestPermission();
+ return perm;
+ } catch {
+ return Notification.permission;
+ }
+ },
+ canNotify() {
+ return state.notificationsEnabled &&
+ typeof Notification !== "undefined" &&
+ Notification.permission === "granted";
+ },
+ notify(title, body, tag = "wplace-charges", force = false) {
+ if (!this.canNotify()) return false;
+ if (!force && state.notifyOnlyWhenUnfocused && document.hasFocus()) return false;
+ try {
+ new Notification(title, {
+ body,
+ tag,
+ renotify: true,
+ icon: this.icon(),
+ badge: this.icon(),
+ silent: false,
+ });
+ return true;
+ } catch {
+ // Graceful fallback
+ Utils.showAlert(body, "info");
+ return false;
+ }
+ },
+ resetEdgeTracking() {
+ state._lastChargesBelow = state.currentCharges < state.cooldownChargeThreshold;
+ state._lastChargesNotifyAt = 0;
+ },
+ maybeNotifyChargesReached(force = false) {
+ if (!state.notificationsEnabled || !state.notifyOnChargesReached) return;
+ const reached = state.currentCharges >= state.cooldownChargeThreshold;
+ const now = Date.now();
+ const repeatMs = Math.max(1, Number(state.notificationIntervalMinutes || 5)) * 60_000;
+ if (reached) {
+ const shouldEdge = state._lastChargesBelow || force;
+ const shouldRepeat = now - (state._lastChargesNotifyAt || 0) >= repeatMs;
+ if (shouldEdge || shouldRepeat) {
+ const msg = `Charges ready: ${Math.floor(state.currentCharges)} / ${state.maxCharges}. Threshold: ${state.cooldownChargeThreshold}.`;
+ this.notify("WPlace โ Charges Ready", msg, "wplace-notify-charges");
+ state._lastChargesNotifyAt = now;
+ }
+ state._lastChargesBelow = false;
+ } else {
+ state._lastChargesBelow = true;
+ }
+ },
+ startPolling() {
+ this.stopPolling();
+ if (!state.notificationsEnabled || !state.notifyOnChargesReached) return;
+ // lightweight background polling
+ this.pollTimer = setInterval(async () => {
+ try {
+ const { charges, cooldown, max } = await WPlaceService.getCharges();
+ state.currentCharges = Math.floor(charges);
+ state.cooldown = cooldown;
+ state.maxCharges = Math.max(1, Math.floor(max));
+ this.maybeNotifyChargesReached();
+ } catch { /* ignore */ }
+ }, this.pollIntervalMs);
+ },
+ stopPolling() {
+ if (this.pollTimer) {
+ clearInterval(this.pollTimer);
+ this.pollTimer = null;
+ }
+ },
+ syncFromState() {
+ this.resetEdgeTracking();
+ if (state.notificationsEnabled && state.notifyOnChargesReached) this.startPolling();
+ else this.stopPolling();
+ },
};
// COLOR MATCHING FUNCTION - Optimized with caching
@@ -3127,7 +3134,7 @@
// Don't re-fetch colors here, use what was captured when user clicked upload
if (!state.availableColors || state.availableColors.length === 0) {
// If no colors have been captured yet, show message
- colorsContainer.innerHTML = 'Upload an image first to capture available colors
';
+ colorsContainer.innerHTML = 'Upload an image first to capture available colors
';
return;
}
@@ -3146,7 +3153,7 @@
// 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
+ 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
@@ -3211,29 +3218,29 @@
}
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 = Utils.detectSitekey();
console.log("๐ Generating Turnstile token for sitekey:", sitekey);
console.log('๐งญ UA:', navigator.userAgent.substring(0, 50) + '...', 'Platform:', navigator.platform);
-
+
// Add additional checks before token generation
if (!window.turnstile) {
await Utils.loadTurnstile();
}
-
+
const token = await Utils.generatePaintToken(sitekey);
-
+
console.log(`๐ Token received - Type: ${typeof token}, Value: ${token ? (typeof token === 'string' ? (token.length > 50 ? token.substring(0, 50) + '...' : token) : JSON.stringify(token)) : 'null/undefined'}, Length: ${token?.length || 0}`);
-
+
if (typeof token === 'string' && token.length > 20) {
const duration = Math.round(performance.now() - startTime);
console.log(`โ
Turnstile token generated successfully in ${duration}ms`);
@@ -3244,7 +3251,7 @@
} 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...");
@@ -3329,6 +3336,8 @@
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")
@@ -3345,6 +3354,7 @@
loadLanguagePreference()
const theme = getCurrentTheme()
+ applyTheme() // <- new: set CSS vars and theme class before building UI
const fontAwesome = document.createElement("link")
fontAwesome.rel = "stylesheet"
@@ -3358,1155 +3368,12 @@
document.head.appendChild(googleFonts)
}
- const style = document.createElement("style")
- style.setAttribute("data-wplace-theme", "true")
-
- style.textContent = `
- ${theme.animations.glow
- ? `
- @keyframes neonGlow {
- 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;
- }
- }`
- : ""
- }
-
- ${theme.animations.pixelBlink
- ? `
- @keyframes pixelBlink {
- 0%, 50% { opacity: 1; }
- 51%, 100% { opacity: 0.7; }
- }`
- : ""
- }
-
- ${theme.animations.scanline
- ? `
- @keyframes scanline {
- 0% { transform: translateY(-100%); }
- 100% { transform: translateY(400px); }
- }`
- : ""
- }
-
- @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); }
- }
- @keyframes slideIn {
- from { transform: translateY(-10px); opacity: 0; }
- to { transform: translateY(0); opacity: 1; }
- }
- @keyframes shimmer {
- 0% { transform: translateX(-100%); }
- 100% { transform: translateX(100%); }
- }
-
- #wplace-image-bot-container {
- position: fixed;
- top: 20px;
- left: 20px;
- width: ${CONFIG.currentTheme === "Neon Retro" ? "280px" : "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-y: auto; /* Allow scrolling for main panel */
- overflow-x: hidden;
- ${theme.backdropFilter ? `backdrop-filter: ${theme.backdropFilter};` : ""}
- transition: all 0.3s ease;
- user-select: none;
- ${CONFIG.currentTheme === "Neon Retro" ? "image-rendering: pixelated;" : ""}
- }
-
- ${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;
- }`
- : ""
- }
-
- ${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
- );
- pointer-events: none;
- z-index: 1;
- }`
- : ""
- }
-
- #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;
- }
- #wplace-image-bot-container.wplace-minimized {
- width: 200px;
- height: auto;
- overflow: hidden;
- }
- #wplace-image-bot-container.wplace-compact {
- width: 240px;
- }
-
- /* Stats Container */
- #wplace-stats-container {
- position: fixed;
- top: 20px;
- left: 330px;
- width: ${CONFIG.currentTheme === "Neon Retro" ? "280px" : "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: 9997;
- font-family: ${theme.fontFamily};
- color: ${theme.text};
- animation: slideIn 0.4s ease-out;
- overflow-y: auto; /* Make stats panel scrollable */
- ${theme.backdropFilter ? `backdrop-filter: ${theme.backdropFilter};` : ""}
- transition: all 0.3s ease;
- user-select: none;
- ${CONFIG.currentTheme === "Neon Retro" ? "image-rendering: pixelated;" : ""}
- }
-
- /* FIX: Disable transition during drag to prevent lag */
- #wplace-stats-container.wplace-dragging {
- transition: none;
- }
-
- .wplace-header {
- padding: ${CONFIG.currentTheme === "Neon Retro" ? "8px 12px" : "8px 12px"};
- background: ${CONFIG.currentTheme === "Classic Autobot"
- ? `linear-gradient(135deg, ${theme.secondary} 0%, #2a2a2a 100%)`
- : theme.secondary
- };
- color: ${theme.highlight};
- font-size: ${CONFIG.currentTheme === "Neon Retro" ? "11px" : "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;" : ""}
- }
-
- .wplace-header-title {
- display: flex;
- align-items: center;
- gap: ${CONFIG.currentTheme === "Neon Retro" ? "6px" : "6px"};
- }
-
- .wplace-header-controls {
- display: flex;
- gap: ${CONFIG.currentTheme === "Neon Retro" ? "6px" : "6px"};
- }
-
- .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};` : ""}
- }
-
- .wplace-content {
- padding: ${CONFIG.currentTheme === "Neon Retro" ? "12px" : "12px"};
- display: block;
- position: relative;
- z-index: 2;
- }
- .wplace-content.wplace-hidden {
- display: none;
- }
-
- .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);
- }
-
- .wplace-section {
- margin-bottom: ${CONFIG.currentTheme === "Neon Retro" ? "12px" : "12px"};
- padding: 12px;
- background: rgba(255,255,255,0.03);
- border-radius: ${theme.borderRadius};
- border: 1px solid rgba(255,255,255,0.1);
- }
-
- .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;
- }
-
- .wplace-controls {
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
- .wplace-row {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 8px;
- }
- .wplace-row.single {
- grid-template-columns: 1fr;
- }
-
- .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);" : ""}
- }
-
- ${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%;
- }`
- }
-
- .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;" : ""}
- }
- .wplace-btn:active:not(:disabled) {
- transform: translateY(0);
- }
-
- .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};` : ""}
- }
- .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;
- }
- .wplace-btn:disabled::before {
- display: none;
- }
-
- .wplace-btn-overlay.active {
- background: linear-gradient(135deg, #29b6f6 0%, #8e2de2 100%);
- box-shadow: 0 0 15px #8e2de2;
- }
-
- .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);" : ""}
- }
-
- .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};
- }
-
- .wplace-colors-section {
- margin-top: 10px;
- padding-top: 8px;
- border-top: 1px solid rgba(255,255,255,0.05);
- }
-
- .wplace-stat-colors-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(16px, 1fr));
- gap: 4px;
- margin-top: 8px;
- padding: 4px;
- background: rgba(0,0,0,0.2);
- border-radius: 4px;
- max-height: 80px; /* Limit height and allow scrolling */
- overflow-y: auto;
- }
-
- .wplace-stat-color-swatch {
- width: 16px;
- height: 16px;
- border-radius: 3px;
- border: 1px solid rgba(255,255,255,0.1);
- box-shadow: inset 0 0 2px rgba(0,0,0,0.5);
- }
-
- .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;
- }
-
- ${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;
- }`
- : ""
- }
-
- .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})`
- };
- 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;
- }
-
- .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};
- }
-
- .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)"
- };
- width: 90%;
- max-width: 700px;
- max-height: 90%;
- overflow: auto;
- font-family: ${theme.fontFamily};
- }
-
- .resize-preview-wrapper {
- display: flex;
- justify-content: center;
- align-items: center;
- border: 1px solid ${theme.accent};
- background: rgba(0,0,0,0.2);
- margin: 15px 0;
- height: 300px;
- overflow: hidden;
- }
-
- .resize-canvas-stack { position: relative; transform-origin: center center; display: inline-block; }
- .resize-base-canvas, .resize-mask-canvas {
- position: absolute; left: 0; top: 0;
- image-rendering: pixelated;
- image-rendering: -moz-crisp-edges;
- image-rendering: crisp-edges;
- }
- .resize-mask-canvas { pointer-events: auto; }
- .resize-tools { display:flex; gap:8px; align-items:center; margin-top:8px; font-size:12px; }
- .resize-tools button { padding:6px 10px; border-radius:6px; border:1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.06); color:#fff; cursor:pointer; }
- .wplace-btn.active,
- .wplace-btn[aria-pressed="true"] {
- background: ${theme.highlight} !important;
- color: ${theme.primary} !important;
- border-color: ${theme.text} !important;
- box-shadow: 0 0 8px rgba(0,0,0,0.25) inset, 0 0 6px rgba(0,0,0,0.2) !important;
- }
- .wplace-btn.active i,
- .wplace-btn[aria-pressed="true"] i { filter: drop-shadow(0 0 3px ${theme.primary}); }
- .mask-mode-group .wplace-btn.active,
- .mask-mode-group .wplace-btn[aria-pressed="true"] {
- background: ${theme.highlight};
- color: ${theme.primary};
- border-color: ${theme.text};
- box-shadow: 0 0 8px rgba(0,0,0,0.25) inset, 0 0 6px rgba(0,0,0,0.2);
- }
-
- .resize-controls {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 15px;
- align-items: center;
- }
-
- .resize-controls label {
- font-size: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "12px"};
- ${CONFIG.currentTheme === "Neon Retro" ? "text-transform: uppercase; letter-spacing: 1px;" : ""}
- color: ${theme.text};
- }
-
- .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;
- }
-
- ${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};
- }
-
- .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};
- }`
- : ""
- }
-
- .resize-zoom-controls {
- grid-column: 1 / -1;
- display: flex;
- align-items: center;
- gap: 10px;
- margin-top: 15px;
- }
-
- .resize-buttons {
- display: flex;
- gap: 10px;
- justify-content: center;
- margin-top: 20px;
- }
-
- .resize-overlay {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.8);
- z-index: 9999;
- display: none;
- }
- .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;
- color: #ccc;
- text-align: center;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- width: 100%;
- }
- .wplace-color-swatch {
- width: 22px;
- height: 22px;
- border: 1px solid rgba(255,255,255,0.2);
- border-radius: 4px;
- cursor: pointer;
- transition: transform 0.1s ease, box-shadow 0.2s ease;
- position: relative;
- margin: 0 auto;
- }
- .wplace-color-swatch.unavailable {
- border-color: #666;
- border-style: dashed;
- cursor: not-allowed;
- }
- .wplace-color-swatch:hover {
- transform: scale(1.1);
- z-index: 1;
- }
- .wplace-color-swatch:not(.active) {
- opacity: 0.3;
- filter: grayscale(80%);
- }
- .wplace-color-swatch.unavailable:not(.active) {
- opacity: 0.2;
- filter: grayscale(90%);
- }
- .wplace-color-swatch.active::after {
- content: 'โ';
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- color: white;
- font-size: 12px;
- font-weight: bold;
- text-shadow: 0 0 3px black;
- }
- .wplace-color-divider {
- border: none;
- height: 1px;
- background: rgba(255,255,255,0.1);
- margin: 8px 0;
- }
-
- .wplace-cooldown-control {
- margin-top: 8px;
- }
- .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;
- background: #444;
- border-radius: 2px;
- outline: none;
- }
- .wplace-slider::-webkit-slider-thumb {
- -webkit-appearance: none;
- width: 14px;
- height: 14px;
- background: ${theme.highlight};
- border-radius: 50%;
- cursor: pointer;
- }
-
-
- ${CONFIG.currentTheme === "Neon Retro"
- ? `
- 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;
- }
-
- input[type="checkbox"]:checked {
- background: ${theme.success};
- }
-
- input[type="checkbox"]:checked::after {
- content: 'โ';
- position: absolute;
- top: -2px;
- left: 1px;
- color: ${theme.primary};
- font-size: 12px;
- font-weight: bold;
- }
-
- .fas, .fa {
- filter: drop-shadow(0 0 3px currentColor);
- }
-
- .wplace-speed-control {
- margin-top: 12px;
- padding: 12px;
- background: ${theme.secondary};
- border: ${theme.borderWidth} ${theme.borderStyle} ${theme.accent};
- border-radius: ${theme.borderRadius};
- backdrop-filter: ${theme.backdropFilter};
- }
-
- .wplace-speed-label {
- display: flex;
- align-items: center;
- margin-bottom: 8px;
- color: ${theme.text};
- font-size: 13px;
- font-weight: 600;
- }
-
- .wplace-speed-label i {
- margin-right: 6px;
- color: ${theme.highlight};
- }
-
- .wplace-speed-slider-container {
- display: flex;
- align-items: center;
- gap: 12px;
- }
-
- .wplace-speed-slider {
- flex: 1;
- height: 6px;
- border-radius: 3px;
- background: ${theme.primary};
- outline: none;
- cursor: pointer;
- -webkit-appearance: none;
- appearance: none;
- }
-
- .wplace-speed-slider::-webkit-slider-thumb {
- -webkit-appearance: none;
- appearance: none;
- width: 18px;
- height: 18px;
- border-radius: 50%;
- background: ${theme.highlight};
- cursor: pointer;
- border: 2px solid ${theme.text};
- box-shadow: ${theme.boxShadow};
- }
-
- .wplace-speed-slider::-moz-range-thumb {
- width: 18px;
- height: 18px;
- border-radius: 50%;
- background: ${theme.highlight};
- cursor: pointer;
- border: 2px solid ${theme.text};
- box-shadow: ${theme.boxShadow};
- }
-
- .wplace-speed-display {
- display: flex;
- align-items: center;
- gap: 4px;
- min-width: 90px;
- justify-content: flex-end;
- }
-
- #speedValue {
- color: ${theme.highlight};
- font-weight: 600;
- font-size: 14px;
- }
-
- .wplace-speed-unit {
- color: ${theme.text};
- font-size: 11px;
- opacity: 0.8;
- }
-
- #wplace-settings-container {
- position: fixed;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- z-index: 10001;
- min-width: 400px;
- max-width: 500px;
- background: ${theme.primary};
- border: ${theme.borderWidth} ${theme.borderStyle} ${theme.accent};
- border-radius: ${theme.borderRadius};
- box-shadow: ${theme.boxShadow};
- backdrop-filter: ${theme.backdropFilter};
- }
-
- .wplace-settings {
- padding: 16px;
- max-height: 400px;
- overflow-y: auto;
- }
-
- .wplace-setting-section {
- margin-bottom: 20px;
- padding: 12px;
- background: ${theme.secondary};
- border: ${theme.borderWidth} ${theme.borderStyle} ${theme.accent};
- border-radius: ${theme.borderRadius};
- }
-
- .wplace-setting-title {
- display: flex;
- align-items: center;
- margin-bottom: 12px;
- color: ${theme.text};
- font-size: 14px;
- font-weight: 600;
- }
-
- .wplace-setting-title i {
- margin-right: 8px;
- color: ${theme.highlight};
- }
-
- .wplace-setting-content {
- color: ${theme.text};
- }
-
- .wplace-section {
- margin-bottom: 20px;
- padding: 15px;
- background: ${theme.secondary};
- border: ${theme.borderWidth} ${theme.borderStyle} ${theme.accent};
- border-radius: ${theme.borderRadius};
- }
-
- .wplace-section-title {
- display: flex;
- align-items: center;
- margin-bottom: 15px;
- color: ${theme.text};
- font-size: 14px;
- font-weight: 600;
- }
-
- .wplace-section-title i {
- margin-right: 8px;
- color: ${theme.highlight};
- }
-
- .wplace-speed-container {
- display: flex;
- align-items: center;
- gap: 15px;
- margin-bottom: 10px;
- }
-
- .wplace-slider {
- flex: 1;
- height: 6px;
- background: ${theme.accent};
- border-radius: 3px;
- outline: none;
- -webkit-appearance: none;
- }
-
- .wplace-slider::-webkit-slider-thumb {
- -webkit-appearance: none;
- width: 18px;
- height: 18px;
- background: ${theme.highlight};
- border-radius: 50%;
- cursor: pointer;
- border: 2px solid ${theme.primary};
- }
-
- .wplace-speed-display {
- background: ${theme.accent};
- padding: 5px 10px;
- border-radius: 4px;
- color: ${theme.text};
- font-weight: 600;
- min-width: 80px;
- text-align: center;
- border: ${theme.borderWidth} ${theme.borderStyle} ${theme.highlight};
- }
-
- .wplace-select {
- width: 100%;
- padding: 8px 12px;
- background: ${theme.secondary};
- border: ${theme.borderWidth} ${theme.borderStyle} ${theme.accent};
- border-radius: ${theme.borderRadius};
- color: ${theme.text};
- font-size: 14px;
- margin-bottom: 10px;
- }
-
- .wplace-select:focus {
- outline: none;
- border-color: ${theme.highlight};
- }
-
- .wplace-description {
- color: ${theme.text};
- font-size: 12px;
- opacity: 0.8;
- line-height: 1.4;
- }
-
- .wplace-theme-custom {
- margin-top: 15px;
- padding: 15px;
- background: ${theme.accent};
- border-radius: ${theme.borderRadius};
- border: ${theme.borderWidth} ${theme.borderStyle} ${theme.highlight};
- }
-
- .wplace-custom-group {
- margin-bottom: 15px;
- }
-
- .wplace-custom-label {
- display: flex;
- align-items: center;
- margin-bottom: 8px;
- color: ${theme.text};
- font-size: 13px;
- font-weight: 600;
- }
-
- .wplace-custom-label i {
- margin-right: 8px;
- color: ${theme.highlight};
- width: 16px;
- }
-
- .wplace-color-input-group {
- display: flex;
- gap: 8px;
- align-items: center;
- }
-
- .wplace-color-input {
- width: 50px;
- height: 30px;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- background: transparent;
- }
-
- .wplace-color-text {
- flex: 1;
- padding: 6px 10px;
- background: ${theme.secondary};
- border: ${theme.borderWidth} ${theme.borderStyle} ${theme.accent};
- border-radius: 4px;
- color: ${theme.text};
- font-size: 12px;
- font-family: monospace;
- }
-
- .wplace-animation-controls {
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
-
- .wplace-checkbox-label {
- display: flex;
- align-items: center;
- gap: 8px;
- color: ${theme.text};
- font-size: 12px;
- cursor: pointer;
- }
-
- .wplace-checkbox-label input[type="checkbox"] {
- accent-color: ${theme.highlight};
- }
-
- .wplace-slider-container {
- display: flex;
- align-items: center;
- gap: 10px;
- }
-
- .wplace-slider-container .wplace-slider {
- flex: 1;
- }
-
- .wplace-slider-container span {
- color: ${theme.text};
- font-size: 12px;
- font-weight: 600;
- min-width: 40px;
- }
-
- .wplace-custom-actions {
- display: flex;
- gap: 10px;
- margin-top: 20px;
- border-top: 1px solid ${theme.accent};
- padding-top: 15px;
- }
-
- .wplace-btn-secondary {
- background: ${theme.accent};
- color: ${theme.text};
- border: ${theme.borderWidth} ${theme.borderStyle} ${theme.highlight};
- }
-
- .wplace-btn-secondary:hover {
- background: ${theme.secondary};
- }`
- : ""
- }
- `
- document.head.appendChild(style)
+ // Link external CSS files
+ const cssLink = document.createElement('link');
+ cssLink.rel = 'stylesheet';
+ cssLink.href = 'https://staninna.github.io/WPlace-AutoBOT/auto-image-styles.css'; // TODO: Before merge change to https://raw.githubusercontent.com/Wplace-AutoBot/WPlace-AutoBOT/refs/heads/main/auto-image-styles.css
+ cssLink.setAttribute('data-wplace-theme', 'true');
+ document.head.appendChild(cssLink);
const container = document.createElement("div")
container.id = "wplace-image-bot-container"
@@ -4595,7 +3462,7 @@
- ${state.cooldownChargeThreshold}
+ ${state.cooldownChargeThreshold}
@@ -4663,25 +3530,19 @@
// 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.style.cssText = `
- position: fixed;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- background: ${themeBackground};
- border: ${theme.borderWidth || '1px'} ${theme.borderStyle || 'solid'} ${theme.accent || 'rgba(255,255,255,0.1)'};
- border-radius: ${theme.borderRadius || '16px'};
- padding: 0;
- z-index: 10002;
- display: none;
+ 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)'};
@@ -4692,171 +3553,85 @@
box-shadow: ${theme.boxShadow || '0 20px 40px rgba(0,0,0,0.3)'},
0 0 30px ${theme.highlight || theme.neon || '#00ffff'};
` : ''}
- `
+ `;
settingsContainer.innerHTML = `
-