inject symbols into palette

This commit is contained in:
Decrypt 2025-08-22 16:36:11 +02:00
parent 5b12b51cf4
commit 00c8bd5aa2
3 changed files with 217 additions and 2 deletions

View file

@ -6,6 +6,7 @@ import { createUI, updateUI } from './ui/panel';
import { displayImageFromData } from './core/overlay';
import { showToast } from './core/toast';
import { urlToDataURL } from './core/gm';
import { enablePaletteSymbols } from './core/palette-inject';
async function applyTemplateFromUrl() {
const urlParams = new URLSearchParams(window.location.search);
@ -64,6 +65,7 @@ export async function bootstrapApp() {
createUI();
setUpdateUI(() => updateUI());
ensureHook();
enablePaletteSymbols();
await applyTemplateFromUrl();
console.log('Overlay Pro UI ready.');
}

195
src/core/palette-inject.ts Normal file
View file

@ -0,0 +1,195 @@
import { WPLACE_FREE, WPLACE_PAID, SYMBOL_TILES, SYMBOL_W, SYMBOL_H } from './palette';
import { config } from './store';
const ALL_COLORS = [...WPLACE_FREE, ...WPLACE_PAID];
let paletteObserver: MutationObserver | null = null;
let isInjected = false;
function createSymbolCanvas(colorIndex: number, _bgColor: string): HTMLCanvasElement {
const canvas = document.createElement('canvas');
canvas.width = 20;
canvas.height = 20;
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('Failed to get 2D context');
// Clear with transparency
ctx.clearRect(0, 0, 20, 20);
if (colorIndex < SYMBOL_TILES.length) {
const symbol = SYMBOL_TILES[colorIndex];
const scale = 3;
const offsetX = Math.floor((20 - SYMBOL_W * scale) / 2);
const offsetY = Math.floor((20 - SYMBOL_H * scale) / 2);
// Draw white outline first (slightly larger)
ctx.fillStyle = '#ffffff';
for (let y = 0; y < SYMBOL_H; y++) {
for (let x = 0; x < SYMBOL_W; x++) {
const bitIndex = y * SYMBOL_W + x;
const bit = (symbol >>> bitIndex) & 1;
if (bit) {
ctx.fillRect(
offsetX + x * scale - 1,
offsetY + y * scale - 1,
scale + 2,
scale + 2
);
}
}
}
// Draw black symbol on top
ctx.fillStyle = '#000000';
for (let y = 0; y < SYMBOL_H; y++) {
for (let x = 0; x < SYMBOL_W; x++) {
const bitIndex = y * SYMBOL_W + x;
const bit = (symbol >>> bitIndex) & 1;
if (bit) {
ctx.fillRect(
offsetX + x * scale,
offsetY + y * scale,
scale,
scale
);
}
}
}
}
return canvas;
}
function getColorFromButton(button: HTMLElement): string | null {
const style = button.getAttribute('style');
if (!style) return null;
const match = style.match(/background:\s*rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
if (!match) return null;
return `${match[1]},${match[2]},${match[3]}`;
}
function findColorIndex(colorKey: string): number {
return ALL_COLORS.findIndex(([r, g, b]) => `${r},${g},${b}` === colorKey);
}
function injectSymbolsIntoPalette() {
if (isInjected) return;
// Find all color buttons in the palette
const colorButtons = document.querySelectorAll('button[id^="color-"]');
if (colorButtons.length === 0) return;
colorButtons.forEach((button) => {
const htmlButton = button as HTMLElement;
const colorKey = getColorFromButton(htmlButton);
if (!colorKey) return;
const colorIndex = findColorIndex(colorKey);
if (colorIndex === -1) return;
// Check if symbol already exists
if (htmlButton.querySelector('.symbol-overlay')) return;
// Create symbol overlay
const symbolCanvas = createSymbolCanvas(colorIndex, colorKey);
symbolCanvas.className = 'symbol-overlay';
symbolCanvas.style.cssText = `
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 10;
opacity: 0.9;
`;
// Make button position relative if it isn't already
const computedStyle = window.getComputedStyle(htmlButton);
if (computedStyle.position === 'static') {
htmlButton.style.position = 'relative';
}
htmlButton.appendChild(symbolCanvas);
});
isInjected = true;
}
function cleanupSymbols() {
const symbols = document.querySelectorAll('.symbol-overlay');
symbols.forEach(symbol => symbol.remove());
isInjected = false;
}
function startPaletteWatcher() {
if (paletteObserver) return;
paletteObserver = new MutationObserver((mutations) => {
let shouldCheck = false;
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
// Check if palette was added or removed
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as Element;
if (element.querySelector('button[id^="color-"]') ||
element.matches('button[id^="color-"]')) {
shouldCheck = true;
}
}
});
mutation.removedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as Element;
if (element.querySelector('.symbol-overlay') ||
element.matches('.symbol-overlay')) {
isInjected = false;
}
}
});
}
});
if (shouldCheck && config.overlayMode === 'minify' && config.minifyStyle === 'symbols') {
// Small delay to ensure DOM is fully updated
setTimeout(injectSymbolsIntoPalette, 100);
}
});
paletteObserver.observe(document.body, {
childList: true,
subtree: true
});
}
export function enablePaletteSymbols() {
startPaletteWatcher();
// Try to inject immediately if palette is already open
if (config.overlayMode === 'minify' && config.minifyStyle === 'symbols') {
setTimeout(injectSymbolsIntoPalette, 500);
}
}
export function disablePaletteSymbols() {
if (paletteObserver) {
paletteObserver.disconnect();
paletteObserver = null;
}
cleanupSymbols();
}
export function updatePaletteSymbols() {
cleanupSymbols();
if (config.overlayMode === 'minify' && config.minifyStyle === 'symbols') {
setTimeout(injectSymbolsIntoPalette, 100);
}
}

