GeoPixels - Ghost Template Manager

Several Features Added to Ghost Template including template history, upload from URL, save all templates as zip file, and image preview toggle

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         GeoPixels - Ghost Template Manager
// @namespace    http://tampermonkey.net/
// @version      3.7
// @description  Several Features Added to Ghost Template including template history, upload from URL, save all templates as zip file, and image preview toggle
// @author       ariapokoteng
// @match        *://geopixels.net/*
// @match        *://*.geopixels.net/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=geopixels.net
// ==/UserScript==

(function() {
    'use strict';

    // ========== CONFIGURATION ==========
    const DEBUG_MODE = false;
    const DB_NAME = 'GP_Ghost_History';
    const DB_VERSION = 3;
    const STORE_NAME = 'images';

    // Marker Colors for Encoding
    const MARKER_R = 71;
    const MARKER_G = 80;
    const MARKER_B = 88;
    const POSITION_OFFSET = 2147483648;

    let isInternalUpdate = false;
    let previewActive = false;
    let previewOverlay = null;

    // ========== UTILITIES ==========
    function gpLog(msg, data = null) {
        if (!DEBUG_MODE) return;
        console.log(`%c[GP Manager] ${msg}`, "color: #00ffff; background: #000; padding: 2px 4px;", data || '');
    }

    // Debug: Log environment info on load
    gpLog("Script loaded. Environment check:", {
        hasWindow: typeof window !== 'undefined',
        hasUnsafeWindow: typeof unsafeWindow !== 'undefined',
        windowMap: typeof window !== 'undefined' ? typeof window.map : 'N/A',
        windowTurf: typeof window !== 'undefined' ? typeof window.turf : 'N/A',
        unsafeWindowMap: typeof unsafeWindow !== 'undefined' ? typeof unsafeWindow.map : 'N/A',
        unsafeWindowTurf: typeof unsafeWindow !== 'undefined' ? typeof unsafeWindow.turf : 'N/A'
    });

    /**
     * Safely get a page variable, avoiding DOM element conflicts.
     * In some browsers, accessing unsafeWindow.map returns the <div id="map"> element
     * instead of the JavaScript map variable.
     */
    function getPageVariable(varName) {
        // Try window first (works in Chrome/Vivaldi)
        if (typeof window !== 'undefined' && window[varName] !== undefined) {
            const val = window[varName];
            // Make sure it's not a DOM element when we expect an object with methods
            if (varName === 'map' && val instanceof HTMLElement) {
                gpLog(`window.${varName} is a DOM element, trying unsafeWindow`);
            } else {
                gpLog(`Found ${varName} in window`);
                return val;
            }
        }

        // Try unsafeWindow (needed in Firefox/Brave with @grant permissions)
        if (typeof unsafeWindow !== 'undefined' && unsafeWindow[varName] !== undefined) {
            const val = unsafeWindow[varName];
            // Check if it's a DOM element when we expect the map object
            if (varName === 'map' && val instanceof HTMLElement) {
                gpLog(`unsafeWindow.${varName} is a DOM element, looking for alternatives`);
                
                // Try to get the map from common Mapbox/Leaflet global patterns
                // The map might be stored in a different variable or we need wrappedJSObject (Firefox)
                if (typeof unsafeWindow.wrappedJSObject !== 'undefined' && unsafeWindow.wrappedJSObject[varName]) {
                    const wrappedVal = unsafeWindow.wrappedJSObject[varName];
                    if (!(wrappedVal instanceof HTMLElement)) {
                        gpLog(`Found ${varName} in wrappedJSObject`);
                        return wrappedVal;
                    }
                }
                
                // For Brave/Chrome with sandboxing, try accessing via page script injection
                gpLog(`Attempting page context injection for ${varName}`);
                return getPageVariableViaInjection(varName);
            } else {
                gpLog(`Found ${varName} in unsafeWindow`);
                return val;
            }
        }

        // Try wrappedJSObject directly (Firefox)
        if (typeof unsafeWindow !== 'undefined' && 
            typeof unsafeWindow.wrappedJSObject !== 'undefined' && 
            unsafeWindow.wrappedJSObject[varName] !== undefined) {
            gpLog(`Found ${varName} in wrappedJSObject`);
            return unsafeWindow.wrappedJSObject[varName];
        }

        gpLog(`Could not find ${varName} in any scope`);
        return null;
    }

    /**
     * Get a page variable by creating a bridge in the page context.
     * This is needed in Brave when @grant permissions create a sandbox.
     */
    function getPageVariableViaInjection(varName) {
        try {
            // Create a unique ID for this retrieval
            const bridgeId = `__gp_bridge_${varName}_${Date.now()}`;
            
            // Inject a script that copies the variable to a data attribute
            const script = document.createElement('script');
            script.textContent = `
                (function() {
                    if (typeof ${varName} !== 'undefined' && ${varName}) {
                        // Store a marker that the variable exists
                        document.documentElement.setAttribute('${bridgeId}', 'exists');
                        // For map object, we can't directly transfer it, so we'll access it differently
                        if ('${varName}' === 'map' && typeof ${varName}.project === 'function') {
                            document.documentElement.setAttribute('${bridgeId}_hasProject', 'true');
                        }
                    }
                })();
            `;
            document.documentElement.appendChild(script);
            script.remove();
            
            // Check if the variable exists
            const exists = document.documentElement.getAttribute(bridgeId);
            document.documentElement.removeAttribute(bridgeId);
            document.documentElement.removeAttribute(`${bridgeId}_hasProject`);
            
            if (exists === 'exists') {
                gpLog(`${varName} exists in page context, creating proxy`);
                
                // For the map object specifically, we need to create a proxy that executes in page context
                if (varName === 'map') {
                    return createMapProxy();
                } else if (varName === 'turf') {
                    return createTurfProxy();
                }
            }
            
            gpLog(`${varName} not found via injection`);
            return null;
        } catch (e) {
            gpLog(`Error in page context injection for ${varName}:`, e.message);
            return null;
        }
    }

    /**
     * Create a proxy object for the map that executes methods in page context
     */
    function createMapProxy() {
        return {
            project: function(lngLat) {
                // Execute in page context and return result
                const script = document.createElement('script');
                const resultId = `__gp_map_result_${Date.now()}`;
                script.textContent = `
                    (function() {
                        try {
                            const result = map.project([${lngLat[0]}, ${lngLat[1]}]);
                            document.documentElement.setAttribute('${resultId}', JSON.stringify({x: result.x, y: result.y}));
                        } catch(e) {
                            document.documentElement.setAttribute('${resultId}_error', e.message);
                        }
                    })();
                `;
                document.documentElement.appendChild(script);
                script.remove();
                
                const resultStr = document.documentElement.getAttribute(resultId);
                const errorStr = document.documentElement.getAttribute(`${resultId}_error`);
                document.documentElement.removeAttribute(resultId);
                document.documentElement.removeAttribute(`${resultId}_error`);
                
                if (errorStr) {
                    throw new Error(errorStr);
                }
                
                return JSON.parse(resultStr);
            },
            on: function(event, handler) {
                gpLog(`Map event listener for ${event} registered (proxy mode)`);
                // Store the handler for later use
                if (!this._handlers) this._handlers = {};
                if (!this._handlers[event]) this._handlers[event] = [];
                this._handlers[event].push(handler);
                
                // Set up event forwarding via page script
                const listenerId = `__gp_map_listener_${event}_${Date.now()}`;
                const script = document.createElement('script');
                script.textContent = `
                    (function() {
                        if (typeof map !== 'undefined' && map.on) {
                            map.on('${event}', function() {
                                document.documentElement.setAttribute('${listenerId}', Date.now());
                            });
                        }
                    })();
                `;
                document.documentElement.appendChild(script);
                script.remove();
                
                // Set up mutation observer to detect attribute changes
                const observer = new MutationObserver(() => {
                    const val = document.documentElement.getAttribute(listenerId);
                    if (val) {
                        document.documentElement.removeAttribute(listenerId);
                        handler();
                    }
                });
                observer.observe(document.documentElement, { attributes: true });
            },
            off: function(event, handler) {
                gpLog(`Map event listener for ${event} removed (proxy mode)`);
                // In proxy mode, we can't easily remove specific handlers
                // This is a limitation of the bridge approach
            },
            getContainer: function() {
                return document.getElementById('map');
            }
        };
    }

    /**
     * Create a proxy object for turf that executes methods in page context
     */
    function createTurfProxy() {
        return {
            toWgs84: function(mercCoords) {
                const script = document.createElement('script');
                const resultId = `__gp_turf_result_${Date.now()}`;
                script.textContent = `
                    (function() {
                        try {
                            const result = turf.toWgs84([${mercCoords[0]}, ${mercCoords[1]}]);
                            document.documentElement.setAttribute('${resultId}', JSON.stringify(result));
                        } catch(e) {
                            document.documentElement.setAttribute('${resultId}_error', e.message);
                        }
                    })();
                `;
                document.documentElement.appendChild(script);
                script.remove();
                
                const resultStr = document.documentElement.getAttribute(resultId);
                const errorStr = document.documentElement.getAttribute(`${resultId}_error`);
                document.documentElement.removeAttribute(resultId);
                document.documentElement.removeAttribute(`${resultId}_error`);
                
                if (errorStr) {
                    throw new Error(errorStr);
                }
                
                return JSON.parse(resultStr);
            }
        };
    }

    function notifyUser(title, message) {
        // Use safe helper to get showAlert function
        const showAlert = getPageVariable('showAlert');
        
        if (typeof showAlert === 'function') {
            showAlert(title, message);
        } else {
            console.log(`[${title}] ${message}`);
            // Fallback alert if site's showAlert is not available
            alert(`${title}: ${message}`);
        }
    }

    function goToTemplateLocation() {
        const savedCoordsStr = localStorage.getItem('ghostImageCoords');
        if (!savedCoordsStr) {
            notifyUser("No Template", "No ghost image template is currently set.");
            return;
        }
        
        try {
            const coords = JSON.parse(savedCoordsStr);
            if (typeof coords.gridX !== 'number' || typeof coords.gridY !== 'number') {
                notifyUser("Error", "Invalid coordinates in template.");
                return;
            }
            
            // Get goToGridLocation using safe helper
            const goToGridLocation = getPageVariable('goToGridLocation');
            
            if (typeof goToGridLocation === 'function') {
                gpLog(`Teleporting to template location: ${coords.gridX}, ${coords.gridY}`);
                goToGridLocation(coords.gridX, coords.gridY);
            } else {
                notifyUser("Error", "Navigation function not available.");
                gpLog("ERROR: goToGridLocation function not found in window or unsafeWindow");
            }
        } catch (e) {
            console.error("Failed to parse coordinates:", e);
            notifyUser("Error", "Failed to read template coordinates.");
        }
    }

    // Computes a SHA-256 fingerprint of the file content
    async function computeFileHash(blob) {
        const buffer = await blob.arrayBuffer();
        const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
        const hashArray = Array.from(new Uint8Array(hashBuffer));
        return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    }

    // Computes a templateId from the clean image content (without position encoding)
    // This allows us to identify the same template even if it's been moved to different positions
    async function computeTemplateId(blob) {
        try {
            const img = await loadImageToCanvas(blob);
            const decoded = decodeRobustPosition(img);
            
            if (decoded && decoded.cleanCanvas) {
                // If position was encoded, use the clean canvas for ID
                const cleanBlob = await new Promise(r => decoded.cleanCanvas.toBlob(r, 'image/png'));
                return await computeFileHash(cleanBlob);
            } else {
                // No position encoding found, use original hash
                return await computeFileHash(blob);
            }
        } catch (e) {
            // On error, fall back to regular hash
            return await computeFileHash(blob);
        }
    }

    // ========== STYLES ==========
    const style = document.createElement('style');
    style.textContent = `
        .gp-to-modal-overlay {
            position: fixed; inset: 0; background: rgba(0, 0, 0, 0.75);
            display: flex; align-items: center; justify-content: center; z-index: 10000;
        }
        .gp-to-modal-panel {
            background: white; border-radius: 1rem; padding: 1.5rem;
            width: 95%; max-width: 600px; max-height: 80vh;
            display: flex; flex-direction: column; gap: 1rem;
            box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
        }
        .gp-to-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #eee; padding-bottom: 10px; }
        .gp-to-title { font-size: 1.25rem; font-weight: bold; color: #1f2937; }

        .gp-to-grid {
            display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
            gap: 10px; overflow-y: auto; padding: 4px;
        }
        .gp-to-card {
            border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden;
            position: relative; transition: transform 0.1s, box-shadow 0.1s;
            cursor: pointer; background: #f9fafb;
        }
        .gp-to-card:hover { transform: translateY(-2px); box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1); border-color: #3b82f6; }
        .gp-to-card img { width: 100%; height: 100px; object-fit: cover; display: block; }
        .gp-to-card-footer {
            padding: 4px; font-size: 10px; text-align: center;
            background: #fff; color: #6b7280; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
        }
        .gp-to-delete-btn {
            position: absolute; top: 2px; right: 2px;
            background: rgba(239, 68, 68, 0.9); color: white;
            border: none; border-radius: 4px; width: 20px; height: 20px;
            display: flex; align-items: center; justify-content: center;
            font-size: 12px; cursor: pointer; z-index: 2;
        }
        .gp-to-delete-btn:hover { background: #dc2626; }

        .gp-to-btn {
            padding: 0.5rem 1rem; border-radius: 0.5rem; font-weight: 600; cursor: pointer; border: none;
            display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem;
            transition: all 0.2s;
        }
        .gp-to-btn-blue { background-color: #3b82f6; color: white; }
        .gp-to-btn-blue:hover { background-color: #2563eb; }
        .gp-to-btn-green { background-color: #10b981; color: white; }
        .gp-to-btn-green:hover { background-color: #059669; }
        .gp-to-btn-purple { background-color: #8b5cf6; color: white; }
        .gp-to-btn-purple:hover { background-color: #7c3aed; }
        .gp-to-btn-red { background-color: #ef4444; color: white; }
        .gp-to-btn-gray { background-color: #e5e7eb; color: #374151; }
        .gp-to-btn-orange { background-color: #f97316; color: white; }
        .gp-to-btn-orange:hover { background-color: #ea580c; }
        .gp-to-btn-cyan { background-color: #06b6d4; color: white; border: 2px solid transparent; }
        .gp-to-btn-cyan:hover { background-color: #0891b2; }
        .gp-to-btn-cyan.active { 
            background-color: #0e7490; 
            border: 2px solid #fbbf24;
            box-shadow: 0 0 0 3px rgba(251, 191, 36, 0.3);
        }

        .gp-to-preview-overlay {
            position: fixed;
            pointer-events: none;
            z-index: 9999;
            opacity: 0.7;
            transition: opacity 0.2s;
        }
    `;
    document.head.appendChild(style);

    // ========== INDEXED DB (CACHE) ==========

    const dbPromise = new Promise((resolve, reject) => {
        const request = indexedDB.open(DB_NAME, DB_VERSION);

        request.onupgradeneeded = (e) => {
            const db = e.target.result;
            const txn = e.target.transaction;

            let store;
            if (!db.objectStoreNames.contains(STORE_NAME)) {
                store = db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true });
            } else {
                store = txn.objectStore(STORE_NAME);
            }

            if (!store.indexNames.contains('hash')) {
                store.createIndex('hash', 'hash', { unique: false });
            }
            if (!store.indexNames.contains('templateId')) {
                store.createIndex('templateId', 'templateId', { unique: false });
            }
        };

        request.onsuccess = (e) => resolve(e.target.result);
        request.onerror = (e) => reject('DB Error');
    });

    const HistoryManager = {
        async add(blob, filename) {
            const db = await dbPromise;
            const hash = await computeFileHash(blob);
            const templateId = await computeTemplateId(blob);

            return new Promise((resolve, reject) => {
                const tx = db.transaction(STORE_NAME, 'readwrite');
                const store = tx.objectStore(STORE_NAME);
                const templateIndex = store.index('templateId');

                const req = templateIndex.get(templateId);

                req.onsuccess = () => {
                    const existing = req.result;

                    if (existing) {
                        gpLog("Duplicate template detected (same image, possibly different position). Updating entry.");
                        store.delete(existing.id);
                    }

                    const item = {
                        blob: blob,
                        name: filename || `Image_${Date.now()}`,
                        date: Date.now(),
                        hash: hash,
                        templateId: templateId
                    };
                    store.add(item);
                };

                tx.oncomplete = () => resolve();
                tx.onerror = () => reject(tx.error);
            });
        },
        async getAll() {
            const db = await dbPromise;
            return new Promise((resolve) => {
                const tx = db.transaction(STORE_NAME, 'readonly');
                const store = tx.objectStore(STORE_NAME);
                const req = store.getAll();
                req.onsuccess = () => resolve(req.result.reverse());
            });
        },
        async delete(id) {
            const db = await dbPromise;
            return new Promise((resolve) => {
                const tx = db.transaction(STORE_NAME, 'readwrite');
                tx.objectStore(STORE_NAME).delete(id);
                tx.oncomplete = () => resolve();
            });
        },
        async clear() {
            const db = await dbPromise;
            return new Promise((resolve) => {
                const tx = db.transaction(STORE_NAME, 'readwrite');
                tx.objectStore(STORE_NAME).clear();
                tx.oncomplete = () => resolve();
            });
        }
    };

    // ========== IMPORT/EXPORT FUNCTIONS ==========

    // Helper function to convert blob to base64
    function blobToBase64(blob) {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onloadend = () => resolve(reader.result.split(',')[1]);
            reader.onerror = reject;
            reader.readAsDataURL(blob);
        });
    }

    async function exportToZip() {
        gpLog("exportToZip: Starting export...");
        const images = await HistoryManager.getAll();
        gpLog(`exportToZip: Retrieved ${images.length} images`);
        
        if (images.length === 0) {
            notifyUser("Info", "No images to export.");
            return;
        }

        // JSZip doesn't work in Tampermonkey sandbox - use JSON bundle instead
        gpLog("exportToZip: Using JSON bundle export (JSZip incompatible with this environment)");
        
        try {
            const exportData = {
                version: "3.4",
                exportDate: new Date().toISOString(),
                images: []
            };

            for (let i = 0; i < images.length; i++) {
                const imgData = images[i];
                gpLog(`Encoding image ${i+1}/${images.length}: ${imgData.name}`);
                
                const base64 = await blobToBase64(imgData.blob);
                
                exportData.images.push({
                    id: imgData.id,
                    name: imgData.name,
                    date: imgData.date,
                    hash: imgData.hash,
                    templateId: imgData.templateId,
                    imageData: base64,
                    mimeType: imgData.blob.type || 'image/png'
                });
            }

            gpLog(`exportToZip: Creating download...`);
            
            const jsonStr = JSON.stringify(exportData);
            const blob = new Blob([jsonStr], { type: 'application/json' });
            
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `GeoPixels_History_${Date.now()}.json`;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);

            gpLog("exportToZip: Export complete");
            notifyUser("Success", `Exported ${images.length} images to JSON bundle.`);
        } catch (error) {
            console.error("exportToZip failed:", error);
            gpLog(`exportToZip: ERROR - ${error.message}`);
            notifyUser("Error", "Failed to export: " + error.message);
        }
    }

    async function importFromZip(file) {
        try {
            gpLog(`importFromZip: Starting import of ${file.name}`);
            
            // Check if it's a JSON file (new format)
            if (file.name.endsWith('.json')) {
                gpLog("importFromZip: Detected JSON bundle format");
                const text = await file.text();
                const data = JSON.parse(text);
                
                if (!data.images || !Array.isArray(data.images)) {
                    notifyUser("Error", "Invalid JSON: 'images' array not found.");
                    return;
                }
                
                let imported = 0;
                for (const imgEntry of data.images) {
                    // Convert base64 back to blob
                    const byteCharacters = atob(imgEntry.imageData);
                    const byteNumbers = new Array(byteCharacters.length);
                    for (let i = 0; i < byteCharacters.length; i++) {
                        byteNumbers[i] = byteCharacters.charCodeAt(i);
                    }
                    const byteArray = new Uint8Array(byteNumbers);
                    const blob = new Blob([byteArray], { type: imgEntry.mimeType || 'image/png' });
                    
                    // Check for duplicate
                    const existingImages = await HistoryManager.getAll();
                    const isDuplicate = existingImages.some(img => img.hash === imgEntry.hash);
                    
                    if (!isDuplicate) {
                        await HistoryManager.add(blob, imgEntry.name, imgEntry.hash);
                        imported++;
                        gpLog(`Imported: ${imgEntry.name}`);
                    } else {
                        gpLog(`Skipped duplicate: ${imgEntry.name}`);
                    }
                }
                
                notifyUser("Success", `Imported ${imported} images from JSON bundle.`);
                return;
            }
            
            // Try ZIP format (legacy) - may not work
            gpLog("importFromZip: Attempting ZIP format (may fail)");
            const zip = await JSZip.loadAsync(file);
            const metadataFile = zip.file('metadata.json');

            if (!metadataFile) {
                notifyUser("Error", "Invalid ZIP: metadata.json not found.");
                return;
            }

            const metadataText = await metadataFile.async('text');
            const metadata = JSON.parse(metadataText);

            let imported = 0;
            for (const item of metadata) {
                const imageFile = zip.file(item.filename);
                if (imageFile) {
                    const blob = await imageFile.async('blob');
                    await HistoryManager.add(blob, item.name);
                    imported++;
                }
            }

            notifyUser("Success", `Imported ${imported} images from ZIP.`);
            return true;
        } catch (e) {
            console.error(e);
            notifyUser("Error", "Failed to import file.");
            return false;
        }
    }

    // ========== ALGORITHM (ENCODE/DECODE) ==========

    function encodeRobustPosition(originalCanvas, gridX, gridY) {
        const width = originalCanvas.width;
        const height = originalCanvas.height;
        const newCanvas = document.createElement('canvas');
        newCanvas.width = width;
        newCanvas.height = height + 1;
        const ctx = newCanvas.getContext('2d', { willReadFrequently: true });
        ctx.drawImage(originalCanvas, 0, 1);
        const headerImage = ctx.getImageData(0, 0, width, 1);
        const data = headerImage.data;
        const valX = (gridX + POSITION_OFFSET) >>> 0;
        const valY = (gridY + POSITION_OFFSET) >>> 0;
        const packetSize = 5;
        const maxPackets = Math.floor(width / packetSize);
        for (let i = 0; i < maxPackets; i++) {
            const base = (i * packetSize) * 4;
            data[base] = MARKER_R; data[base + 1] = MARKER_G; data[base + 2] = MARKER_B; data[base + 3] = 255;
            data[base + 4] = (valX >>> 24) & 0xFF; data[base + 5] = (valX >>> 16) & 0xFF; data[base + 6] = 0; data[base + 7] = 255;
            data[base + 8] = (valX >>> 8) & 0xFF; data[base + 9] = valX & 0xFF; data[base + 10] = 0; data[base + 11] = 255;
            data[base + 12] = (valY >>> 24) & 0xFF; data[base + 13] = (valY >>> 16) & 0xFF; data[base + 14] = 0; data[base + 15] = 255;
            data[base + 16] = (valY >>> 8) & 0xFF; data[base + 17] = valY & 0xFF; data[base + 18] = 0; data[base + 19] = 255;
        }
        ctx.putImageData(headerImage, 0, 0);
        return newCanvas;
    }

    function decodeRobustPosition(img) {
        const canvas = document.createElement('canvas');
        canvas.width = img.width;
        canvas.height = img.height;
        const ctx = canvas.getContext('2d', { willReadFrequently: true });
        ctx.drawImage(img, 0, 0);
        const headerData = ctx.getImageData(0, 0, img.width, 1).data;
        const votesX = new Map();
        const votesY = new Map();
        let validPackets = 0;
        const packetSize = 5;
        const maxPackets = Math.floor(img.width / packetSize);
        for (let i = 0; i < maxPackets; i++) {
            const base = (i * packetSize) * 4;
            if (headerData[base] === MARKER_R && headerData[base + 1] === MARKER_G && headerData[base + 2] === MARKER_B && headerData[base + 3] === 255) {
                const xVal = ((headerData[base + 4] << 24) | (headerData[base + 5] << 16) | (headerData[base + 8] << 8) | headerData[base + 9]) >>> 0;
                const yVal = ((headerData[base + 12] << 24) | (headerData[base + 13] << 16) | (headerData[base + 16] << 8) | headerData[base + 17]) >>> 0;
                votesX.set(xVal, (votesX.get(xVal) || 0) + 1);
                votesY.set(yVal, (votesY.get(yVal) || 0) + 1);
                validPackets++;
            }
        }
        if (validPackets === 0) return null;
        const getWinner = (map) => [...map.entries()].reduce((a, b) => b[1] > a[1] ? b : a)[0];
        const gridX = getWinner(votesX) - POSITION_OFFSET;
        const gridY = getWinner(votesY) - POSITION_OFFSET;
        const cleanCanvas = document.createElement('canvas');
        cleanCanvas.width = img.width;
        cleanCanvas.height = img.height - 1;
        const cleanCtx = cleanCanvas.getContext('2d');
        cleanCtx.drawImage(canvas, 0, 1, img.width, img.height - 1, 0, 0, img.width, img.height - 1);
        return { gridX, gridY, cleanCanvas };
    }

    // ========== PREVIEW FUNCTIONALITY ==========

    let previewImageCache = null;
    let previewRenderHandler = null;

    function drawPreviewImageOnCanvas() {
        gpLog("drawPreviewImageOnCanvas called");
        
        if (!previewOverlay) {
            gpLog("No preview overlay, returning");
            return;
        }
        
        if (!previewActive) {
            gpLog("Preview not active, returning");
            return;
        }

        const savedImageData = localStorage.getItem('ghostImageData');
        const savedCoordsStr = localStorage.getItem('ghostImageCoords');
        
        if (!savedCoordsStr || !savedImageData) {
            gpLog("Missing ghost image data or coords in localStorage");
            return;
        }

        const coords = JSON.parse(savedCoordsStr);
        gpLog("Ghost coords", coords);
        
        // Use cached image to avoid reloading
        if (!previewImageCache || previewImageCache.src !== savedImageData) {
            previewImageCache = new Image();
            previewImageCache.src = savedImageData;
            gpLog("Loading new preview image");
        }
        
        const img = previewImageCache;
        if (!img.complete) {
            gpLog("Image not loaded yet, waiting...");
            img.onload = () => {
                gpLog("Image loaded, redrawing");
                drawPreviewImageOnCanvas();
            };
            return;
        }
        
        gpLog("Image loaded, dimensions:", { width: img.width, height: img.height });

        // Get required game variables
        const pixelCanvas = document.getElementById('pixel-canvas');
        if (!pixelCanvas) {
            gpLog("ERROR: pixel-canvas not found");
            return;
        }

        // Match canvas size to pixel canvas
        if (previewOverlay.width !== pixelCanvas.width || previewOverlay.height !== pixelCanvas.height) {
            previewOverlay.width = pixelCanvas.width;
            previewOverlay.height = pixelCanvas.height;
            gpLog("Resized preview canvas to", { width: pixelCanvas.width, height: pixelCanvas.height });
        }

        const ctx = previewOverlay.getContext('2d');
        const { width, height } = previewOverlay;
        ctx.clearRect(0, 0, width, height);
        gpLog("Cleared canvas");

        // Get map and turf using safe helper to avoid DOM element conflicts
        const map = getPageVariable('map');
        const turf = getPageVariable('turf');
        
        // gridSize is often 25 (standard grid size for geopixels)
        // Try to get from page, fallback to defaults
        let gridSize = getPageVariable('gridSize') || 25;
        let halfSize = getPageVariable('halfSize') || (gridSize / 2);
        let offsetMetersX = getPageVariable('offsetMetersX') || 0;
        let offsetMetersY = getPageVariable('offsetMetersY') || 0;
        
        gpLog("Grid values:", { gridSize, halfSize, offsetMetersX, offsetMetersY });

        if (!map || !turf) {
            gpLog("ERROR: Missing required variables", { 
                hasMap: !!map, 
                hasTurf: !!turf, 
                gridSize: gridSize 
            });
            return;
        }

        if (typeof map.project !== 'function') {
            gpLog("ERROR: map.project is not a function", { mapType: typeof map });
            return;
        }

        // Calculate corners using the SAME method as the game's drawGhostImageOnCanvas
        // Top-left pixel center
        const tl_pixel_center_x = coords.gridX * gridSize;
        const tl_pixel_center_y = coords.gridY * gridSize;

        // Top-left mercator edge
        const tl_merc_edge = [
            tl_pixel_center_x - halfSize + offsetMetersX,
            tl_pixel_center_y + halfSize + offsetMetersY
        ];

        // Bottom-right grid coordinates
        const br_pixel_gridX = coords.gridX + img.width - 1;
        const br_pixel_gridY = coords.gridY - img.height + 1;

        const br_pixel_center_x = br_pixel_gridX * gridSize;
        const br_pixel_center_y = br_pixel_gridY * gridSize;

        // Bottom-right mercator edge
        const br_merc_edge = [
            br_pixel_center_x + halfSize + offsetMetersX,
            br_pixel_center_y - halfSize + offsetMetersY
        ];
        
        gpLog("Mercator coords (ghost method)", { tl_merc_edge, br_merc_edge });

        // Convert to WGS84 and then project to screen
        const topLeftScreen = map.project(turf.toWgs84(tl_merc_edge));
        const bottomRightScreen = map.project(turf.toWgs84(br_merc_edge));
        
        gpLog("Screen coords", { topLeftScreen, bottomRightScreen });

        const drawX = topLeftScreen.x;
        const drawY = topLeftScreen.y;
        const screenWidth = bottomRightScreen.x - drawX;
        const screenHeight = bottomRightScreen.y - drawY;
        
        gpLog("Draw position and dimensions", { drawX, drawY, screenWidth, screenHeight });

        // Check if visible
        if (drawX + screenWidth < 0 || 
            drawX > width ||
            drawY + screenHeight < 0 || 
            drawY > height) {
            gpLog("Image not in viewport, skipping draw");
            return;
        }

        // Draw fully opaque
        ctx.imageSmoothingEnabled = false;
        ctx.drawImage(img, drawX, drawY, screenWidth, screenHeight);
        
        gpLog("Drew preview image successfully");
    }

    function togglePreview(button) {
        gpLog("togglePreview called, current state:", previewActive);
        gpLog("Button click - environment check:", {
            windowExists: typeof window !== 'undefined',
            unsafeWindowExists: typeof unsafeWindow !== 'undefined',
            windowKeys: typeof window !== 'undefined' ? Object.keys(window).filter(k => k.includes('map') || k.includes('turf')).slice(0, 10) : [],
            unsafeWindowKeys: typeof unsafeWindow !== 'undefined' ? Object.keys(unsafeWindow).filter(k => k.includes('map') || k.includes('turf')).slice(0, 10) : []
        });
        
        if (previewActive) {
            // Deactivate preview
            gpLog("Deactivating preview");
            
            if (previewOverlay && previewOverlay.parentNode) {
                previewOverlay.parentNode.removeChild(previewOverlay);
                gpLog("Removed preview overlay from DOM");
            }
            
            // Unhook from map events
            if (previewRenderHandler) {
                const map = getPageVariable('map');
                if (map && typeof map.off === 'function') {
                    try {
                        map.off('move', previewRenderHandler);
                        map.off('zoom', previewRenderHandler);
                        map.off('rotate', previewRenderHandler);
                        gpLog("Removed map event listeners");
                    } catch (e) {
                        gpLog("Error removing map listeners", e);
                    }
                }
            }
            
            previewOverlay = null;
            previewImageCache = null;
            previewRenderHandler = null;
            previewActive = false;
            button.innerHTML = '👁️ Preview';
            button.classList.remove('active');
            gpLog("Preview deactivated");
        } else {
            // Activate preview
            gpLog("Activating preview");
            
            const savedImageData = localStorage.getItem('ghostImageData');
            const savedCoordsStr = localStorage.getItem('ghostImageCoords');
            
            if (!savedImageData || !savedCoordsStr) {
                gpLog("ERROR: No ghost image data in localStorage");
                notifyUser("Error", "No ghost image on map to preview.");
                return;
            }

            gpLog("Found ghost data in localStorage");

            // Find the pixel canvas to match its size
            const pixelCanvas = document.getElementById('pixel-canvas');
            if (!pixelCanvas) {
                gpLog("ERROR: pixel-canvas not found");
                notifyUser("Error", "Pixel canvas not found. Make sure you're on the map view.");
                return;
            }

            gpLog("Found pixel canvas", { width: pixelCanvas.width, height: pixelCanvas.height });

            // Verify map exists
            const map = getPageVariable('map');
            if (!map) {
                gpLog("ERROR: map not found in any scope");
                notifyUser("Error", "Map not initialized yet. Please wait a moment and try again.");
                return;
            }
            
            gpLog("Map object found", { 
                mapType: typeof map, 
                hasProject: typeof map.project,
                isHTMLElement: map instanceof HTMLElement,
                constructor: map.constructor ? map.constructor.name : 'unknown'
            });

            if (typeof map.project !== 'function') {
                gpLog("ERROR: map.project is not a function", { 
                    mapType: typeof map,
                    projectType: typeof map.project,
                    mapKeys: Object.keys(map).slice(0, 20),
                    mapConstructor: map.constructor ? map.constructor.name : 'unknown'
                });
                notifyUser("Error", "Map projection not available. Page may not be fully loaded.");
                return;
            }
            
            gpLog("map.project verified as function");

            // Verify turf exists
            const turf = getPageVariable('turf');
            if (!turf) {
                gpLog("ERROR: turf not found in any scope");
                notifyUser("Error", "Turf.js library not loaded. Page may not be fully loaded.");
                return;
            }
            
            gpLog("Turf object found", { turfType: typeof turf, hasToWgs84: typeof turf.toWgs84 });

            if (typeof turf.toWgs84 !== 'function') {
                gpLog("ERROR: turf.toWgs84 is not a function", { 
                    turfType: typeof turf,
                    toWgs84Type: typeof turf.toWgs84,
                    turfKeys: Object.keys(turf).slice(0, 20)
                });
                notifyUser("Error", "Map projection not available. Page may not be fully loaded.");
                return;
            }
            
            gpLog("turf.toWgs84 verified as function");

            gpLog("Map and turf are ready with required functions");

            // Create preview canvas
            previewOverlay = document.createElement('canvas');
            previewOverlay.id = 'gp-preview-canvas';
            previewOverlay.className = 'pixel-perfect';
            previewOverlay.width = pixelCanvas.width;
            previewOverlay.height = pixelCanvas.height;
            previewOverlay.style.cssText = 'display: block; image-rendering: pixelated; position: absolute; top: 0; left: 0; pointer-events: none; z-index: 5;';

            gpLog("Created preview canvas element");

            // Insert into DOM - find the map container
            const mapContainer = map.getContainer ? map.getContainer() : document.getElementById('map');
            if (mapContainer) {
                mapContainer.appendChild(previewOverlay);
                gpLog("Appended preview canvas to map container");
            } else {
                document.body.appendChild(previewOverlay);
                gpLog("Appended preview canvas to body (fallback)");
            }
            
            previewActive = true;
            button.innerHTML = '👁️ Hide Preview';
            button.classList.add('active');

            // Create render handler
            previewRenderHandler = () => {
                gpLog("Map event triggered, redrawing preview");
                drawPreviewImageOnCanvas();
            };

            // Hook into map events (same as geopixels++)
            try {
                map.on('move', previewRenderHandler);
                map.on('zoom', previewRenderHandler);
                map.on('rotate', previewRenderHandler);
                gpLog("Attached to map events");
            } catch (e) {
                gpLog("ERROR attaching map listeners", e);
            }

            // Render once immediately
            gpLog("Drawing initial preview");
            drawPreviewImageOnCanvas();
            
            gpLog("Preview activated successfully");
        }
    }

    /**
     * Replicates the logic of the 'Save Pos' button to cache the currently placed ghost image.
     * This function is available globally but is no longer called automatically.
     */
    async function cacheCurrentGhostPosition() {
        const savedCoordsStr = localStorage.getItem('ghostImageCoords');
        const savedImageData = localStorage.getItem('ghostImageData');
        if (!savedCoordsStr || !savedImageData) {
            gpLog("Auto-Cache: No ghost image on map or coordinates found.");
            return;
        }
        gpLog("Auto-Cache: Starting cache process.");

        const coords = JSON.parse(savedCoordsStr);
        const img = new Image();
        img.src = savedImageData;
        await new Promise(r => img.onload = r);

        const tempCanvas = document.createElement('canvas');
        tempCanvas.width = img.width; tempCanvas.height = img.height;
        tempCanvas.getContext('2d').drawImage(img, 0, 0);

        const encodedCanvas = encodeRobustPosition(tempCanvas, coords.gridX, coords.gridY);
        encodedCanvas.toBlob(async (blob) => {
            if(!blob) return;

            // Save to History (Cache)
            try {
                await HistoryManager.add(blob, `Backup_${coords.gridX}_${coords.gridY}`);
                gpLog("Auto-Cache: Cached image with position data.");
                notifyUser("Auto-Cache", `Ghost image position ${coords.gridX}, ${coords.gridY} auto-cached.`);
            } catch (e) {
                console.error("Auto-Cache failed", e);
                notifyUser("Auto-Cache Error", "Failed to auto-cache the image position.");
            }
        }, 'image/png');
    }
    // Expose for direct use if needed, but primarily used internally now
    window.cacheCurrentGhostPosition = cacheCurrentGhostPosition;


    // ========== GAME INTEGRATION ==========

    function applyCoordinatesToGame(coords) {
        gpLog("Applying coordinates...", coords);
        let attempts = 0;
        const interval = setInterval(() => {
            const placeBtn = document.getElementById('initiatePlaceGhostBtn');
            if (placeBtn && !placeBtn.disabled) {
                clearInterval(interval);
                localStorage.setItem('ghostImageCoords', JSON.stringify(coords));
                
                // Get function using safe helper
                const initializeGhostFromStorage = getPageVariable('initializeGhostFromStorage');
                
                if (typeof initializeGhostFromStorage === 'function') {
                    gpLog("Calling initializeGhostFromStorage to place template");
                    initializeGhostFromStorage();
                    notifyUser("Auto-Place", `Position detected: ${coords.gridX}, ${coords.gridY}`);
                } else {
                    gpLog("ERROR: initializeGhostFromStorage function not found");
                    notifyUser("Warning", `Position set to ${coords.gridX}, ${coords.gridY} but auto-place failed. Click 'Place on Map' manually.`);
                }
            }
            if (++attempts > 50) {
                clearInterval(interval);
                gpLog("Timeout waiting for place button to be ready");
            }
        }, 100);
    }

    async function loadImageToCanvas(blob) {
        return new Promise((resolve, reject) => {
            const img = new Image();
            img.onload = () => resolve(img);
            img.onerror = reject;
            img.src = URL.createObjectURL(blob);
        });
    }

    // ========== PROCESSING LOGIC ==========

    async function processAndLoadImage(file, saveToHistory = true) {
        gpLog("Processing image...");
        const placeBtn = document.getElementById('initiatePlaceGhostBtn');
        if (placeBtn) { placeBtn.innerText = "Analyzing..."; placeBtn.disabled = true; }

        try {
            const img = await loadImageToCanvas(file);
            const decoded = decodeRobustPosition(img);

            let finalFile = file;
            let coords = null;

            if (decoded) {
                gpLog("Found encoded position.", { gridX: decoded.gridX, gridY: decoded.gridY });
                coords = { gridX: decoded.gridX, gridY: decoded.gridY };
                const cleanBlob = await new Promise(r => decoded.cleanCanvas.toBlob(r, 'image/png'));
                finalFile = new File([cleanBlob], file.name || "ghost.png", { type: "image/png" });
            } else {
                gpLog("No encoded position found in image");
            }

            if (saveToHistory) {
                await HistoryManager.add(file, file.name);
            }

            const input = document.getElementById('ghostImageInput');
            const dt = new DataTransfer();
            dt.items.add(finalFile);
            input.files = dt.files;

            isInternalUpdate = true;
            input.dispatchEvent(new Event('change', { bubbles: true }));
            isInternalUpdate = false;

            // Wait for the game to process the image first
            await new Promise(resolve => setTimeout(resolve, 100));

            if (coords) {
                gpLog("Applying coordinates to game", coords);
                applyCoordinatesToGame(coords);
            } else {
                // Clear old coordinates if this template has no encoded position
                localStorage.removeItem('ghostImageCoords');
                gpLog("No encoded position found, cleared old coordinates");
            }

        } catch (e) {
            console.error(e);
            notifyUser("Error", "Failed to process image.");
        } finally {
            if (placeBtn) placeBtn.innerText = "Place on Map";
        }
    }

    // ========== INTERCEPTOR ==========

    function setupNativeInterceptor() {
        const input = document.getElementById('ghostImageInput');
        if (!input) return;

        // 3. Add .zip to the file input's accepted types
        input.setAttribute('accept', 'image/png, image/jpeg, image/webp, image/gif, application/zip, .zip');

        input.addEventListener('change', async (e) => {
            if (isInternalUpdate) return;
            const file = e.target.files[0];
            if (!file) return;
            e.stopImmediatePropagation();
            e.preventDefault();

            // Check if it's a ZIP file
            if (file.type === 'application/zip' || file.type === 'application/x-zip-compressed' || file.name.toLowerCase().endsWith('.zip')) {
                gpLog("Detected ZIP file upload");
                const success = await importFromZip(file);
                if (success) {
                    // Clear the input so same file can be uploaded again
                    input.value = '';
                }
                return;
            }

            // Otherwise process as image
            processAndLoadImage(file, false);
        }, true);
    }

    // ========== UI HANDLERS ==========

    async function handleUrlUpload() {
        const url = prompt("Enter Image or ZIP URL:");
        if (!url) return;
        
        try {
            // Use GM_xmlhttpRequest to bypass CSP restrictions
            const blob = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: url,
                    responseType: 'blob',
                    onload: (response) => {
                        if (response.status >= 200 && response.status < 300) {
                            resolve(response.response);
                        } else {
                            reject(new Error(`HTTP ${response.status}: ${response.statusText}`));
                        }
                    },
                    onerror: (error) => {
                        reject(new Error('Failed to fetch URL'));
                    },
                    ontimeout: () => {
                        reject(new Error('Request timed out'));
                    }
                });
            });

            // Check if it's a ZIP file
            if (blob.type === 'application/zip' || blob.type === 'application/x-zip-compressed' || url.toLowerCase().endsWith('.zip')) {
                gpLog("Detected ZIP file from URL");
                await importFromZip(blob);
                notifyUser("Success", "Imported cache from URL!");
                return;
            }

            // Otherwise treat as image
            if (!blob.type.startsWith('image/')) throw new Error("Invalid image");
            processAndLoadImage(new File([blob], "url_upload.png", { type: blob.type }), false);
        } catch (e) {
            console.error(e);
            notifyUser("Error", "Could not load file from URL: " + e.message);
        }
    }

    async function downloadWithPos() {
        const savedImageData = localStorage.getItem('ghostImageData');
        if (!savedImageData) {
            notifyUser("Error", "No ghost image loaded.");
            return;
        }
        
        const savedCoordsStr = localStorage.getItem('ghostImageCoords');
        const img = new Image();
        img.src = savedImageData;
        await new Promise(r => img.onload = r);

        const tempCanvas = document.createElement('canvas');
        tempCanvas.width = img.width; tempCanvas.height = img.height;
        tempCanvas.getContext('2d').drawImage(img, 0, 0);

        if (savedCoordsStr) {
            // If coordinates exist, encode them and save
            const coords = JSON.parse(savedCoordsStr);
            const encodedCanvas = encodeRobustPosition(tempCanvas, coords.gridX, coords.gridY);
            encodedCanvas.toBlob(async (blob) => {
                if(!blob) return;

                // Save to History (Cache)
                try {
                    await HistoryManager.add(blob, `Backup_${coords.gridX}_${coords.gridY}`);
                    gpLog("Cached image with position data");
                    notifyUser("Success", "Template saved to history!");
                } catch (e) {
                    console.error("Cache failed", e);
                    notifyUser("Error", "Failed to save template");
                }
            }, 'image/png');
        } else {
            // No coordinates: just save the image as-is
            tempCanvas.toBlob(async (blob) => {
                if(!blob) return;
                
                try {
                    await HistoryManager.add(blob, `Image_${Date.now()}`);
                    gpLog("Cached image without position data");
                    notifyUser("Success", "Template saved to history!");
                } catch (e) {
                    console.error("Cache failed", e);
                    notifyUser("Error", "Failed to save template");
                }
            }, 'image/png');
        }
    }

    async function openHistoryModal() {
        const existing = document.getElementById('gp-history-modal');
        if (existing) existing.remove();

        const images = await HistoryManager.getAll();
        const modal = document.createElement('div');
        modal.id = 'gp-history-modal';
        modal.className = 'gp-to-modal-overlay';
        modal.innerHTML = `
            <div class="gp-to-modal-panel">
                <div class="gp-to-header">
                    <span class="gp-to-title">Image History (${images.length})</span>
                    <div class="flex gap-2">
                        <button id="gp-export-zip" class="gp-to-btn gp-to-btn-orange text-xs">💾 Export JSON</button>
                        <button id="gp-import-zip" class="gp-to-btn gp-to-btn-green text-xs">📁 Import JSON</button>
                        <button id="gp-clear-all" class="gp-to-btn gp-to-btn-red text-xs">Clear All</button>
                        <button id="gp-close-hist" class="gp-to-btn gp-to-btn-gray">Close</button>
                    </div>
                </div>
                <div class="gp-to-grid" id="gp-history-grid">
                    ${images.length === 0 ? '<p class="p-4 text-gray-500 col-span-full text-center">No images found.</p>' : ''}
                </div>
            </div>
        `;
        document.body.appendChild(modal);

        const grid = modal.querySelector('#gp-history-grid');
        images.forEach(imgData => {
            const card = document.createElement('div');
            card.className = 'gp-to-card';
            card.innerHTML = `
                <button class="gp-to-delete-btn" title="Delete">✖</button>
                <img src="${URL.createObjectURL(imgData.blob)}" />
                <div class="gp-to-card-footer">${new Date(imgData.date).toLocaleTimeString()} - ${imgData.name.substring(0,12)}</div>
            `;
            card.onclick = (e) => {
                if (e.target.closest('.gp-to-delete-btn')) return;
                processAndLoadImage(imgData.blob, false);
                modal.remove();
            };
            card.querySelector('.gp-to-delete-btn').onclick = async () => {
                await HistoryManager.delete(imgData.id);
                card.remove();
            };
            grid.appendChild(card);
        });

        modal.querySelector('#gp-export-zip').onclick = async () => {
            await exportToZip();
        };

        modal.querySelector('#gp-import-zip').onclick = () => {
            const input = document.createElement('input');
            input.type = 'file';
            input.accept = '.json, .zip, application/json, application/zip'; // Accept JSON (new) and ZIP (legacy)
            input.onchange = async (e) => {
                const file = e.target.files[0];
                if (file) {
                    await importFromZip(file);
                    modal.remove();
                    openHistoryModal(); // Refresh the modal
                }
            };
            input.click();
        };

        modal.querySelector('#gp-clear-all').onclick = async () => {
            if(confirm("Clear all cached images?")) {
                await HistoryManager.clear();
                modal.remove();
            }
        };
        modal.querySelector('#gp-close-hist').onclick = () => modal.remove();
    }

    // ========== INJECTION ==========

    /**
     * Watches the document for the coordinate-setting success message
     * and triggers the auto-cache function.
     * This addresses issue #2.
     */
    function setupAlertBodyObserver() {
        const targetNode = document.getElementById('alertBody');
        if (!targetNode) {
             gpLog("Could not find alertBody for position observer.");
             return;
        }

        const observer = new MutationObserver((mutationsList, observer) => {
            for(const mutation of mutationsList) {
                if (mutation.type === 'childList' || mutation.type === 'characterData') {
                    const textContent = targetNode.textContent;
                    if (textContent && textContent.includes("Ghost image position set")) {
                        gpLog("Detected 'Ghost image position set'. Triggering auto-cache.");
                        cacheCurrentGhostPosition();
                        // Disconnect after first success to avoid spamming the cache,
                        // as a new observer will be created when the modal is opened next.
                        observer.disconnect();
                        break;
                    }
                }
            }
        });

        // Start observing the target node for configured mutations
        const config = { childList: true, subtree: true, characterData: true };
        observer.observe(targetNode, config);
    }

    function injectControls() {
        const modal = document.getElementById('ghostImageModal');
        if (!modal) return;
        const container = modal.querySelector('.flex.flex-wrap.items-center.justify-center.gap-3');
        if (!container || container.dataset.gpInjected) return;
        container.dataset.gpInjected = "true";

        // 1. Remove the 'hidden' class from the hexDisplay span
        const hexDisplay = document.getElementById('hexDisplay');
        if (hexDisplay) {
            hexDisplay.classList.remove('hidden');
            gpLog("Removed 'hidden' class from hexDisplay.");
        }

        setupNativeInterceptor();

        const btnUrl = document.createElement('button');
        btnUrl.innerHTML = '🔗 URL'; btnUrl.className = 'gp-to-btn gp-to-btn-blue shadow';
        btnUrl.title = 'Load from URL (Image or ZIP)';
        btnUrl.onclick = handleUrlUpload;

        const btnLocal = document.createElement('button');
        btnLocal.innerHTML = '📂 File'; btnLocal.className = 'gp-to-btn gp-to-btn-green shadow';
        btnLocal.title = 'Upload Image or ZIP';
        // Note: The click handler for this just triggers the native input, which we intercept.
        btnLocal.onclick = () => document.getElementById('ghostImageInput').click();

        const btnHist = document.createElement('button');
        btnHist.innerHTML = '📜 History'; btnHist.className = 'gp-to-btn gp-to-btn-purple shadow';
        btnHist.onclick = openHistoryModal;

        const btnDL = document.createElement('button');
        btnDL.innerHTML = '💾 Save'; btnDL.className = 'gp-to-btn gp-to-btn-gray shadow';
        btnDL.onclick = downloadWithPos;

        const btnPreview = document.createElement('button');
        btnPreview.innerHTML = '👁️ Preview'; 
        btnPreview.className = 'gp-to-btn gp-to-btn-cyan shadow';
        btnPreview.title = 'Toggle image preview overlay';
        btnPreview.onclick = () => togglePreview(btnPreview);

        const btnGoTo = document.createElement('button');
        btnGoTo.innerHTML = '🎯 Go To'; 
        btnGoTo.className = 'gp-to-btn gp-to-btn-orange shadow';
        btnGoTo.title = 'Teleport to template location';
        btnGoTo.onclick = goToTemplateLocation;

        container.prepend(btnGoTo);
        container.prepend(btnPreview);
        container.prepend(btnDL);
        container.prepend(btnHist);
        container.prepend(btnLocal);
        container.prepend(btnUrl);

        // Auto-caching disabled - user must manually press Save Pos button
        // setupAlertBodyObserver();
    }

    const observer = new MutationObserver(() => injectControls());
    observer.observe(document.body, { childList: true, subtree: true });

    document.querySelector('label[for="ghostImageInput"]')?.classList.add('hidden');

    gpLog("GeoPixels Ultimate Ghost Template Manager v3.4 Loaded (with uint8array ZIP fix)");

})();