Drawaria.online Avatar Switcher

Switch between two saved avatars in Drawaria.online avatar builder.

// ==UserScript==
// @name         Drawaria.online Avatar Switcher
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Switch between two saved avatars in Drawaria.online avatar builder.
// @author       YouTubeDrawaria
// @match        *://drawaria.online/avatar/builder*
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        unsafeWindow
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=drawaria.online
// ==/UserScript==

(function($) { // Tampermonkey should pass the @require'd jQuery as $
    'use strict';

    // unsafeWindow is directly available due to @grant unsafeWindow
    const pageWindow = unsafeWindow;

    const AVATAR_SLOT_KEYS = {
        1: 'avatar_switcher_slot_1_image',
        2: 'avatar_switcher_slot_2_image'
    };
    const SWITCHER_PANEL_ID = 'avatar-switcher-panel';
    let scriptInitialized = false;

    function log(message, data) { console.log(`[Drawaria Avatar Switcher v0.5.1] ${message}`, data || ''); }
    function warn(message, data) { console.warn(`[Drawaria Avatar Switcher v0.5.1] ${message}`, data || ''); }
    function error(message, data) { console.error(`[Drawaria Avatar Switcher v0.5.1] ${message}`, data || ''); }

    // --- START OF COPIED FUNCTIONS (Ensure these are filled from v0.5) ---
    function addSwitcherStyles() {
        const styles = `
            #${SWITCHER_PANEL_ID} { background-color: #f0f0f0; border: 1px solid #ccc; padding: 10px; margin-top: 20px; border-radius: 5px; }
            #${SWITCHER_PANEL_ID} h3 { text-align: center; margin-top: 0; margin-bottom: 10px; font-size: 16px; color: #333; }
            .${SWITCHER_PANEL_ID}-container { display: flex; justify-content: space-around; align-items: flex-start; }
            .${SWITCHER_PANEL_ID}-slot { display: flex; flex-direction: column; align-items: center; width: 45%; }
            .${SWITCHER_PANEL_ID}-preview-circle { width: 100px; height: 100px; border-radius: 50%; background-color: #fff; border: 2px solid #aaa; display: flex; justify-content: center; align-items: center; text-align: center; margin-bottom: 10px; overflow: hidden; font-size: 14px; color: #555; }
            .${SWITCHER_PANEL_ID}-preview-circle img { width: 100%; height: 100%; object-fit: cover; }
            #${SWITCHER_PANEL_ID} button { background-color: #fff; border: 1px solid #000; color: #000; padding: 5px 10px; margin-top: 5px; cursor: pointer; border-radius: 4px; font-size: 12px; width: 80px; }
            #${SWITCHER_PANEL_ID} button:hover { background-color: #e0e0e0; }
        `;
        $('<style>').prop('type', 'text/css').html(styles).appendTo('head');
    }

    function createSwitcherPanelHTML() {
        return `
            <div id="${SWITCHER_PANEL_ID}">
                <h3>2 avatars switcher</h3>
                <div class="${SWITCHER_PANEL_ID}-container">
                    <div class="${SWITCHER_PANEL_ID}-slot" id="avatar-slot-1-container"><div class="${SWITCHER_PANEL_ID}-preview-circle" id="avatar-preview-1"><span>avatar 1</span></div><button class="upload-btn" data-slot="1">upload</button><button class="delete-btn" data-slot="1">delete</button><button class="use-btn" data-slot="1">use</button></div>
                    <div class="${SWITCHER_PANEL_ID}-slot" id="avatar-slot-2-container"><div class="${SWITCHER_PANEL_ID}-preview-circle" id="avatar-preview-2"><span>avatar 2</span></div><button class="upload-btn" data-slot="2">upload</button><button class="delete-btn" data-slot="2">delete</button><button class="use-btn" data-slot="2">use</button></div>
                </div>
            </div>`;
    }

     function findKnownCategoryAnchor() {
        const knownExistingCategoryTexts = ["FACE", "EYES", "MOUTH", "ACCESSORIES"];
        const allElements = document.querySelectorAll('body *');
        for (const categoryText of knownExistingCategoryTexts) {
            for (let el of allElements) {
                if (el.childNodes.length === 1 && el.firstChild.nodeType === Node.TEXT_NODE && el.firstChild.textContent.trim().toUpperCase() === categoryText) { log(`Found category anchor (type 1): "${categoryText}" in`, el); return el; }
                if (el.textContent.trim().toUpperCase() === categoryText && el.children.length < 2) { if (['LI', 'DIV', 'SPAN', 'H3', 'H4', 'P'].includes(el.tagName)) { if ($(el).closest('ul').length > 0 || $(el).closest('.Panel.parts').length > 0 ) { log(`Found category anchor (type 2): "${categoryText}" in`, el); return el; } } }
            }
        }
        warn('No known category anchor element found.'); return null;
    }

    function injectSwitcherPanel() {
        const panelHTML = createSwitcherPanelHTML(); let $panel = $(panelHTML); const knownCategoryElement = findKnownCategoryAnchor();
        if (knownCategoryElement) {
            const $partsListUl = $(knownCategoryElement).closest('ul');
            if ($partsListUl.length) { const $container = $partsListUl.parent(); if ($container.length && !$container.is('body') && !$container.is('html')) { $container.append($panel); log('Avatar Switcher panel appended to parent of parts list UL.'); return; } else { $partsListUl.after($panel); log('Avatar Switcher panel appended *after* parts list UL.'); return; } }
            else { const $categoryParent = $(knownCategoryElement).parent(); if ($categoryParent.length && !$categoryParent.is('body') && !$categoryParent.is('html')) { $categoryParent.append($panel); log('Avatar Switcher panel appended to parent of category anchor.'); return; } }
        }
        const $targetContainer = $('.column.right .Panel.parts');
        if ($targetContainer.length) { $targetContainer.append($panel); log('Avatar Switcher panel appended to .column.right .Panel.parts'); }
        else { warn('Could not find .Panel.parts. Appending to body.'); $('body').append($panel); }
    }

    async function displayAvatarInSlotPreview(slotNumber, imageUrl) { const previewCircle = $(`#avatar-preview-${slotNumber}`); if (imageUrl) { previewCircle.html(`<img src="${imageUrl}" alt="Avatar ${slotNumber}">`); } else { previewCircle.html(`<span>avatar ${slotNumber}</span>`); } }
    async function loadAndDisplayAvatars() { for (const slotNumber of [1, 2]) { const imageUrl = await GM_getValue(AVATAR_SLOT_KEYS[slotNumber], null); await displayAvatarInSlotPreview(slotNumber, imageUrl); } log('Finished loading saved avatars.'); }
    async function handleUploadClick(slotNumber) { const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*'; input.onchange = async (event) => { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = async (e) => { const imageDataUrl = e.target.result; try { await GM_setValue(AVATAR_SLOT_KEYS[slotNumber], imageDataUrl); await displayAvatarInSlotPreview(slotNumber, imageDataUrl); log(`Avatar for slot ${slotNumber} uploaded.`); } catch (err) { error(`Error saving avatar for slot ${slotNumber}:`, err); alert(`Failed to save avatar slot ${slotNumber}.`); } }; reader.onerror = () => { error('FileReader failed.'); alert('Error reading file.'); }; reader.readAsDataURL(file); }; input.click(); }
    async function handleDeleteClick(slotNumber) { if (confirm(`Delete avatar in slot ${slotNumber}?`)) { await GM_deleteValue(AVATAR_SLOT_KEYS[slotNumber]); await displayAvatarInSlotPreview(slotNumber, null); log(`Avatar for slot ${slotNumber} deleted.`); } }
    // --- END OF COPIED FUNCTIONS ---

    async function handleUseClick(slotNumber) {
        const base64RawImageData = await GM_getValue(AVATAR_SLOT_KEYS[slotNumber], null);
        if (!base64RawImageData) {
            alert(`No avatar saved in slot ${slotNumber}. Upload an image first.`);
            return;
        }
        log(`Attempting to use avatar from slot ${slotNumber}`);

        if (typeof pageWindow.ACCOUNT_AVATARSAVE === 'undefined' || !Array.isArray(pageWindow.ACCOUNT_AVATARSAVE)) {
            error('pageWindow.ACCOUNT_AVATARSAVE is not defined or not an array AT THE TIME OF USE.', pageWindow.ACCOUNT_AVATARSAVE);
            alert('Error: Avatar configuration (ACCOUNT_AVATARSAVE) is missing or invalid. Cannot save.');
            return;
        }

        const base64JpegImageData = base64RawImageData.replace(/^data:[^;]+;/, 'data:image/jpeg;');
        const avatarPreviewImg = document.querySelector('.Panel.preview img.AvatarImage, .Panel.preview img');
        if (avatarPreviewImg) {
            avatarPreviewImg.src = base64RawImageData;
            log('Main avatar preview image updated.');
        } else {
            warn('Main avatar preview image element not found when trying to update preview.');
        }

        log('Current ACCOUNT_AVATARSAVE (from pageWindow):', JSON.stringify(pageWindow.ACCOUNT_AVATARSAVE));
        triggerSaveToServer(base64JpegImageData, pageWindow.ACCOUNT_AVATARSAVE);
    }

    function triggerSaveToServer(formattedBase64ImageData, avatarSaveObjectFromPage) {
        if (typeof pageWindow.LOGGEDIN === 'undefined') {
            warn('pageWindow.LOGGEDIN is undefined. Saving might fail or be treated as guest.');
        }

        log('Attempting to save avatar...');
        $.ajax({
            url: pageWindow.LOGGEDIN ? '/saveavatar' : '/uploadavatarimage',
            type: 'POST',
            data: {
                'avatarsave_builder': JSON.stringify(avatarSaveObjectFromPage),
                'imagedata': formattedBase64ImageData,
                'fromeditor': true
            },
            xhr: () => {
                var xhr = new window.XMLHttpRequest();
                xhr.upload.addEventListener('progress', evt => {
                    if (evt.lengthComputable) {
                        var percentComplete = (evt.loaded / evt.total) * 100;
                        log(`Upload Progress: ${percentComplete.toFixed(0)}%`);
                    }
                }, false);
                return xhr;
            }
        }).done(data => {
            log('Avatar data sent, server responded with cache key:', data);
            fetch(`${location.origin}/avatar/cache/${data}.jpg`, { method: 'GET', mode: 'cors', cache: 'reload' })
                .then(response => {
                    if (!response.ok) throw new Error(`Failed to fetch cached avatar: ${response.statusText} (status: ${response.status})`);
                    return response.blob();
                })
                .then(() => {
                    log('Avatar saved and cache confirmed! Redirecting to origin.');
                    alert('Avatar saved successfully!');
                    location.href = new URL(location.href).origin;
                })
                .catch(err => {
                    error('Error confirming saved avatar from cache.', err);
                    alert(`Avatar might be saved, but confirmation failed: ${err.message}. Please check your profile.`);
                    location.href = new URL(location.href).origin;
                });
        }).fail((jqXHR, textStatus, errorThrown) => {
            error('$.ajax save request failed.', { status: textStatus, error: errorThrown, response: jqXHR.responseText });
            alert(`Failed to save avatar: ${errorThrown || textStatus}. Server response: ${jqXHR.responseText}`);
        });
    }

    function runMainLogic() {
        if (scriptInitialized) {
            log('Main logic already initialized.');
            return;
        }
        scriptInitialized = true;
        log('Key elements and ACCOUNT_AVATARSAVE are present. Initializing main logic.');

        addSwitcherStyles();
        injectSwitcherPanel();
        loadAndDisplayAvatars();

        $(`#${SWITCHER_PANEL_ID}`).on('click', '.upload-btn', async function() { await handleUploadClick($(this).data('slot')); });
        $(`#${SWITCHER_PANEL_ID}`).on('click', '.delete-btn', async function() { await handleDeleteClick($(this).data('slot')); });
        $(`#${SWITCHER_PANEL_ID}`).on('click', '.use-btn', async function() { await handleUseClick($(this).data('slot')); });

        log('Avatar Switcher UI injected and event listeners attached.');
    }

    function initializeWhenReady() {
        if (!window.location.pathname.includes('/avatar/builder')) return;
        log('Avatar builder page. Waiting for page resources...');

        if (typeof $ === 'undefined') { error("jQuery ($) not available!"); return; } // Check jQuery from @require
        if (typeof GM_setValue === 'undefined') { error("GM functions not available!"); return; }

        let attempts = 0;
        const maxAttempts = 150; // Max 15 seconds (150 * 100ms)
        const checkInterval = 100; // Check every 100ms

        const intervalId = setInterval(() => {
            attempts++;
            const mainPanel = document.querySelector('main .Panel.preview');
            const accountSaveReady = typeof pageWindow.ACCOUNT_AVATARSAVE !== 'undefined' && Array.isArray(pageWindow.ACCOUNT_AVATARSAVE);

            if (mainPanel && accountSaveReady) {
                log(`Page resources ready after ${attempts * checkInterval}ms. ACCOUNT_AVATARSAVE found.`);
                clearInterval(intervalId);
                runMainLogic();
            } else if (attempts >= maxAttempts) {
                clearInterval(intervalId);
                error(`Page resources not ready after ${maxAttempts * checkInterval}ms. ACCOUNT_AVATARSAVE ready: ${accountSaveReady}, Main panel found: ${!!mainPanel}`);
                if (!accountSaveReady) {
                    error('CRITICAL: pageWindow.ACCOUNT_AVATARSAVE is STILL not available. Avatar saving will likely fail.');
                    alert('Avatar Switcher: Critical configuration missing. Functionality may be broken.');
                }
                if (mainPanel) { warn('Attempting to run main logic despite potential issues.'); runMainLogic(); }
                else { error('Main panel element also not found. Cannot initialize script.'); }
            } else {
                if (attempts % 20 === 0) { log(`Still waiting for resources... Attempt ${attempts}. ACCOUNT_AVATARSAVE: ${accountSaveReady}, Main panel: ${!!mainPanel}`); }
            }
        }, checkInterval);
    }

    // Start the initialization process. $ should be jQuery here.
    $(initializeWhenReady);

})(jQuery); // Pass the jQuery provided by @require into the IIFE