View file

@ -9,6 +9,7 @@ import { extractPixelCoords } from '../core/overlay';
import { buildCCModal, openCCModal } from './ccModal';
import { buildRSModal, openRSModal } from './rsModal';
import { EV_ANCHOR_SET, EV_AUTOCAP_CHANGED } from '../core/events';
import { updatePaletteSymbols } from '../core/palette-inject';
let panelEl: HTMLDivElement | null = null;
@ -314,14 +315,31 @@ function addEventListeners(panel: HTMLDivElement) {
saveConfig(['overlayMode']);
ensureHook();
updateUI();
updatePaletteSymbols();
});
});
const styleDotsEl = $('op-style-dots') as HTMLInputElement | null;
styleDotsEl?.addEventListener('change', () => { if (styleDotsEl.checked) { config.minifyStyle = 'dots'; saveConfig(['minifyStyle']); clearOverlayCache(); ensureHook(); } });
styleDotsEl?.addEventListener('change', () => {
if (styleDotsEl.checked) {
config.minifyStyle = 'dots';
saveConfig(['minifyStyle']);
clearOverlayCache();
ensureHook();
updatePaletteSymbols();
}
});
const styleSymbolsEl = $('op-style-symbols') as HTMLInputElement | null;
styleSymbolsEl?.addEventListener('change', () => { if (styleSymbolsEl.checked) { config.minifyStyle = 'symbols'; saveConfig(['minifyStyle']); clearOverlayCache(); ensureHook(); } });
styleSymbolsEl?.addEventListener('change', () => {
if (styleSymbolsEl.checked) {
config.minifyStyle = 'symbols';
saveConfig(['minifyStyle']);
clearOverlayCache();
ensureHook();
updatePaletteSymbols();
}
});
$('op-autocap-toggle')?.addEventListener('click', () => { config.autoCapturePixelUrl = !config.autoCapturePixelUrl; saveConfig(['autoCapturePixelUrl']); ensureHook(); updateUI(); });