Drawaria.online Custom Frame Avatars

Adds a custom "FRAMES" section to the Drawaria.online avatar builder. Each frame includes a specific emoji, and clicking saves the combined image directly to your profile.

// ==UserScript==
// @name         Drawaria.online Custom Frame Avatars
// @namespace    http://tampermonkey.net/
// @version      1.01
// @description  Adds a custom "FRAMES" section to the Drawaria.online avatar builder. Each frame includes a specific emoji, and clicking saves the combined image directly to your profile.
// @author       YouTubeDrawaria
// @match        *://drawaria.online/avatar/builder/*
// @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        GM_addStyle
// @license      MIT
// ==/UserScript==

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

    // --- CONFIGURATION ---
    // SVG Frames and Emojis data. Each entry defines a combined frame+emoji avatar.
    // 'frame_svg_part' and 'emoji_svg_part' are combined into one SVG for display and saving.
    const FRAMES_DATA = [
        {
            id: 'frame-gold-classic-smiley',
            name: 'Gold Smiley',
            frame_svg_part: '<rect x="2" y="2" width="96" height="96" fill="none" stroke="#FFD700" stroke-width="4" rx="8" ry="8"/>',
            emoji_svg_part: '<circle cx="50" cy="50" r="35" fill="#FFC107"/><circle cx="35" cy="40" r="5" fill="#333"/><circle cx="65" cy="40" r="5" fill="#333"/><path d="M30,65 Q50,75 70,65" fill="none" stroke="#333" stroke-width="4"/>'
        },
        {
            id: 'frame-neon-blue-pensive',
            name: 'Neon Pensive',
            frame_svg_part: '<rect x="5" y="5" width="90" height="90" fill="none" stroke="#00FFFF" stroke-width="2" stroke-dasharray="5,3"><animate attributeName="stroke-dashoffset" from="0" to="100" dur="5s" repeatCount="indefinite"/></rect>',
            emoji_svg_part: '<circle cx="50" cy="50" r="35" fill="#FFC107"/><circle cx="35" cy="40" r="5" fill="#333"/><circle cx="65" cy="40" r="5" fill="#333"/><path d="M30,65 Q50,55 70,65" fill="none" stroke="#333" stroke-width="4"/>' // Straight/pensive mouth
        },
        {
            id: 'frame-floral-cry',
            name: 'Floral Crying',
            frame_svg_part: '<defs><linearGradient id="g1" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:rgb(255,100,0);stop-opacity:1"/><stop offset="100%" style="stop-color:rgb(255,200,0);stop-opacity:1"/></linearGradient><path id="f1" d="M0,0 Q10,5 0,10 Q5,10 10,0 Z" fill="url(#g1)"/></defs><use href="#f1" x="5" y="5"/><use href="#f1" x="95" y="5" transform="rotate(90 95 5)"/><use href="#f1" x="95" y="95" transform="rotate(180 95 95)"/><use href="#f1" x="5" y="95" transform="rotate(270 5 95)"/><rect x="15" y="15" width="70" height="70" fill="none" stroke="#8B4513" stroke-width="1"/>',
            emoji_svg_part: '<circle cx="50" cy="50" r="35" fill="#FFC107"/><circle cx="35" cy="40" r="5" fill="#333"/><circle cx="65" cy="40" r="5" fill="#333"/><path d="M30,65 C35,55 65,55 70,65" fill="none" stroke="#333" stroke-width="4"/><circle cx="40" cy="55" r="3" fill="#00BFFF"/><circle cx="60" cy="55" r="3" fill="#00BFFF"/>' // Frown + tears
        },
        {
            id: 'frame-blue-circles-shocked',
            name: 'Blue Shocked',
            frame_svg_part: '<rect x="0" y="0" width="100" height="100" fill="none" stroke="#6A5ACD" stroke-width="2"/><circle cx="10" cy="10" r="8" fill="#FFC0CB"/><circle cx="90" cy="10" r="8" fill="#FFC0CB"/><circle cx="10" cy="90" r="8" fill="#FFC0CB"/><circle cx="90" cy="90" r="8" fill="#FFC0CB"/>',
            emoji_svg_part: '<circle cx="50" cy="50" r="35" fill="#FFC107"/><circle cx="35" cy="40" r="5" fill="#333"/><circle cx="65" cy="40" r="5" fill="#333"/><ellipse cx="50" cy="65" rx="15" ry="10" fill="#333"/>' // O-shaped mouth
        },
        {
            id: 'frame-art-deco-sunglasses',
            name: 'Art Deco Cool',
            frame_svg_part: '<rect x="5" y="5" width="90" height="90" fill="none" stroke="#A9A9A9" stroke-width="2"/><line x1="5" y1="20" x2="20" y2="5" stroke="#D3D3D3" stroke-width="1.5"/><line x1="80" y1="5" x2="95" y2="20" stroke="#D3D3D3" stroke-width="1.5"/><line x1="5" y1="80" x2="20" y2="95" stroke="#D3D3D3" stroke-width="1.5"/><line x1="80" y1="95" x2="95" y2="80" stroke="#D3D3D3" stroke-width="1.5"/><path d="M10 5 L10 15 L20 15" fill="none" stroke="#A9A9A9" stroke-width="3"/><path d="M90 5 L90 15 L80 15" fill="none" stroke="#A9A9A9" stroke-width="3"/><path d="M10 95 L10 85 L20 85" fill="none" stroke="#A9A9A9" stroke-width="3"/><path d="M90 95 L90 85 L80 85" fill="none" stroke="#A9A9A9" stroke-width="3"/>',
            emoji_svg_part: '<circle cx="50" cy="50" r="35" fill="#FFC107"/><rect x="25" y="35" width="50" height="15" fill="#333"/><path d="M30,65 Q50,75 70,65" fill="none" stroke="#333" stroke-width="4"/>' // Sunglasses + smile
        }
        // Add more custom SVG combined frames + emojis here!
        // Remember to give the outer SVG a viewBox="0 0 100 100"
    ];

    const CONSTANTS = {
        CUSTOM_CATEGORY_NAME: "CUSTOM FRAME AVATARS",
        KNOWN_EXISTING_CATEGORY_TEXT: "ACCESSORIES", // Anchor point in UI
        CUSTOM_CATEGORY_MARKER_ATTRIBUTE: "data-custom-category-scripted",
        CUSTOM_FRAME_MARKER_ATTRIBUTE: "data-custom-frame-scripted",
        STORAGE_KEY: 'drawaria_avatar_selected_frame_v1_5', // Unique key for localStorage
        AVATAR_SIZE: 128, // Standard avatar size in px for canvas rendering
        UPLOAD_URL: 'https://drawaria.online/uploadavatarimage'
    };

    // --- END CONFIGURATION ---

    let customCategoryAdded = false;
    let observer = null;
    let avatarCanvas = null; // Hidden canvas for rendering and uploading
    let avatarCanvasContext = null;
    let isCanvasReady = false; // New flag to track canvas readiness
    let activeFrameLayerItem = null; // This variable should ideally be managed more locally or passed, but keeping it for now to minimize changes.

    function log(message, data) { console.log(`[Drawaria Custom Avatars v${GM_info.script.version}] ${message}`, data || ''); }
    function warn(message, data) { console.warn(`[Drawaria Custom Avatars v${GM_info.script.version}] ${message}`, data || ''); }
    function error(message, data) { console.error(`[Drawaria Custom Avatars v${GM_info.script.version}] ${message}`, data || ''); }

    /**
     * Converts an SVG XML string to a Base64 data URL.
     * @param {string} svgString
     * @returns {string}
     */
    function svgToBase64DataUrl(svgString) {
        // Original implementation which is fine for basic SVG characters:
        const base64 = btoa(unescape(encodeURIComponent(svgString)));
        return `data:image/svg+xml;base64,${base64}`;
    }

    /**
     * Creates a complete SVG string from parts.
     * @param {string} framePartSvg
     * @param {string} emojiPartSvg
     * @returns {string} Full SVG XML
     */
    function createFullSvg(framePartSvg, emojiPartSvg) {
        return `<svg width="100%" height="100%" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">${framePartSvg}${emojiPartSvg}</svg>`;
    }

    /**
     * Clones and modifies a category header LI element.
     * @param {string} categoryName
     * @param {HTMLElement} templateCategoryLi
     * @returns {HTMLElement}
     */
    function cloneAndModifyCategoryHeaderLi(categoryName, templateCategoryLi) {
        if (!templateCategoryLi) {
            error('No templateCategoryLi provided for cloning! Creating basic LI.');
            const newHeader = document.createElement('li');
            newHeader.className = 'category';
            newHeader.textContent = categoryName;
            return newHeader;
        }
        const newHeader = $(templateCategoryLi).clone()[0];
        newHeader.textContent = categoryName;
        newHeader.id = '';
        newHeader.setAttribute(CONSTANTS.CUSTOM_CATEGORY_MARKER_ATTRIBUTE, 'true');
        return newHeader;
    }

    /**
     * Clones and modifies an asset LI element for a custom frame.
     * @param {Object} frameData - {id, name, frame_svg_part, emoji_svg_part}
     * @param {HTMLElement} templateAssetLi
     * @returns {HTMLElement}
     */
    function cloneAndModifyFrameLi(frameData, templateAssetLi) {
        const fullSvg = createFullSvg(frameData.frame_svg_part, frameData.emoji_svg_part);
        const svgDataUrl = svgToBase64DataUrl(fullSvg);

        const newItem = $(templateAssetLi).clone()[0];
        newItem.title = frameData.name;

        const img = newItem.querySelector('img');
        if (img) {
            img.src = svgDataUrl;
            img.alt = frameData.name;
            // Ensure style properties are set, prevent browser defaults
            if (!img.style.objectFit) img.style.objectFit = 'contain';
            if (img.draggable === undefined) img.draggable = false;
            if (img.srcset) img.srcset = ''; // Clear srcset to prevent unwanted loading
        } else {
            warn('Template asset li did not contain an img. Creating one for frame.');
            const newImg = document.createElement('img');
            newImg.src = svgDataUrl;
            newImg.alt = frameData.name;
            newImg.className = 'asset';
            newImg.draggable = false;
            newImg.style.objectFit = 'contain';
            newItem.innerHTML = ''; // Clear existing content if any
            newItem.appendChild(newImg);
        }

        newItem.id = '';
        newItem.setAttribute(CONSTANTS.CUSTOM_FRAME_MARKER_ATTRIBUTE, 'true');
        newItem.setAttribute('data-frame-id', frameData.id);
        newItem.onclick = null; // Clear existing onclick to prevent conflicts

        $(newItem).on('click', (event) => {
            event.stopPropagation();
            event.preventDefault();
            handleCustomFrameClick(frameData, newItem);
        });

        return newItem;
    }

    /**
     * Creates and returns the "Clear Avatar" LI element.
     * @param {HTMLElement} templateAssetLi
     * @returns {HTMLElement}
     */
    function createClearAvatarLi(templateAssetLi) {
        const newItem = $(templateAssetLi).clone()[0];
        newItem.title = 'Clear Custom Avatar';
        newItem.id = ''; // Clear ID from template
        newItem.setAttribute(CONSTANTS.CUSTOM_FRAME_MARKER_ATTRIBUTE, 'true');
        newItem.setAttribute('data-frame-id', 'no-frame'); // Unique ID for clear option

        // Remove image if present and add text content
        const img = newItem.querySelector('img');
        if (img) img.remove();

        const itemNameSpan = document.createElement('span');
        itemNameSpan.className = 'item-name'; // Use existing class for text styling
        itemNameSpan.textContent = 'Clear Avatar';

        // Clear existing content and append new elements
        $(newItem).empty().append(itemNameSpan);

        // Add specific classes for styling
        $(newItem).addClass('no-frame-option').removeClass('frame-option');

        // Attach dedicated click handler
        $(newItem).off('click').on('click', removeFrame);

        return newItem;
    }

    /**
     * Initializes the hidden canvas for avatar rendering and upload.
     */
    function setupAvatarCanvas() {
        if (!avatarCanvas) {
            avatarCanvas = document.createElement('canvas');
            avatarCanvas.width = CONSTANTS.AVATAR_SIZE;
            avatarCanvas.height = CONSTANTS.AVATAR_SIZE;
            avatarCanvasContext = avatarCanvas.getContext('2d');
            avatarCanvas.style.display = 'none'; // Keep hidden
            document.body.appendChild(avatarCanvas);
            isCanvasReady = true;
            log('Hidden avatar canvas initialized.');
        } else {
            // Ensure context is still valid if canvas was previously created
            if (!avatarCanvasContext) {
                 avatarCanvasContext = avatarCanvas.getContext('2d');
                 isCanvasReady = true;
                 log('Re-initialized avatar canvas context.');
            }
        }
    }

    /**
     * Draws the combined SVG onto the canvas and triggers the upload.
     * @param {Object} frame - The selected frame object.
     * @param {boolean} isInitialLoad - True if called during page load, false if by user click.
     */
    async function renderAndUploadAvatar(frame, isInitialLoad = false) {
        if (!isCanvasReady || !avatarCanvas || !avatarCanvasContext) {
            error('Avatar canvas not initialized or ready for rendering. Aborting upload.', {isCanvasReady, avatarCanvas, avatarCanvasContext});
            alert('Cannot save avatar: Canvas not ready. Please refresh the page and try again.');
            return;
        }

        log(`Rendering ${frame.name} to canvas for upload...`);
        const fullSvg = createFullSvg(frame.frame_svg_part, frame.emoji_svg_part);
        const svgDataUrl = svgToBase64DataUrl(fullSvg);

        const img = new Image();
        img.src = svgDataUrl;

        img.onload = async () => {
            avatarCanvasContext.clearRect(0, 0, CONSTANTS.AVATAR_SIZE, CONSTANTS.AVATAR_SIZE);
            avatarCanvasContext.drawImage(img, 0, 0, CONSTANTS.AVATAR_SIZE, CONSTANTS.AVATAR_SIZE);

            try {
                // Get Base64 data for upload. JPEG is generally preferred for avatars.
                const imageDataUrl = avatarCanvas.toDataURL('image/jpeg', 0.9); // Quality 0.9 for balance

                // Update the preview image in the avatar builder UI immediately
                const previewImg = document.querySelector('.Panel.preview img.AvatarImage, #selfavatarimage');
                if (previewImg) {
                    previewImg.src = imageDataUrl;
                    log('Avatar preview updated locally with new frame.');
                }

                // Only trigger save to backend if it's a user action, not initial load
                if (!isInitialLoad) {
                    await uploadAvatarImage(imageDataUrl);

                    // Update active class in the right panel and localStorage only after successful upload
                    $('.item-option[data-frame-id]').removeClass('active');
                    $(`.item-option[data-frame-id="${frame.id}"]`).addClass('active');
                    $('.item-option.no-frame-option').removeClass('active');

                    localStorage.setItem(CONSTANTS.STORAGE_KEY, JSON.stringify({ id: frame.id, name: frame.name }));
                } else {
                    // On initial load, just update UI and localStorage based on the loaded frame
                    $('.item-option[data-frame-id]').removeClass('active');
                    $(`.item-option[data-frame-id="${frame.id}"]`).addClass('active');
                    $('.item-option.no-frame-option').removeClass('active');
                    updateActiveFrameLayer(frame.name, frame.id);
                    log('Initial load: Avatar preview and UI updated.');
                }

            } catch (err) {
                error('Failed to render, get image data, or upload avatar:', err);
                alert(`Failed to apply & save avatar: ${err.message}. Check console for details.`);
            }
        };
        img.onerror = (e) => {
            error('Failed to load SVG image onto canvas.', e);
            alert('Error rendering avatar. SVG might be malformed or invalid.');
        };
    }

    /**
     * Uploads the Base64 image data to Drawaria's server.
     * This mimics the upload logic from the "Drawaria Avatar Copy Players" script.
     * @param {string} base64ImageData
     */
    async function uploadAvatarImage(base64ImageData) {
        log('Attempting to upload avatar image to Drawaria server...');

        try {
            const response = await fetch(CONSTANTS.UPLOAD_URL, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
                },
                body: 'imagedata=' + encodeURIComponent(base64ImageData)
            });

            if (response.ok && response.status === 200) {
                const responseText = await response.text();
                if (responseText && responseText.trim()) {
                    log('Avatar upload successful. Server responded:', responseText);
                    alert('Avatar saved successfully! Page will reload to confirm.');
                    location.reload(); // Reload to see the permanent change
                } else {
                    throw new Error('Server returned empty or invalid response after upload.');
                }
            } else {
                throw new Error(`Server responded with status: ${response.status} ${response.statusText}`);
            }
        } catch (error) {
            error('Avatar upload failed:', error);
            throw new Error(`Upload failed: ${error.message}. Please check console (F12) for details.`);
        }
    }

    /**
     * Handles when a custom frame is clicked in the right panel.
     * @param {Object} frameData - The frame object.
     * @param {HTMLElement} clickedLiElement - The LI element that was clicked.
     */
    function handleCustomFrameClick(frameData, clickedLiElement) {
        log(`Clicked frame: ${frameData.name}. Preparing to save.`);
        renderAndUploadAvatar(frameData, false); // Not initial load

        // Visual feedback immediately (active class will be managed after upload success)
        $(clickedLiElement).siblings().removeClass('active');
        $(clickedLiElement).addClass('active');
    }

    /**
     * Removes the current frame from the avatar by uploading a blank/default image.
     */
    async function removeFrame() {
        if (!isCanvasReady || !avatarCanvas || !avatarCanvasContext) {
            warn('Canvas not ready for clearing avatar. Aborting.');
            alert('Cannot clear avatar: Canvas not ready. Please refresh the page and try again.');
            return;
        }

        log('Removing frame. Uploading a transparent default avatar...');
        avatarCanvasContext.clearRect(0, 0, CONSTANTS.AVATAR_SIZE, CONSTANTS.AVATAR_SIZE); // Clear canvas to transparent

        // Use PNG for transparency when clearing
        const imageDataUrl = avatarCanvas.toDataURL('image/png');

        // Update preview
        const previewImg = document.querySelector('.Panel.preview img.AvatarImage, #selfavatarimage');
        if (previewImg) {
            previewImg.src = imageDataUrl;
        }

        try {
            await uploadAvatarImage(imageDataUrl);
            if (activeFrameLayerItem) {
                $(activeFrameLayerItem).remove();
                activeFrameLayerItem = null;
            }
            $('.item-option[data-frame-id]').removeClass('active');
            $('.item-option.no-frame-option').addClass('active');
            localStorage.removeItem(CONSTANTS.STORAGE_KEY);
            log('Frame removed and transparent avatar uploaded successfully.');
        } catch (err) {
            error('Failed to upload transparent avatar for frame removal:', err);
            alert(`Error removing frame: ${err.message}.`);
        }
    }

    /**
     * Updates or creates the "Frame" item in the left panel's active layers list.
     * @param {string} frameName
     * @param {string} frameId
     */
    function updateActiveFrameLayer(frameName, frameId) {
        const playerlist = document.getElementById('playerlist');
        if (!playerlist) {
            warn('Left panel playerlist not found for active frame layer.');
            return;
        }

        activeFrameLayerItem = playerlist.querySelector(`li[${CONSTANTS.CUSTOM_FRAME_MARKER_ATTRIBUTE}="true"]`);

        if (frameId === 'no-frame') { // If "Clear Avatar" is selected
            if (activeFrameLayerItem) {
                $(activeFrameLayerItem).remove();
                activeFrameLayerItem = null;
            }
            return;
        }

        if (!activeFrameLayerItem) {
            activeFrameLayerItem = document.createElement('li');
            activeFrameLayerItem.className = 'layer-item custom-frame-layer';
            activeFrameLayerItem.setAttribute(CONSTANTS.CUSTOM_FRAME_MARKER_ATTRIBUTE, 'true');
            activeFrameLayerItem.setAttribute('data-frame-id', frameId);

            const layerNameSpan = document.createElement('span');
            layerNameSpan.className = 'layer-name';
            layerNameSpan.textContent = `Custom: ${frameName}`; // Indicate it's a custom saved avatar

            const layerControlsDiv = document.createElement('div');
            layerControlsDiv.className = 'layer-controls';

            const removeButton = document.createElement('button');
            removeButton.className = 'btn-remove-part';
            removeButton.title = 'Remove Custom Avatar';
            removeButton.textContent = 'x';
            $(removeButton).on('click', removeFrame);

            // Move Up/Down buttons (disabled)
            const moveUpButton = document.createElement('button');
            moveUpButton.className = 'btn-move-up';
            moveUpButton.title = 'Move Up (disabled for custom avatar)';
            moveUpButton.textContent = '▲';
            moveUpButton.disabled = true;

            const moveDownButton = document.createElement('button');
            moveDownButton.className = 'btn-move-down';
            moveDownButton.title = 'Move Down (disabled for custom avatar)';
            moveDownButton.textContent = '▼';
            moveDownButton.disabled = true;

            layerControlsDiv.append(removeButton, moveUpButton, moveDownButton);
            activeFrameLayerItem.append(layerNameSpan, layerControlsDiv);

            // Prepend to playerlist
            $(playerlist).prepend(activeFrameLayerItem);
            log('Created new active custom avatar layer in left panel.');
        } else {
            activeFrameLayerItem.querySelector('.layer-name').textContent = `Custom: ${frameName}`;
            activeFrameLayerItem.setAttribute('data-frame-id', frameId);
            log('Updated existing active custom avatar layer in left panel.');
        }
    }

    /**
     * Finds the target UL list for categories and template LI elements.
     * This logic is adapted from the provided Custom Emojis script.
     * @returns {Object|null}
     */
    function findTargetListAndTemplates() {
        let knownCategoryElement = null;
        const categoryTitles = document.querySelectorAll('.category');
        for (let el of categoryTitles) {
            if (el.textContent.trim().toUpperCase() === CONSTANTS.KNOWN_EXISTING_CATEGORY_TEXT) {
                knownCategoryElement = el;
                break;
            }
        }

        if (!knownCategoryElement) {
            warn(`Could not find the anchor category "${CONSTANTS.KNOWN_EXISTING_CATEGORY_TEXT}".`);
            return null;
        }
        log(`Found anchor category "${CONSTANTS.KNOWN_EXISTING_CATEGORY_TEXT}":`, knownCategoryElement);

        const targetListElement = $(knownCategoryElement).closest('ul')[0];
        if (!targetListElement) {
            warn('Could not find the main <ul> list containing the anchor category.');
            return null;
        }
        log('Found target <ul> list element:', targetListElement);

        const templateCategoryLi = targetListElement.querySelector('li.category');
        // Find a representative asset item, ignoring other categories or special items
        let templateAssetLi = targetListElement.querySelector(`li:not(.category):not(.title):not([${CONSTANTS.CUSTOM_CATEGORY_MARKER_ATTRIBUTE}]):not([${CONSTANTS.CUSTOM_FRAME_MARKER_ATTRIBUTE}])`);


        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. Trying fallback.');
            // Fallback for asset LI if the stricter selector fails
            const allLis = targetListElement.querySelectorAll('li');
            for (let li of allLis) {
                if (!li.classList.contains('category') && li.querySelector('img.asset')) {
                    log('Using fallback asset LI template.');
                    templateAssetLi = li;
                    break;
                }
            }
            if (!templateAssetLi) {
                 warn('No suitable asset LI template found, even with fallback.');
                 return null;
            }
        }

        log('Found templates: Category LI, Asset LI', { templateCategoryLi, templateAssetLi });
        return { targetListElement, knownCategoryElement, templateCategoryLi, templateAssetLi };
    }

    /**
     * Attempts to add the custom "CUSTOM AVATARS" category and its items.
     * @returns {boolean} - True if added successfully, false otherwise.
     */
    function attemptToAddCustomFrames() {
        if (customCategoryAdded) {
            // Already added, ensure canvas is ready and load saved state if not already done
            if (!isCanvasReady) {
                 setupAvatarCanvas();
                 if (isCanvasReady) loadSavedFrame(); // Only load if canvas is ready now
            }
            if (observer) observer.disconnect();
            log('Custom category already added. Observer disconnected.');
            return true;
        }

        const result = findTargetListAndTemplates();
        if (!result) return false;

        const { targetListElement, knownCategoryElement, templateCategoryLi, templateAssetLi } = result;

        if (targetListElement.querySelector(`li[${CONSTANTS.CUSTOM_CATEGORY_MARKER_ATTRIBUTE}="true"]`)) {
            log('Custom category already exists in the list. Finalizing.');
            customCategoryAdded = true;
            if (observer) observer.disconnect();
            // Ensure canvas is ready before attempting to load saved frame
            setupAvatarCanvas();
            if (isCanvasReady) loadSavedFrame();
            return true;
        }

        log('Adding new CUSTOM AVATARS category and items to the right panel.');

        const newHeader = cloneAndModifyCategoryHeaderLi(CONSTANTS.CUSTOM_CATEGORY_NAME, templateCategoryLi);
        $(knownCategoryElement).after(newHeader);

        let currentElement = newHeader; // Anchor for inserting items
        FRAMES_DATA.forEach(frame => {
            const newLi = cloneAndModifyFrameLi(frame, templateAssetLi);
            $(currentElement).after(newLi);
            currentElement = newLi;
        });

        // Add the "Clear Avatar" option
        const noFrameLi = createClearAvatarLi(templateAssetLi);
        $(currentElement).after(noFrameLi);


        log(`Successfully added "${CONSTANTS.CUSTOM_CATEGORY_NAME}" category with ${FRAMES_DATA.length} items.`);
        customCategoryAdded = true;
        if (observer) observer.disconnect();

        // Ensure canvas is ready before loading saved frame
        setupAvatarCanvas();
        if (isCanvasReady) loadSavedFrame();

        return true;
    }

    /**
     * Loads the previously selected frame from localStorage and applies it.
     */
    function loadSavedFrame() {
        if (!isCanvasReady) {
            warn('Canvas not ready for loading saved frame. Deferring loadSavedFrame.');
            return;
        }

        const savedFrameJson = localStorage.getItem(CONSTANTS.STORAGE_KEY);
        if (savedFrameJson) {
            try {
                const savedFrame = JSON.parse(savedFrameJson);
                if (savedFrame.id === 'no-frame') {
                    log("Saved avatar is 'Clear Avatar'. Activating it.");
                    $('.item-option[data-frame-id]').removeClass('active');
                    $('.item-option.no-frame-option').addClass('active');
                    updateActiveFrameLayer('Clear Avatar', 'no-frame'); // Update left panel for 'no-frame'
                    // No need to render anything if it's 'no-frame', current avatar is already clear
                } else {
                    const frameToApply = FRAMES_DATA.find(f => f.id === savedFrame.id);
                    if (frameToApply) {
                        // Only update preview and left panel, don't re-upload on page load
                        renderAndUploadAvatar(frameToApply, true); // True for isInitialLoad
                        log(`Loaded saved avatar: ${frameToApply.name}`);
                    } else {
                        // If saved ID is not found in FRAMES_DATA, clear it.
                        warn(`Saved avatar ID "${savedFrame.id}" not found in current FRAMES_DATA. Clearing saved state.`);
                        localStorage.removeItem(CONSTANTS.STORAGE_KEY);
                        // Default to 'Clear Avatar' visually and in left panel.
                        $('.item-option[data-frame-id]').removeClass('active');
                        $('.item-option.no-frame-option').addClass('active');
                        updateActiveFrameLayer('Clear Avatar', 'no-frame');
                    }
                }
            } catch (e) {
                error("Failed to parse or load saved avatar data. Clearing localStorage.", e);
                localStorage.removeItem(CONSTANTS.STORAGE_KEY);
                $('.item-option[data-frame-id]').removeClass('active');
                $('.item-option.no-frame-option').addClass('active');
            }
        } else {
            log("No saved avatar found in localStorage. Setting 'Clear Avatar' as active by default.");
            $('.item-option[data-frame-id]').removeClass('active');
            $('.item-option.no-frame-option').addClass('active');
            updateActiveFrameLayer('Clear Avatar', 'no-frame'); // Show "Clear Avatar" in left panel
        }
    }


    /**
     * Injects CSS styles into the document.
     * Uses GM_addStyle (Tampermonkey/Greasemonkey) or falls back to appending a style tag.
     * @param {string} css
     */
    function injectCSS(css) {
        if (typeof GM_addStyle !== 'undefined') {
            GM_addStyle(css);
        } else {
            const style = document.createElement('style');
            style.textContent = css;
            document.head.appendChild(style);
        }
    }

    function startObserver() {
        if (observer) observer.disconnect();
        observer = new MutationObserver((mutationsList, obs) => {
            // Check for avatar container and initialize canvas
            const avatarContainer = document.getElementById('avatarcontainer');
            if (avatarContainer && !isCanvasReady) { // Ensure canvas is initialized only once and when container is present
                 setupAvatarCanvas();
                 // If canvas is now ready, and categories were added, load saved frame
                 if (isCanvasReady && customCategoryAdded) {
                     loadSavedFrame();
                 }
            }

            // Attempt to add custom frames
            if (attemptToAddCustomFrames()) {
                // If categories are added and observer has done its job, disconnect
                obs.disconnect();
                observer = null;
            }
        });
        // Observe the entire document for changes (childList and subtree are important for dynamic loading)
        observer.observe(document.documentElement, { childList: true, subtree: true });
        log('MutationObserver started.');

        // Initial check in case elements are already present very early
        setTimeout(() => {
            if (!customCategoryAdded) {
                log("Initial attempt to add frames after a short delay (post-DOMReady).");
                attemptToAddCustomFrames();
            }
            if (!isCanvasReady) {
                // Fallback for canvas initialization if observer somehow missed it or it takes longer
                const avatarContainer = document.getElementById('avatarcontainer');
                if (avatarContainer) {
                    setupAvatarCanvas();
                    if (isCanvasReady && customCategoryAdded) {
                        loadSavedFrame();
                    }
                }
            }
        }, 750);
    }

    // Main execution block when jQuery is ready
    $(() => {
        log("jQuery ready. Script starting main logic.");

        injectCSS(`
            /* --- Right Panel Category & Item Styling --- */

            /* Basic styling for the custom category header */
            li[${CONSTANTS.CUSTOM_CATEGORY_MARKER_ATTRIBUTE}="true"] {
                font-weight: bold;
                padding: 0.5em 1em;
                margin-top: 1em;
                border-bottom: 1px solid #b0b5b9;
                background-color: #e6f7ff; /* Lighter blue for custom category */
                border-radius: 5px;
                text-align: center;
                color: #333;
                box-shadow: 0 2px 5px rgba(0,0,0,0.1);
            }

            /* Styling for custom frame items */
            li[${CONSTANTS.CUSTOM_FRAME_MARKER_ATTRIBUTE}="true"].item-option {
                width: 60px; /* Consistent size */
                height: 60px;
                border-radius: 50%;
                background-color: #ffffff;
                border: 1px solid #dee2e6;
                cursor: pointer;
                display: flex;
                flex-direction: column; /* To stack image and name if name is shown */
                align-items: center;
                justify-content: center;
                overflow: hidden; /* Hide overflow for circular image */
                position: relative; /* For item-name positioning */
                box-sizing: border-box;
                transition: all 0.2s ease;
                margin: 5px; /* Add some margin for spacing */
                flex-shrink: 0; /* Prevent shrinking in flex container */
            }
            li[${CONSTANTS.CUSTOM_FRAME_MARKER_ATTRIBUTE}="true"].item-option:hover {
                border-color: #007bff;
                box-shadow: 0 0 5px rgba(0, 123, 255, 0.5);
                background-color: #e9f0f7;
            }
            li[${CONSTANTS.CUSTOM_FRAME_MARKER_ATTRIBUTE}="true"].item-option.active {
                border-color: #007bff !important;
                box-shadow: 0 0 8px rgba(0, 123, 255, 0.8) !important;
                background-color: #e9f0f7;
            }

            /* Image inside the custom frame item */
            li[${CONSTANTS.CUSTOM_FRAME_MARKER_ATTRIBUTE}="true"].item-option img.asset {
                width: 100%; /* Fill the circular item */
                height: 100%;
                object-fit: contain; /* Ensure SVG fits */
                border-radius: 50%; /* Make image circular */
            }

            /* Item name (tooltip-like) */
            li[${CONSTANTS.CUSTOM_FRAME_MARKER_ATTRIBUTE}="true"].item-option .item-name {
                font-size: 0.6em;
                text-align: center;
                word-break: break-word;
                position: absolute;
                bottom: 0;
                left: 0;
                right: 0;
                background: rgba(0, 0, 0, 0.7);
                color: white;
                padding: 2px 0;
                display: none; /* Hidden by default */
                border-bottom-left-radius: 50%; /* Match parent border-radius */
                border-bottom-right-radius: 50%;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
                box-sizing: border-box; /* Include padding/border in width */
            }
            li[${CONSTANTS.CUSTOM_FRAME_MARKER_ATTRIBUTE}="true"].item-option:hover .item-name {
                display: block; /* Show on hover */
            }

            /* Specific styles for the "Clear Avatar" option */
            li[data-frame-id="no-frame"].no-frame-option {
                background-color: #f0f0f0;
                color: #555;
                font-weight: bold;
                font-size: 0.7em;
                justify-content: center;
                align-items: center;
                padding: 5px; /* Add some padding for text */
            }
            li[data-frame-id="no-frame"].no-frame-option img {
                display: none; /* Ensure no image is shown */
            }
            li[data-frame-id="no-frame"].no-frame-option .item-name {
                position: static; /* Text is part of flow, not absolutely positioned */
                background: none;
                color: inherit;
                padding: 0;
                white-space: normal;
                overflow: visible;
                text-overflow: clip;
                font-size: 0.7em; /* Inherit font size from parent */
                display: block; /* Always visible for clear button */
            }
            li[data-frame-id="no-frame"].no-frame-option:hover {
                 border-color: #dc3545; /* Red for clear/remove */
                 background-color: #f8d7da;
            }
            li[data-frame-id="no-frame"].no-frame-option.active {
                border-color: #dc3545 !important;
                box-shadow: 0 0 8px rgba(220, 53, 69, 0.8) !important;
            }

            /* --- Left Panel Active Layer Styling --- */
            .layer-item.custom-frame-layer {
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 0.5em;
                margin-bottom: 0.3em;
                background-color: #f0f8ff;
                border-radius: 7px;
                font-size: 0.8em;
                transition: background-color 0.2s ease;
            }
            .layer-item.custom-frame-layer:hover {
                background-color: #fbffad;
            }
            .layer-item.custom-frame-layer .layer-name {
                flex-grow: 1;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
                padding-right: 5px;
            }
            .layer-item.custom-frame-layer .layer-controls {
                display: flex;
                gap: 5px;
            }
            .layer-item.custom-frame-layer .layer-controls button {
                background: #ff5050; /* Red for remove button */
                color: white;
                border: none;
                border-radius: 3px;
                cursor: pointer;
                padding: 3px 6px;
                font-size: 0.7em;
                line-height: 1;
                min-width: 20px;
                text-align: center;
            }
            .layer-item.custom-frame-layer .layer-controls button:hover {
                background: #cc0000;
            }

            .layer-item.custom-frame-layer .layer-controls .btn-move-up,
            .layer-item.custom-frame-layer .layer-controls .btn-move-down {
                background: #cccccc; /* Grey for disabled buttons */
                cursor: not-allowed;
            }
        `);


        // Use onPageReady or directly call startObserver depending on how DOM is loaded.
        // Given `MutationObserver` is used, it should reliably pick up elements.
        if (window.location.pathname.includes('/avatar/builder')) {
            log('Avatar builder page detected. Initializing script.');
            startObserver();
        } else {
            log('Not on avatar builder page. Script will not actively modify DOM.');
        }
    });

})(window.jQuery);