// ==UserScript==
// @name Wplace Overlay Pro
// @namespace http://tampermonkey.net/
// @version 2.0.0
// @description Manage multiple sharable overlays (with import/export), render them behind tiles on wplace.live.
// @author shinkonet
// @match https://wplace.live/*
// @license MIT
// @grant GM_setValue
// @grant GM_getValue
// @grant GM.setValue
// @grant GM.getValue
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @connect *
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
// ---------- Constants ----------
const TILE_SIZE = 1000;
// ---------- GM helpers (support GM.* and GM_*) ----------
const gmGet = (key, def) => {
try {
if (typeof GM !== 'undefined' && typeof GM.getValue === 'function') return GM.getValue(key, def);
if (typeof GM_getValue === 'function') return Promise.resolve(GM_getValue(key, def));
} catch {}
return Promise.resolve(def);
};
const gmSet = (key, value) => {
try {
if (typeof GM !== 'undefined' && typeof GM.setValue === 'function') return GM.setValue(key, value);
if (typeof GM_setValue === 'function') return Promise.resolve(GM_setValue(key, value));
} catch {}
return Promise.resolve();
};
// Cross-origin blob fetch via GM_xmlhttpRequest
function gmFetchBlob(url) {
return new Promise((resolve, reject) => {
try {
GM_xmlhttpRequest({
method: 'GET',
url,
responseType: 'blob',
onload: (res) => {
if (res.status >= 200 && res.status < 300 && res.response) {
resolve(res.response);
} else {
reject(new Error(`GM_xhr failed: ${res.status} ${res.statusText}`));
}
},
onerror: (e) => reject(new Error('GM_xhr network error')),
ontimeout: () => reject(new Error('GM_xhr timeout')),
});
} catch (e) {
reject(e);
}
});
}
function blobToDataURL(blob) {
return new Promise((resolve, reject) => {
const fr = new FileReader();
fr.onload = () => resolve(fr.result);
fr.onerror = reject;
fr.readAsDataURL(blob);
});
}
async function urlToDataURL(url) {
const blob = await gmFetchBlob(url);
if (!blob || !String(blob.type).startsWith('image/')) {
throw new Error('URL did not return an image blob');
}
return await blobToDataURL(blob);
}
const config = {
overlays: [],
activeOverlayId: null,
overlayMode: 'overlay',
isPanelCollapsed: false,
autoCapturePixelUrl: false
};
const CONFIG_KEYS = Object.keys(config);
async function loadConfig() {
try {
await Promise.all(CONFIG_KEYS.map(async k => { config[k] = await gmGet(k, config[k]); }));
} catch (e) {
console.error("Overlay Pro: Failed to load config", e);
}
}
async function saveConfig(keys = CONFIG_KEYS) {
try {
await Promise.all(keys.map(k => gmSet(k, config[k])));
} catch (e) {
console.error("Overlay Pro: Failed to save config", e);
}
}
const page = unsafeWindow;
function uid() {
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
}
function createCanvas(w, h) {
if (typeof OffscreenCanvas !== 'undefined') return new OffscreenCanvas(w, h);
const c = document.createElement('canvas'); c.width = w; c.height = h; return c;
}
function canvasToBlob(canvas) {
if (canvas.convertToBlob) return canvas.convertToBlob(); // OffscreenCanvas
return new Promise((resolve, reject) => canvas.toBlob(b => b ? resolve(b) : reject(new Error("toBlob failed")), "image/png"));
}
async function blobToImage(blob) {
if (typeof createImageBitmap === 'function') {
try { return await createImageBitmap(blob); } catch {/* fallback below */}
}
return new Promise((resolve, reject) => {
const url = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => { URL.revokeObjectURL(url); resolve(img); };
img.onerror = (e) => { URL.revokeObjectURL(url); reject(e); };
img.src = url;
});
}
function loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
});
}
function extractPixelCoords(pixelUrl) {
try {
const u = new URL(pixelUrl);
const parts = u.pathname.split('/');
const sp = new URLSearchParams(u.search);
return {
chunk1: parseInt(parts[3], 10),
chunk2: parseInt(parts[4], 10),
posX: parseInt(sp.get('x') || '0', 10),
posY: parseInt(sp.get('y') || '0', 10),
};
} catch {
return { chunk1: 0, chunk2: 0, posX: 0, posY: 0 };
}
}
function matchTileUrl(urlStr) {
try {
const u = new URL(urlStr, location.href);
if (u.hostname !== 'backend.wplace.live' || !u.pathname.startsWith('/files/')) return null;
const m = u.pathname.match(/\/(\d+)\/(\d+)\.png$/i);
if (!m) return null;
return { chunk1: parseInt(m[1], 10), chunk2: parseInt(m[2], 10) };
} catch { return null; }
}
function matchPixelUrl(urlStr) {
try {
const u = new URL(urlStr, location.href);
if (u.hostname !== 'backend.wplace.live') return null;
const m = u.pathname.match(/\/s0\/pixel\/(\d+)\/(\d+)$/);
if (!m) return null;
const sp = u.searchParams;
return {
normalized: `https://backend.wplace.live/s0/pixel/${m[1]}/${m[2]}?x=${sp.get('x')||0}&y=${sp.get('y')||0}`
};
} catch { return null; }
}
function rectIntersect(ax, ay, aw, ah, bx, by, bw, bh) {
const x = Math.max(ax, bx);
const y = Math.max(ay, by);
const r = Math.min(ax + aw, bx + bw);
const b = Math.min(ay + ah, by + bh);
const w = Math.max(0, r - x);
const h = Math.max(0, b - y);
return { x, y, w, h };
}
const overlayCache = new Map();
function overlaySignature(ov) {
const imgKey = ov.imageBase64 ? ov.imageBase64.slice(0, 64) + ':' + ov.imageBase64.length : 'none';
return [imgKey, ov.pixelUrl || 'null', ov.offsetX, ov.offsetY, ov.opacity].join('|');
}
function clearOverlayCache() {
overlayCache.clear();
}
async function buildOverlayDataForChunk(ov, targetChunk1, targetChunk2) {
if (!ov.enabled || !ov.imageBase64 || !ov.pixelUrl) return null;
const sig = overlaySignature(ov);
const cacheKey = `${ov.id}|${sig}|${targetChunk1}|${targetChunk2}`;
if (overlayCache.has(cacheKey)) return overlayCache.get(cacheKey);
const img = await loadImage(ov.imageBase64);
if (!img) return null;
const base = extractPixelCoords(ov.pixelUrl);
if (!Number.isFinite(base.chunk1) || !Number.isFinite(base.chunk2)) return null;
const drawX = (base.chunk1 * TILE_SIZE + base.posX + ov.offsetX) - (targetChunk1 * TILE_SIZE);
const drawY = (base.chunk2 * TILE_SIZE + base.posY + ov.offsetY) - (targetChunk2 * TILE_SIZE);
const isect = rectIntersect(0, 0, TILE_SIZE, TILE_SIZE, drawX, drawY, img.width, img.height);
if (isect.w === 0 || isect.h === 0) {
overlayCache.set(cacheKey, null);
return null;
}
const canvas = createCanvas(TILE_SIZE, TILE_SIZE);
const ctx = canvas.getContext('2d');
ctx.drawImage(img, drawX, drawY);
const imageData = ctx.getImageData(isect.x, isect.y, isect.w, isect.h);
const data = imageData.data;
const colorStrength = ov.opacity;
const whiteStrength = 1 - colorStrength;
for (let i = 0; i < data.length; i += 4) {
if (data[i + 3] > 0) {
data[i ] = Math.round(data[i ] * colorStrength + 255 * whiteStrength);
data[i + 1] = Math.round(data[i + 1] * colorStrength + 255 * whiteStrength);
data[i + 2] = Math.round(data[i + 2] * colorStrength + 255 * whiteStrength);
data[i + 3] = 255;
}
}
const result = { imageData, dx: isect.x, dy: isect.y };
overlayCache.set(cacheKey, result);
return result;
}
async function mergeOverlaysBehind(originalBlob, overlayDatas) {
if (!overlayDatas || overlayDatas.length === 0) return originalBlob;
const originalImage = await blobToImage(originalBlob);
const w = originalImage.width;
const h = originalImage.height;
const canvas = createCanvas(w, h);
const ctx = canvas.getContext('2d');
for (const ovd of overlayDatas) {
if (!ovd) continue;
ctx.putImageData(ovd.imageData, ovd.dx, ovd.dy);
}
ctx.drawImage(originalImage, 0, 0);
return await canvasToBlob(canvas);
}
function hookFetch() {
const originalFetch = page.fetch;
if (!originalFetch || originalFetch.__overlayHooked) return;
const hookedFetch = async (input, init) => {
const urlStr = typeof input === 'string' ? input : (input && input.url) || '';
if (config.autoCapturePixelUrl && config.activeOverlayId) {
const pixelMatch = matchPixelUrl(urlStr);
if (pixelMatch) {
const ov = config.overlays.find(o => o.id === config.activeOverlayId);
if (ov) {
if (ov.pixelUrl !== pixelMatch.normalized) {
ov.pixelUrl = pixelMatch.normalized;
await saveConfig(['overlays']);
clearOverlayCache();
updateUI();
}
}
}
}
const tileMatch = matchTileUrl(urlStr);
if (!tileMatch || config.overlayMode !== 'overlay') {
return originalFetch(input, init);
}
try {
const response = await originalFetch(input, init);
if (!response.ok) return response;
const enabledOverlays = config.overlays.filter(o => o.enabled && o.imageBase64 && o.pixelUrl);
if (enabledOverlays.length === 0) return response;
const originalBlob = await response.blob();
const overlayDatas = [];
for (const ov of enabledOverlays) {
overlayDatas.push(await buildOverlayDataForChunk(ov, tileMatch.chunk1, tileMatch.chunk2));
}
const mergedBlob = await mergeOverlaysBehind(originalBlob, overlayDatas.filter(Boolean));
const headers = new Headers(response.headers);
headers.set('Content-Type', 'image/png');
headers.delete('Content-Length');
return new Response(mergedBlob, {
status: response.status,
statusText: response.statusText,
headers
});
} catch (e) {
console.error("Overlay Pro: Error processing tile", e);
return originalFetch(input, init);
}
};
hookedFetch.__overlayHooked = true;
page.fetch = hookedFetch;
window.fetch = hookedFetch;
console.log('Overlay Pro: Fetch hook installed.');
}
function injectStyles() {
const style = document.createElement('style');
style.textContent = `
#overlay-pro-panel {
position: fixed;
top: 230px;
right: 15px;
z-index: 10001;
background: rgba(20,20,20,0.9);
backdrop-filter: blur(8px);
border: 1px solid #444;
border-radius: 8px;
color: white;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-size: 14px;
width: 320px;
}
.op-header { padding: 8px 12px; background: #333; cursor: pointer; display: flex; justify-content: space-between; align-items: center; border-top-left-radius: 8px; border-top-right-radius: 8px; }
.op-header h3 { margin: 0; font-size: 16px; user-select: none; }
.op-toggle-btn { font-size: 20px; background: none; border: none; color: white; cursor: pointer; padding: 0 5px; }
.op-content { padding: 12px; display: flex; flex-direction: column; gap: 12px; }
.op-section { display: flex; flex-direction: column; gap: 8px; }
.op-row { display: flex; align-items: center; gap: 8px; }
.op-button { background: #555; color: white; border: 1px solid #777; border-radius: 4px; padding: 6px 10px; cursor: pointer; }
.op-button:hover { background: #666; }
.op-button.danger { background: #7a2b2b; border-color: #a34242; }
.op-input, .op-select { background: #222; border: 1px solid #555; color: white; border-radius: 4px; padding: 5px; }
.op-input[type="number"] { width: 80px; }
.op-input[type="text"] { width: 100%; }
.op-slider { width: 100%; }
.op-list { display: flex; flex-direction: column; gap: 6px; max-height: 200px; overflow: auto; border: 1px solid #444; padding: 6px; border-radius: 6px; background: #1a1a1a; }
.op-item { display: flex; align-items: center; gap: 6px; padding: 4px; border-radius: 4px; }
.op-item.active { background: #2a2a2a; }
.op-item-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.op-grow { flex: 1; }
.op-muted { color: #bbb; font-size: 12px; }
.op-preview { width: 100%; height: 80px; background: #111; display: flex; align-items: center; justify-content: center; border: 1px dashed #444; border-radius: 4px; overflow: hidden; }
.op-preview img { max-width: 100%; max-height: 100%; display: block; }
`;
document.head.appendChild(style);
}
function createUI() {
if (document.getElementById('overlay-pro-panel')) return;
const panel = document.createElement('div');
panel.id = 'overlay-pro-panel';
panel.innerHTML = `
<div class="op-header" id="op-header">
<h3>Overlay Pro</h3>
<button class="op-toggle-btn" id="op-panel-toggle">▶</button>
</div>
<div class="op-content" id="op-content">
<div class="op-section">
<div class="op-row" style="justify-content: space-between;">
<div>
<button class="op-button" id="op-mode-toggle">Mode</button>
</div>
<div class="op-row">
<label class="op-muted">Place overlay:</label>
<button class="op-button" id="op-autocap-toggle">OFF</button>
</div>
</div>
</div>
<div class="op-section">
<div class="op-row" style="justify-content: space-between;">
<strong>Overlays</strong>
<div class="op-row">
<button class="op-button" id="op-add-overlay">+ Add</button>
<button class="op-button" id="op-import-overlay">Import</button>
</div>
</div>
<div class="op-list" id="op-overlay-list"></div>
<div class="op-row" style="justify-content: flex-end; gap: 6px;">
<button class="op-button" id="op-export-overlay">Export</button>
<button class="op-button danger" id="op-delete-overlay">Delete</button>
</div>
</div>
<div class="op-section" id="op-editor-section">
<strong>Editor</strong>
<div class="op-row"><span class="op-muted">Active overlay fields</span></div>
<div class="op-row">
<label style="width: 90px;">Name</label>
<input type="text" class="op-input op-grow" id="op-name">
</div>
<div class="op-row">
<label style="width: 90px;">PNG URL</label>
<input type="text" class="op-input op-grow" id="op-image-url" placeholder="https://files.catbox.moe/....png">
<button class="op-button" id="op-load-image">Load</button>
</div>
<div class="op-preview"><img id="op-image-preview" alt="No image"></div>
<div class="op-row">
<label style="width: 90px;">Pixel URL</label>
<input type="text" class="op-input op-grow" id="op-pixel-url" placeholder="Place overlay!">
</div>
<div class="op-row"><span class="op-muted" id="op-coord-display"></span></div>
<div class="op-row" style="gap: 5px;">
<div class="op-row">
<span>X</span>
<input type="number" class="op-input" style="width: 55px;" id="op-offset-x">
<button class="op-button" data-offset="x" data-amount="-1">-</button>
<button class="op-button" data-offset="x" data-amount="1">+</button>
</div>
<div class="op-row">
<span>Y</span>
<input type="number" class="op-input" style="width: 55px;" id="op-offset-y">
<button class="op-button" data-offset="y" data-amount="-1">-</button>
<button class="op-button" data-offset="y" data-amount="1">+</button>
</div>
</div>
<div class="op-row" style="width: 100%; gap: 12px; padding: 10px;">
<label style="width: 40px;">Opacity</label>
<input type="range" min="0" max="1" step="0.05" class="op-slider op-grow" id="op-opacity-slider">
<span id="op-opacity-value" style="width: 25px; text-align: right;">70%</span>
</div>
</div>
<div class="op-section">
<button class="op-button" id="op-reload-btn">Reload Tiles (Refresh Page)</button>
</div>
</div>
`;
document.body.appendChild(panel);
addEventListeners();
updateUI();
}
function getActiveOverlay() {
return config.overlays.find(o => o.id === config.activeOverlayId) || null;
}
function rebuildOverlayListUI() {
const list = document.getElementById('op-overlay-list');
if (!list) return;
list.innerHTML = '';
for (const ov of config.overlays) {
const item = document.createElement('div');
item.className = 'op-item' + (ov.id === config.activeOverlayId ? ' active' : '');
item.innerHTML = `
<input type="radio" name="op-active" ${ov.id === config.activeOverlayId ? 'checked' : ''} />
<input type="checkbox" ${ov.enabled ? 'checked' : ''} title="Toggle enabled" />
<div class="op-item-name" title="${ov.name || '(unnamed)'}">${ov.name || '(unnamed)'}</div>
`;
const [radio, checkbox, nameDiv] = item.children;
radio.addEventListener('change', () => {
config.activeOverlayId = ov.id;
saveConfig(['activeOverlayId']);
updateUI();
});
checkbox.addEventListener('change', () => {
ov.enabled = checkbox.checked;
saveConfig(['overlays']);
clearOverlayCache();
});
nameDiv.addEventListener('click', () => {
config.activeOverlayId = ov.id;
saveConfig(['activeOverlayId']);
updateUI();
});
list.appendChild(item);
}
}
async function addOverlayFromUrl(url, name = '') {
if (!/\.png(\?|$)/i.test(url)) {
if (!confirm("The URL does not look like a .png. Try anyway?")) return null;
}
const base64 = await urlToDataURL(url);
const ov = {
id: uid(),
name: name || 'Overlay',
enabled: true,
imageUrl: url,
imageBase64: base64,
pixelUrl: null,
offsetX: 0,
offsetY: 0,
opacity: 0.7
};
config.overlays.push(ov);
config.activeOverlayId = ov.id;
await saveConfig(['overlays', 'activeOverlayId']);
clearOverlayCache();
updateUI();
return ov;
}
async function importOverlayFromJSON(jsonText) {
let obj;
try {
obj = JSON.parse(jsonText);
} catch {
alert('Invalid JSON');
return;
}
const arr = Array.isArray(obj) ? obj : [obj];
let imported = 0, failed = 0;
for (const item of arr) {
const name = item.name || 'Imported Overlay';
const imageUrl = item.imageUrl;
const pixelUrl = item.pixelUrl ?? null;
const offsetX = Number.isFinite(item.offsetX) ? item.offsetX : 0;
const offsetY = Number.isFinite(item.offsetY) ? item.offsetY : 0;
const opacity = Number.isFinite(item.opacity) ? item.opacity : 0.7;
if (!imageUrl) { failed++; continue; }
try {
const base64 = await urlToDataURL(imageUrl);
const ov = {
id: uid(),
name,
enabled: true,
imageUrl,
imageBase64: base64,
pixelUrl,
offsetX, offsetY, opacity
};
config.overlays.push(ov);
imported++;
} catch (e) {
console.error('Import failed for', imageUrl, e);
failed++;
}
}
if (imported > 0) {
config.activeOverlayId = config.overlays[config.overlays.length - 1].id;
await saveConfig(['overlays', 'activeOverlayId']);
clearOverlayCache();
updateUI();
}
alert(`Import finished. Imported: ${imported}${failed ? `, Failed: ${failed}` : ''}`);
}
function exportActiveOverlayToClipboard() {
const ov = getActiveOverlay();
if (!ov) { alert('No active overlay selected.'); return; }
if (!ov.imageUrl) {
alert('This overlay has no image URL. Set a direct PNG URL to export a compact JSON.');
return;
}
const payload = {
version: 1,
name: ov.name,
imageUrl: ov.imageUrl,
pixelUrl: ov.pixelUrl ?? null,
offsetX: ov.offsetX,
offsetY: ov.offsetY,
opacity: ov.opacity
};
const text = JSON.stringify(payload, null, 2);
copyText(text).then(() => alert('Overlay JSON copied to clipboard!')).catch(() => {
prompt('Copy the JSON below:', text);
});
}
function copyText(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(text);
}
return Promise.reject(new Error('Clipboard API not available'));
}
function addEventListeners() {
const $ = (id) => document.getElementById(id);
$('op-header').addEventListener('click', () => {
config.isPanelCollapsed = !config.isPanelCollapsed;
saveConfig(['isPanelCollapsed']);
updateUI();
});
$('op-mode-toggle').addEventListener('click', () => {
config.overlayMode = config.overlayMode === 'overlay' ? 'original' : 'overlay';
saveConfig(['overlayMode']);
updateUI();
});
$('op-autocap-toggle').addEventListener('click', () => {
config.autoCapturePixelUrl = !config.autoCapturePixelUrl;
saveConfig(['autoCapturePixelUrl']);
updateUI();
});
$('op-add-overlay').addEventListener('click', async () => {
const url = prompt('Enter direct PNG URL (e.g., https://files.catbox.moe/....png):');
if (!url) return;
const name = prompt('Enter a name for this overlay:', 'Overlay');
try {
await addOverlayFromUrl(url.trim(), (name || '').trim());
} catch (e) {
console.error(e);
alert('Failed to add overlay from URL. See console for details.');
}
});
$('op-import-overlay').addEventListener('click', async () => {
const text = prompt('Paste overlay JSON (single or array):');
if (!text) return;
await importOverlayFromJSON(text);
});
$('op-export-overlay').addEventListener('click', () => exportActiveOverlayToClipboard());
$('op-delete-overlay').addEventListener('click', async () => {
const ov = getActiveOverlay();
if (!ov) { alert('No active overlay selected.'); return; }
if (!confirm(`Delete overlay "${ov.name}"?`)) return;
const idx = config.overlays.findIndex(o => o.id === ov.id);
if (idx >= 0) {
config.overlays.splice(idx, 1);
if (config.activeOverlayId === ov.id) {
config.activeOverlayId = config.overlays[0]?.id || null;
}
await saveConfig(['overlays', 'activeOverlayId']);
clearOverlayCache();
updateUI();
}
});
$('op-load-image').addEventListener('click', async () => {
const ov = getActiveOverlay();
if (!ov) { alert('No active overlay selected.'); return; }
const url = $('op-image-url').value.trim();
if (!url) { alert('Enter a PNG URL first.'); return; }
try {
const base64 = await urlToDataURL(url);
ov.imageUrl = url;
ov.imageBase64 = base64;
await saveConfig(['overlays']);
clearOverlayCache();
updateUI();
} catch (e) {
console.error(e);
alert('Failed to load image from URL. Check the link or try a direct PNG.');
}
});
$('op-name').addEventListener('change', async (e) => {
const ov = getActiveOverlay(); if (!ov) return;
ov.name = e.target.value;
await saveConfig(['overlays']);
rebuildOverlayListUI();
});
$('op-image-url').addEventListener('change', (e) => {
});
$('op-pixel-url').addEventListener('change', async (e) => {
const ov = getActiveOverlay(); if (!ov) return;
const v = e.target.value.trim() || null;
ov.pixelUrl = v;
await saveConfig(['overlays']);
clearOverlayCache();
updateUI();
});
document.querySelectorAll('[data-offset]').forEach(btn => btn.addEventListener('click', async () => {
const ov = getActiveOverlay(); if (!ov) return;
const offset = btn.dataset.offset;
const amount = parseInt(btn.dataset.amount, 10);
if (offset === 'x') ov.offsetX += amount;
if (offset === 'y') ov.offsetY += amount;
await saveConfig(['overlays']);
clearOverlayCache();
updateUI();
}));
$('op-offset-x').addEventListener('change', async (e) => {
const ov = getActiveOverlay(); if (!ov) return;
const v = parseInt(e.target.value, 10);
ov.offsetX = Number.isFinite(v) ? v : 0;
await saveConfig(['overlays']);
clearOverlayCache();
updateUI();
});
$('op-offset-y').addEventListener('change', async (e) => {
const ov = getActiveOverlay(); if (!ov) return;
const v = parseInt(e.target.value, 10);
ov.offsetY = Number.isFinite(v) ? v : 0;
await saveConfig(['overlays']);
clearOverlayCache();
updateUI();
});
$('op-opacity-slider').addEventListener('input', (e) => {
const ov = getActiveOverlay(); if (!ov) return;
ov.opacity = parseFloat(e.target.value);
document.getElementById('op-opacity-value').textContent = Math.round(ov.opacity * 100) + '%';
});
$('op-opacity-slider').addEventListener('change', async () => {
await saveConfig(['overlays']);
clearOverlayCache();
});
$('op-reload-btn').addEventListener('click', () => location.reload());
}
function updateEditorUI() {
const $ = (id) => document.getElementById(id);
const ov = getActiveOverlay();
const editor = $('op-editor-section');
editor.style.display = ov ? 'block' : 'none';
if (!ov) return;
$('op-name').value = ov.name || '';
$('op-image-url').value = ov.imageUrl || '';
$('op-pixel-url').value = ov.pixelUrl || '';
const preview = $('op-image-preview');
if (ov.imageBase64) {
preview.src = ov.imageBase64;
} else {
preview.removeAttribute('src');
}
const coords = ov.pixelUrl ? extractPixelCoords(ov.pixelUrl) : { chunk1: '-', chunk2: '-', posX: '-', posY: '-' };
$('op-coord-display').textContent = ov.pixelUrl
? `Ref: chunk ${coords.chunk1}/${coords.chunk2} at (${coords.posX}, ${coords.posY})`
: `No pixel URL set`;
$('op-offset-x').value = ov.offsetX;
$('op-offset-y').value = ov.offsetY;
$('op-opacity-slider').value = String(ov.opacity);
$('op-opacity-value').textContent = Math.round(ov.opacity * 100) + '%';
}
function updateUI() {
const $ = (id) => document.getElementById(id);
const panel = $('overlay-pro-panel');
if (!panel) return;
const content = $('op-content');
const toggle = $('op-panel-toggle');
const collapsed = !!config.isPanelCollapsed;
content.style.display = collapsed ? 'none' : 'flex';
toggle.textContent = collapsed ? '▶' : '◀';
const modeBtn = $('op-mode-toggle');
modeBtn.textContent = `Mode: ${config.overlayMode === 'overlay' ? 'Overlay' : 'Original'}`;
modeBtn.classList.toggle('active', config.overlayMode === 'overlay');
const autoBtn = $('op-autocap-toggle');
autoBtn.textContent = config.autoCapturePixelUrl ? 'ON' : 'OFF';
autoBtn.classList.toggle('active', !!config.autoCapturePixelUrl);
rebuildOverlayListUI();
updateEditorUI();
}
async function main() {
await loadConfig();
injectStyles();
if (document.readyState === 'loading') {
window.addEventListener('DOMContentLoaded', createUI);
} else {
createUI();
}
hookFetch();
console.log("Overlay Pro — Collections v2.0.0: Initialized.");
}
main();
})();