Drawaria.online Custom Emojis! :3

Adds new custom emojis to Drawaria.online avatar builder and attempts to save them to the profile when clicked.

// ==UserScript==
// @name         Drawaria.online Custom Emojis! :3
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Adds new custom emojis to Drawaria.online avatar builder and attempts to save them to the profile when clicked.
// @author       YouTubeDrawaria
// @match        *://drawaria.online/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @icon         https://www.google.com/s2/favicons?sz=64&domain=drawaria.online
// @grant        none
// @license MIT
// ==/UserScript==

(function($, undefined) { // jQuery noConflict wrapper
    'use strict';

    // --- CONFIGURATION ---
    const newCustomEmojis = [
        // Emojis anteriores que se conservan (Mono original eliminado)
        { name: 'Ghost',  id: 'custom_face_ghost',  url: 'https://i.ibb.co/G4rcDxtr/5b17de15-3964-46ec-8af9-f5980940123b-removebg-preview.png' },
        { name: 'Dog',    id: 'custom_face_dog',    url: 'https://i.ibb.co/3mJvGWng/dog-face-removebg-preview.png' },

        // Nuevos emojis agregados
        { name: 'Monkey', id: 'custom_face_monkey_new', url: 'https://i.postimg.cc/mDkXk9Rs/monkey-face-removebg-preview.png' },
        { name: 'Demon', id: 'custom_face_demon', url: 'https://i.postimg.cc/7Y3ppgZs/smiling-face-with-horns-removebg-preview.png' },
        { name: 'Exploding Head', id: 'custom_face_exploding_head', url: 'https://emojiisland.com/cdn/shop/products/7_large.png' },
        { name: 'OMG Face', id: 'custom_face_omg', url: 'https://emojiisland.com/cdn/shop/products/OMG_Emoji_Icon_0cda9b05-20a8-47f0-b80f-df5c982e0963_large.png' },
        { name: 'Tears Face', id: 'custom_face_tears', url: 'https://emojiisland.com/cdn/shop/products/Tears_Emoji_Icon_2_large.png?v=1571606092' },
        { name: 'Pumpkin', id: 'custom_face_pumpkin', url: 'https://emojiisland.com/cdn/shop/products/Pumpkin_Emoji_Icon_169be5a5-3354-4573-b327-5d69e23e8458_large.png' },
        { name: 'Cold Face', id: 'custom_face_cold', url: 'https://emojiisland.com/cdn/shop/products/9_large.png' },
        { name: 'Hot Face', id: 'custom_face_hot', url: 'https://emojiisland.com/cdn/shop/products/8_large.png' },
        { name: 'Raised Eyebrow', id: 'custom_face_raised_eyebrow', url: 'https://i.ibb.co/v6xdzRnm/original-removebg-preview.png' },
        { name: 'Nerd Face', id: 'custom_face_nerd', url: 'https://emojiisland.com/cdn/shop/products/Nerd_Emoji_Icon_4ab932f8-9ec9-4180-8420-c74b84546f57_large.png' },
        { name: 'Poop Face', id: 'custom_face_poop', url: 'https://emojiisland.com/cdn/shop/products/Poop_Emoji_2_large.png?v=1571606092' }
    ];
    const customCategoryName = "Custom Faces";
    const knownExistingCategoryText = "FACE"; // O "EYE", "MOUTH". Se usa como ancla.
    const customCategoryMarkerAttribute = "data-custom-category-scripted";
    const customEmojiMarkerAttribute = "data-custom-emoji-scripted";
    // --- END CONFIGURATION ---

    let customCategoryAdded = false;
    let observer = null;

    function log(message, data) { console.log(`[Drawaria Custom Emojis v0.9.1] ${message}`, data || ''); }
    function warn(message, data) { console.warn(`[Drawaria Custom Emojis v0.9.1] ${message}`, data || ''); }
    function error(message, data) { console.error(`[Drawaria Custom Emojis v0.9.1] ${message}`, data || ''); }

    function cloneAndModifyAssetLi(emojiData, templateAssetLi) {
        if (!templateAssetLi) {
            error('No templateAssetLi provided for cloning!');
            const newItem = document.createElement('li');
            const img = document.createElement('img');
            newItem.title = emojiData.name; img.src = emojiData.url; img.alt = emojiData.name;
            img.className = 'asset'; img.draggable = false; img.style.objectFit = 'contain';
            newItem.appendChild(img);
            newItem.setAttribute(customEmojiMarkerAttribute, 'true');
            newItem.setAttribute('data-custom-emoji-id', emojiData.id);
            newItem.addEventListener('click', (event) => {
                event.stopPropagation(); event.preventDefault();
                handleCustomEmojiClick(emojiData, newItem);
            });
            return newItem;
        }
        const newItem = templateAssetLi.cloneNode(true);
        newItem.title = emojiData.name;
        const img = newItem.querySelector('img');
        if (img) {
            img.src = emojiData.url; img.alt = emojiData.name;
            if (!img.style.objectFit) img.style.objectFit = 'contain';
            if (img.draggable === undefined) img.draggable = false;
            if (img.srcset) img.srcset = '';
        } else {
            warn('Template asset li did not contain an img. Creating one.');
            const newImg = document.createElement('img');
            newImg.src = emojiData.url; newImg.alt = emojiData.name; newImg.className = 'asset';
            newImg.draggable = false; newImg.style.objectFit = 'contain';
            newItem.innerHTML = ''; newItem.appendChild(newImg);
        }
        newItem.id = '';
        newItem.setAttribute(customEmojiMarkerAttribute, 'true');
        newItem.setAttribute('data-custom-emoji-id', emojiData.id);
        newItem.onclick = null;
        newItem.addEventListener('click', (event) => {
            event.stopPropagation(); event.preventDefault();
            handleCustomEmojiClick(emojiData, newItem);
        });
        return newItem;
    }

    function cloneAndModifyCategoryHeaderLi(categoryName, templateCategoryLi) {
        if (!templateCategoryLi) {
            error('No templateCategoryLi provided for cloning!');
            const newHeader = document.createElement('li');
            newHeader.className = 'category'; newHeader.textContent = categoryName;
            newHeader.setAttribute(customCategoryMarkerAttribute, 'true');
            return newHeader;
        }
        const newHeader = templateCategoryLi.cloneNode(true);
        newHeader.textContent = categoryName; newHeader.id = '';
        newHeader.setAttribute(customCategoryMarkerAttribute, 'true');
        return newHeader;
    }

    function handleCustomEmojiClick(emojiData, clickedLiElement) {
        log(`Clicked ${emojiData.name}, URL: ${emojiData.url}`);

        const avatarPreviewImg = document.querySelector('.Panel.preview img.AvatarImage, .Panel.preview img');
        if (avatarPreviewImg) {
            avatarPreviewImg.src = emojiData.url;
            log('Avatar preview image updated.');
        } else {
            warn('Avatar preview image element not found.');
        }

        if (window.ACCOUNT_AVATARSAVE && typeof window.ACCOUNT_AVATARSAVE === 'object') {
            window.ACCOUNT_AVATARSAVE.face = emojiData.id;
            log('Attempted to update ACCOUNT_AVATARSAVE.face to:', window.ACCOUNT_AVATARSAVE.face);
            log('Current ACCOUNT_AVATARSAVE:', JSON.stringify(window.ACCOUNT_AVATARSAVE));
        } else {
            warn('window.ACCOUNT_AVATARSAVE not found or not an object. Saving might fail or use default face.');
            if (!window.ACCOUNT_AVATARSAVE) {
                window.ACCOUNT_AVATARSAVE = { face: emojiData.id, eye: 0, mouth: 0, acc: 0 };
                warn('Initialized a basic ACCOUNT_AVATARSAVE. This might be incorrect.');
            }
        }

        const parentUl = clickedLiElement.closest('ul');
        if (parentUl) {
            parentUl.querySelectorAll('li.active').forEach(activeLi => activeLi.classList.remove('active'));
        }
        clickedLiElement.classList.add('active');

        log('Fetching image data to prepare for save...');
        fetch(emojiData.url)
            .then(response => {
                if (!response.ok) {
                    throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);
                }
                return response.blob();
            })
            .then(blob => {
                const reader = new FileReader();
                reader.onloadend = function() {
                    let base64ImageData = reader.result;
                    if (blob.type && blob.type !== 'image/jpeg') {
                        log(`Image type is ${blob.type}. Using as is or converting for backend if needed.`);
                    } else {
                        base64ImageData = reader.result.replace(/^data:[^;]+;/, 'data:image/jpeg;');
                    }
                    log('Image data fetched and converted to base64. Triggering save.');
                    triggerSaveWithImageData(base64ImageData);
                };
                reader.onerror = function() {
                    error('FileReader failed to read image blob.');
                    alert('Error preparing image for save. Could not read image data.');
                };
                reader.readAsDataURL(blob);
            })
            .catch(err => {
                error('Failed to fetch or process custom emoji image for saving.', err);
                alert(`Error fetching image: ${err.message}`);
            });
    }

    function triggerSaveWithImageData(base64ImageData) {
        if (!window.LOGGEDIN) {
            alert("Error: You must be logged in to save your avatar.");
            warn("Save attempt aborted: User not logged in.");
            return;
        }
        if (!window.ACCOUNT_AVATARSAVE) {
            alert("Error: Avatar configuration is missing. Cannot save.");
            warn("Save attempt aborted: ACCOUNT_AVATARSAVE is null/undefined.");
            return;
        }

        log('Saving avatar with custom image data...');
        const console_log_save_status = (text) => log(`Save Status: ${text}`);
        console_log_save_status('Uploading...');

        $.ajax({
            url:  LOGGEDIN ? '/saveavatar' : '/uploadavatarimage',
            type: 'POST',
            data: {
                'avatarsave_builder': JSON.stringify(ACCOUNT_AVATARSAVE),
                'imagedata': base64ImageData,
                '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 => {
            console_log_save_status('Saving...');
            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}`);
                    return response.blob();
                })
                .then(() => {
                    console_log_save_status('Save OK!');
                    log('Avatar saved and cache confirmed! Redirecting to origin.');
                    location.href = new URL(location.href).origin;
                })
                .catch(err => {
                    error('Error confirming saved avatar from cache.', err);
                    console_log_save_status('Save attempt finished, but cache confirmation failed.');
                    alert(`Avatar might be saved, but confirmation failed: ${err.message}. Please check your profile.`);
                });
        }).fail((jqXHR, textStatus, errorThrown) => {
            error('$.ajax save request failed.', { status: textStatus, error: errorThrown, response: jqXHR.responseText });
            console_log_save_status('Upload Image');
            alert(`Failed to save avatar: ${errorThrown || textStatus}. Server response: ${jqXHR.responseText}`);
        });
    }

    function findTargetListAndTemplates() {
        const allElements = document.querySelectorAll('body *');
        let knownCategoryElement = null;
        for (let el of allElements) {
            if (el.childNodes.length === 1 && el.firstChild.nodeType === Node.TEXT_NODE && el.firstChild.textContent.trim().toUpperCase() === knownExistingCategoryText) {
                knownCategoryElement = el; break;
            }
            if (el.textContent.trim().toUpperCase() === knownExistingCategoryText && el.children.length < 2) {
                if (['LI', 'DIV', 'SPAN', 'H3', 'H4', 'P'].includes(el.tagName)) {
                    knownCategoryElement = el; break;
                }
            }
        }
        if (!knownCategoryElement) return null;
        log(`Found element containing "${knownExistingCategoryText}":`, knownCategoryElement);
        let targetListElement = knownCategoryElement.closest('ul');
        if (!targetListElement) {
            if (knownCategoryElement.tagName === 'LI' && knownCategoryElement.parentElement.tagName === 'UL') {
                targetListElement = knownCategoryElement.parentElement;
            } else {
                let parentCandidate = knownCategoryElement.parentElement; let searchDepth = 0;
                while (parentCandidate && searchDepth < 5 && !targetListElement) {
                    targetListElement = parentCandidate.querySelector('ul');
                    if(targetListElement && targetListElement.querySelector('li.category')) break;
                    targetListElement = null; parentCandidate = parentCandidate.parentElement; searchDepth++;
                }
            }
        }
        if (!targetListElement) { warn('Could not find the main <ul> list.'); return null; }
        log('Found target <ul> list element:', targetListElement);
        const templateCategoryLi = targetListElement.querySelector('li.category');
        const templateAssetLi = targetListElement.querySelector('li:not(.category)');
        if (!templateCategoryLi) { warn('Template for category header (li.category) not found.'); return null; }
        if (!templateAssetLi) { warn('Template for asset item (li:not(.category)) not found.'); return null; }
        log('Found templates: Category LI, Asset LI', { templateCategoryLi, templateAssetLi });
        return { targetListElement, templateCategoryLi, templateAssetLi };
    }

    function attemptToAddCustomEmojis() {
        if (customCategoryAdded) {
            if (observer) observer.disconnect();
            log('Custom category already added. Observer disconnected.');
            return true;
        }
        const result = findTargetListAndTemplates();
        if (!result) return false;
        const { targetListElement, templateCategoryLi, templateAssetLi } = result;
        if (targetListElement.querySelector(`li[${customCategoryMarkerAttribute}="true"]`)) {
            log('Custom category already exists in the list. Finalizing.');
            customCategoryAdded = true; if (observer) observer.disconnect(); return true;
        }
        log('Adding new category and emojis to the list.');
        const newHeader = cloneAndModifyCategoryHeaderLi(customCategoryName, templateCategoryLi);
        targetListElement.appendChild(newHeader);
        newCustomEmojis.forEach(emoji => {
            const newLi = cloneAndModifyAssetLi(emoji, templateAssetLi);
            targetListElement.appendChild(newLi);
        });
        log(`Successfully added "${customCategoryName}" category with ${newCustomEmojis.length} emojis.`);
        customCategoryAdded = true; if (observer) observer.disconnect();
        return true;
    }

    function startObserver() {
        if (observer) observer.disconnect();
        observer = new MutationObserver((mutationsList, obs) => {
            if (attemptToAddCustomEmojis()) {
                obs.disconnect(); observer = null;
            }
        });
        observer.observe(document.documentElement, { childList: true, subtree: true });
        log('MutationObserver started.');
        setTimeout(() => {
            if (!customCategoryAdded) {
                log("Initial attempt to add emojis after a longer delay."); // Increased delay for initial attempt
                attemptToAddCustomEmojis();
            }
        }, 2500); // Increased delay for initial attempt
    }

    function onPageReady(callback) {
        if (document.readyState === 'complete' || (document.readyState !== 'loading' && !document.documentElement.doScroll)) {
            setTimeout(callback, 750);
        } else {
            document.addEventListener('DOMContentLoaded', () => setTimeout(callback, 1000));
            window.addEventListener('load', () => setTimeout(callback, 2000));
        }
    }

    if (typeof $ === 'undefined') {
        error("jQuery is not loaded. Script will not function correctly. Ensure @require is working.");
        return;
    }

    $(() => {
        log("jQuery ready. Script starting main logic.");
        onPageReady(() => {
            if (window.location.pathname.includes('/avatar/builder')) {
                log('Avatar builder page detected. Initializing script.');
                if (typeof window.LOGGEDIN === 'undefined') {
                    warn('window.LOGGEDIN is undefined. Saving will likely fail or be treated as guest.');
                }
                if (typeof window.ACCOUNT_AVATARSAVE === 'undefined') {
                    warn('window.ACCOUNT_AVATARSAVE is undefined. This variable is needed for saving existing avatar parts.');
                }
                startObserver();
            } else {
                log('Not on avatar builder page. Script will not actively modify DOM.');
            }
        });
    });

})(window.jQuery);