diff --git a/src/app.ts b/src/app.ts index bfa1a2b..0ec11dd 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,8 +1,61 @@ /// -import { loadConfig, applyTheme } from './core/store'; +import { config, loadConfig, applyTheme } from './core/store'; import { ensureHook, setUpdateUI } from './core/hook'; import { injectStyles } from './ui/styles'; import { createUI, updateUI } from './ui/panel'; +import { displayImageFromData } from './core/overlay'; +import { showToast } from './core/toast'; +import { urlToDataURL } from './core/gm'; + +async function applyTemplateFromUrl() { + const urlParams = new URLSearchParams(window.location.search); + const templateUrl = urlParams.get('template'); + if (!templateUrl) return; + + try { + console.log(`Fetching template from URL: ${templateUrl}`); + const res = await fetch(templateUrl); + if (!res.ok) { + const errorText = await res.text(); + throw new Error( + `Failed to fetch template: ${res.status} ${res.statusText} - ${errorText}`, + ); + } + const json = await res.json(); + + if (!json.record || !json.record.imageUrl) { + throw new Error('Invalid template format: missing `record.imageUrl`'); + } + const { name, imageUrl, pixelUrl, offsetX, offsetY, opacity } = json.record; + + if (config.overlays.some(o => o.name === name || o.imageUrl === imageUrl)) { + return; + } + + console.log(`Fetching image from: ${imageUrl}`); + const imageBase64 = await urlToDataURL(imageUrl); + + const newOverlay = { + id: crypto.randomUUID(), + name, + enabled: true, + imageUrl, + isLocal: false, + imageBase64, + pixelUrl, + offsetX, + offsetY, + opacity, + }; + + console.log('Adding new overlay from URL template:', newOverlay); + await displayImageFromData(newOverlay); + showToast(`Template "${name}" loaded from URL`, 'success'); + } catch (err) { + console.error('Error loading template from URL:', err); + showToast(`Error: ${err.message}`, 'error'); + } +} export async function bootstrapApp() { injectStyles(); @@ -11,5 +64,6 @@ export async function bootstrapApp() { createUI(); setUpdateUI(() => updateUI()); ensureHook(); + await applyTemplateFromUrl(); console.log('Overlay Pro UI ready.'); } \ No newline at end of file diff --git a/src/core/hook.ts b/src/core/hook.ts index fe9cd51..5703aec 100644 --- a/src/core/hook.ts +++ b/src/core/hook.ts @@ -12,6 +12,10 @@ export function setUpdateUI(cb: () => void) { updateUICallback = cb; } +export function getUpdateUI() { + return updateUICallback; +} + export function overlaysNeedingHook() { const hasImage = config.overlays.some(o => o.enabled && o.imageBase64); const placing = !!config.autoCapturePixelUrl && !!config.activeOverlayId; diff --git a/src/core/overlay.ts b/src/core/overlay.ts index bcf5c54..7fd740a 100644 --- a/src/core/overlay.ts +++ b/src/core/overlay.ts @@ -1,9 +1,10 @@ -import { createCanvas, createHTMLCanvas, canvasToBlob, blobToImage, loadImage } from './canvas'; +import { createCanvas, canvasToBlob, blobToImage, loadImage } from './canvas'; import { MINIFY_SCALE, MINIFY_SCALE_SYMBOL, TILE_SIZE, MAX_OVERLAY_DIM } from './constants'; -import { imageDecodeCache, overlayCache, tooLargeOverlays, paletteDetectionCache, baseMinifyCache } from './cache'; +import { imageDecodeCache, overlayCache, tooLargeOverlays, paletteDetectionCache, baseMinifyCache, clearOverlayCache } from './cache'; import { showToast } from './toast'; -import { config } from './store'; +import { config, saveConfig, type OverlayItem } from './store'; import { WPLACE_FREE, WPLACE_PAID, SYMBOL_TILES, SYMBOL_W, SYMBOL_H } from './palette'; +import { getUpdateUI, ensureHook } from './hook'; const ALL_COLORS = [...WPLACE_FREE, ...WPLACE_PAID]; const colorIndexMap = new Map(); @@ -370,9 +371,13 @@ export async function composeTileUnified( const scale = config.minifyStyle === 'symbols' ? MINIFY_SCALE_SYMBOL : MINIFY_SCALE; const w = originalImage.width, h = originalImage.height; - const baseCacheKey = `base:${originalBlob.size}:${w}x${h}:${scale}:${config.minifyStyle}`; + const arrayBuffer = await originalBlob.arrayBuffer(); + const view = new DataView(arrayBuffer); + const hash = view.getUint32(0, true) ^ view.getUint32(view.byteLength - 4, true); + + const baseCacheKey = `base:${originalBlob.size}:${hash}:${w}x${h}:${scale}:${config.minifyStyle}`; let scaledBaseImageData = baseMinifyCache.get(baseCacheKey); - + if (!scaledBaseImageData) { const baseCanvas = createCanvas(w * scale, h * scale) as any; const baseCtx = baseCanvas.getContext('2d', { willReadFrequently: true })!; @@ -424,4 +429,20 @@ export async function composeTileUnified( } return await canvasToBlob(canvas); } +} + +export async function displayImageFromData(newOverlay: OverlayItem) { + if (!config.overlays) { + config.overlays = []; + } + config.overlays.push(newOverlay); + await saveConfig(); + + clearOverlayCache(); + ensureHook(); + + const updateUI = getUpdateUI(); + if (updateUI) { + updateUI(); + } } \ No newline at end of file diff --git a/src/core/toast.ts b/src/core/toast.ts index f2c61ed..edea88b 100644 --- a/src/core/toast.ts +++ b/src/core/toast.ts @@ -1,6 +1,10 @@ import { config } from './store'; -export function showToast(message: string, duration = 3000) { +export function showToast( + message: string, + type: 'info' | 'error' | 'success' = 'info', + duration = 3000, +) { let stack = document.getElementById('op-toast-stack'); if (!stack) { stack = document.createElement('div'); @@ -11,7 +15,7 @@ export function showToast(message: string, duration = 3000) { stack.classList.toggle('op-dark', config.theme === 'dark'); const t = document.createElement('div'); - t.className = 'op-toast'; + t.className = `op-toast op-toast-${type}`; t.textContent = message; stack.appendChild(t); requestAnimationFrame(() => t.classList.add('show')); diff --git a/src/meta.js b/src/meta.js index 7152efe..3a6fda0 100644 --- a/src/meta.js +++ b/src/meta.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Wplace Overlay Pro // @namespace http://tampermonkey.net/ -// @version 3.1.3 +// @version 3.1.5 // @description Overlays tiles on wplace.live. Can also resize, and color-match your overlay to wplace's palette. Make sure to comply with the site's Terms of Service, and rules! This script is not affiliated with Wplace.live in any way, use at your own risk. This script is not affiliated with TamperMonkey. The author of this userscript is not responsible for any damages, issues, loss of data, or punishment that may occur as a result of using this script. This script is provided "as is" under GPLv3. // @author shinkonet // @match https://wplace.live/* diff --git a/src/ui/panel.ts b/src/ui/panel.ts index 616cb2e..ef68a7c 100644 --- a/src/ui/panel.ts +++ b/src/ui/panel.ts @@ -55,7 +55,7 @@ export function createUI() {
-
+
diff --git a/src/ui/styles.ts b/src/ui/styles.ts index 0a35d10..df14e78 100644 --- a/src/ui/styles.ts +++ b/src/ui/styles.ts @@ -94,6 +94,10 @@ export function injectStyles() { .op-toast { background: rgba(255,255,255,0.98); border: 1px solid #e6ebf2; color: #111827; padding: 8px 12px; border-radius: 10px; font-size: 12px; box-shadow: 0 6px 16px rgba(16,24,40,0.12); opacity: 0; transform: translateY(-6px); transition: opacity .18s ease, transform .18s ease; max-width: 100%; text-align: center; } .op-toast.show { opacity: 1; transform: translateY(0); } .op-toast-stack.op-dark .op-toast { background: rgba(27,30,36,0.98); border-color: #2a2f3a; color: #f5f6f9; } + .op-toast.op-toast-error { background: #fee2e2; border-color: #fecaca; color: #7f1d1d; } + .op-toast-stack.op-dark .op-toast.op-toast-error { background: #4a1f1f; border-color: #5b2d2d; color: #fecaca; } + .op-toast.op-toast-success { background: #dcfce7; border-color: #bbf7d0; color: #14532d; } + .op-toast-stack.op-dark .op-toast.op-toast-success { background: #163822; border-color: #225a35; color: #bbf7d0; } .op-cc-backdrop { position: fixed; inset: 0; z-index: 10000; background: rgba(0,0,0,0.45); display: none; } .op-cc-backdrop.show { display: block; }