Disconnect Tools

Advanced Tools For VRChat Website

当前为 2025-04-01 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Disconnect Tools
// @namespace    http://tampermonkey.net/
// @license      MIT
// @version      2025-04-01v0.7.9
// @description  Advanced Tools For VRChat Website
// @author       Disconnect3301
// @match        https://*.vrchat.com/*
// @grant        GM_addStyle
// @grant        GM_cookie
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @run-at       document-idle
// ==/UserScript==

'use strict';

let savedAvatars = GM_getValue('savedAvatars', []) || [];
let isCustomFavoritesMenuOpen = false;

let homeContentElement = null;
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;

const addMetod = {
    current : 'current',
    ID : 'ID'
}
let authorName = null;
let authorId = null;
let created_at = null;
let description = null;
let avatarID = null;
let imageUrl = null;
let avatarName = null;
let releaseStatus = null;
let updated_at = null;
let version = null;

let userId = null;
let authCookie = null;
// let displayName = null;

GM_addStyle(`
	/* Контейнер уведомлений */
    #notification-container {
        position: fixed;
        top: 20px;
        right: 20px;
        z-index: 1050;
        display: flex;
        flex-direction: column-reverse;
        align-items: flex-end;
        gap: 10px;
        pointer-events: none;
		transition: all .15s ease-in-out;
    }

    /* Базовые стили уведомления */
    .notification {
        position: relative;
        min-width: 280px;
        max-width: 400px;
		width: auto;
        padding: 18px 35px 18px 20px;
        border-radius: 12px;
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
        background: linear-gradient(135deg, #2d2d2d, #242424);
        color: #e0e0e0;
        font-family: 'Segoe UI', system-ui, sans-serif;
        font-size: 14px;
        line-height: 1.5;
        cursor: pointer;
        transform: translateY(-20px);
        opacity: 0;
        transition: all 0.3s cubic-bezier(0.68, -0.55, 0.27, 1.55);
        pointer-events: auto;
        overflow: hidden;
        border: 1px solid transparent;
        display: flex;
        flex-direction: row;
        align-items: center;
        gap: 10px;
    }

    /* Анимация появления */
    .notification.show {
        transform: translateY(0);
        opacity: 1;
    }

    /* Анимация исчезновения */
    .notification.hide {
        height: 0 !important; /* Схлопываем высоту */
        padding: 0 !important; /* Убираем отступы */
        margin: 0 !important; /* Убираем внешние отступы */
        opacity: 0;
        transform: translateY(-20px);
        border-radius: 0;
        box-shadow: none;
        pointer-events: none;
    }

    /* Типы уведомлений */
    .notification.info {
        border-left: 5px solid #2196F3;
        background-color: #2196F31A;
    }

    .notification.success {
        border-left: 5px solid #4CAF50;
        background-color: #4CAF501A;
    }

    .notification.warning {
        border-left: 5px solid #FFA726;
        background-color: #FFA7261A;
    }

    .notification.error {
        border-left: 5px solid #F44336;
        background-color: #F443361A;
    }

    /* Иконки статуса */
    .status-icon {
        font-size: 20px;
        opacity: 0.9;
    }

    /* Сообщение */
    .message {
        flex-grow: 1;
        overflow: hidden;
        text-overflow: ellipsis;
        word-wrap: break-word;
    }

    /* Прогресс-бар */
    .progress-bar {
        position: absolute;
        bottom: 0;
        left: 0;
        width: 100%; /* Занимает всю ширину */
        height: 3px; /* Фиксированная высота */
        background: linear-gradient(90deg, #ddd, #bbb);
        transform-origin: left;
        transform: scaleX(1); /* Начальное состояние */
        transition: transform 0.1s linear;
    }

    /* Ховер-эффекты */
    .notification:hover {
        transform: scale(1.02) translateY(-2px);
        box-shadow: 0 8px 18px rgba(0, 0, 0, 0.2);
    }

    /* Респонсив */
    @media (max-width: 480px) {
        .notification {
            width: 90vw;
            min-width: unset;
        }
    }
`);

//Main Buttons
GM_addStyle(`
	.btn-custom {
		--bs-btn-font-family: ;
		--bs-btn-font-weight: normal;
		--bs-btn-line-height: 1.25;
		--bs-btn-color: var(--bs-body-color);
		--bs-btn-bg: transparent;
		--bs-btn-border-width: 1px;
		--bs-btn-border-color: transparent;
		--bs-btn-hover-border-color: transparent;
		--bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(10, 10, 13, 0.075);
		--bs-btn-disabled-opacity: 0.65;
		--bs-btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);
		display: inline-block;
		padding: var(--bs-btn-padding-y) var(--bs-btn-padding-x);
		font-family: var(--bs-btn-font-family);
		font-size: var(--bs-btn-font-size);
		font-weight: var(--bs-btn-font-weight);
		line-height: var(--bs-btn-line-height);
		color: var(--bs-btn-color);
		text-align: center;
		vertical-align: middle;
		cursor: pointer;
		user-select: none;
		border: var(--bs-btn-border-width) solid var(--bs-btn-border-color);
		border-radius: var(--bs-btn-border-radius);
		background-color: var(--bs-btn-bg);
		background-image: var(--bs-gradient);
		transition: all .15s ease-in-out;
		--bs-btn-padding-y: 0.5rem;
		--bs-btn-padding-x: 1rem;
		--bs-btn-font-size: 1.25rem;
		--bs-btn-border-radius: 0.3rem;
		position: relative;
		flex: 1 1 auto;
		width: 100%;
		margin-top: calc(1px* -1);
		border-top-left-radius: 0;
		border-top-right-radius: 0;
		border-bottom-right-radius: 0;
		border-bottom-left-radius: 0;
	}
	.btn-custom:active {
		color: var(--bs-btn-active-color);
		background-color: var(--bs-btn-active-bg);
		background-image: none;
		border-color: var(--bs-btn-active-border-color);
		z-index: 1;
	}
	.btn-custom:focus-visible {
		color: var(--bs-btn-hover-color);
		background-color: var(--bs-btn-hover-bg);
		background-image: var(--bs-gradient);
		border-color: var(--bs-btn-hover-border-color);
		outline: 0;
		box-shadow: var(--bs-btn-focus-box-shadow);
	}
	.btn-custom:hover {
		color: var(--bs-btn-hover-color);
		text-decoration: none;
		background-color: var(--bs-btn-hover-bg);
	}
	.css-yjay0l-custom {
		background: rgb(7, 36, 43);
		border: 2px solid rgb(5, 60, 72);
		color: rgb(106, 227, 249);
		display: flex;
		flex-direction: row;
		place-content: start space-between;
		-webkit-box-align: center;
		align-items: center;
		-webkit-box-pack: justify;
		height: 45px;
		border-radius: 8px !important;
		box-shadow: none !important;
		padding: 0px 10px !important;
	}
	.css-yjay0l-custom:hover {
		background: rgb(7, 52, 63);
		border-color: rgb(8, 108, 132);
		transform: scale(1.1);
	}
	.css-yjay0l-custom:active {
		color: var(--bs-btn-active-color);
		background-color: var(--bs-btn-active-bg);
		background-image: none;
		border-color: var(--bs-btn-active-border-color);
		z-index: 1;
	}

	.css-1vrq36y-custom {
		border: 2px solid rgb(6, 75, 92);
		border-radius: 4px;
		background: rgb(6, 75, 92);
		color: rgb(106, 227, 249);
		padding: 5px;
		box-sizing: border-box;
		flex: 1 1 0%;
		outline: none !important;
	}
	.css-1vrq36y-custom:hover {
		border-color: rgb(8, 108, 132);
	}

    .avatarCardToggles {
		border: 2px solid rgb(6, 75, 92);
		border-radius: 4px;
		background: rgb(6, 75, 92);
		color: rgb(106, 227, 249);
		padding: 5px;
		box-sizing: border-box;
		outline: none !important;
        transition: all .15s ease-in-out;
	}
    .avatarCardToggles:hover {
		border-color: rgb(8, 108, 132);
	}
    .avatarCardToggles.Remove {
		border: 2px solid rgb(6, 75, 92);
		border-radius: 4px;
		background: rgba(255, 0, 0, 0.1); !important
		color: rgb(106, 227, 249);
		padding: 5px;
		box-sizing: border-box;
		outline: none !important;
        font-weight: bold;
	}
    .avatarCardToggles.Remove:hover {
        background: rgb(5, 25, 29) !important;
    }
`);

GM_addStyle(`
	.css-14ngdq4-custom {
		display: flex;
		background: rgb(54, 54, 54);
		border-radius: 4px;
		padding: 0.25rem 0.75rem;
		-webkit-box-align: center;
		align-items: center;
		font-weight: bold;
		font-size: 1.1rem;
		color: rgb(230, 230, 230);
	}
	.css-zjik7-custom {
        display: flex;

        // after my change
        align-items: center;
        justify-content: space-between;
    }
    .AvatarNameSelection {
        display: flex;
        align-items: center;
        justify-content: space-between;
        height: 20px;
    }
    .avatarCardTogglesSelection {
        display: flex;
        flex-wrap: wrap;
        justify-content: space-between;
        align-content: center;
        flex-direction: row;
        margin-top: 10px;
	}
	.css-qcqlg7-custom {
		display: flex;
		margin-bottom: 0.8rem;
		color: rgb(255, 255, 255);
		text-decoration: none;
		flex-direction: column;
		border-radius: 8px;
		background-color: rgb(24, 27, 31);
		transition: border-color 0.2s ease-in-out;
		overflow: visible;
	}
	.css-1kj6np9-custom {
		padding-top: 75%;
		height: 0px;
		overflow: hidden;
		border-radius: 8px;
		position: relative;
		display: flex;
		flex-shrink: 0;
		margin-bottom: 0.5rem;
	}
	.avatarCardImage {
		width: 100%;
		height: 100%;
		top: 0px;
		left: 0px;
		position: absolute;
		z-index: 0;
		border: 4px solid #656565a8;
	}
	.css-1brgsnm-custom {
		display: flex;
		flex-direction: column;
		padding: 0.9rem;
		background-color: rgb(37, 42, 48);
		border-color: rgb(37, 42, 48);
		border-style: solid;
		border-width: 3px 3px 0px;
		border-radius: 8px 8px 0px 0px;
	}
	.css-1106r7n-custom {
		display: flex;
		flex-direction: column;
		position: absolute;
		width: 100%;
		height: 100%;
		top: 0px;
		left: 0px;
		-webkit-box-align: center;
		align-items: center;
		-webkit-box-pack: end;
		justify-content: flex-end;
		z-index: 1;
		border-radius: 8px;
		border: 4px solid rgba(255, 255, 255, 0.184);
	}
	.css-1il99ht-custom {
		display: grid;
		grid-template-columns: 20px 1fr 1fr;
		gap: 0.25rem 1rem;
		-webkit-box-align: center;
		align-items: center;
		color: rgb(115, 115, 114);
	}
	.css-13sdljk-custom {
		position: relative;
		overflow: hidden;
		border-radius: 4px;
		display: flex;
	}
	.css-13sdljk-2-custom {
		display: flex;
  		align-items: center;
	    padding: 0.5rem 0.5rem;
	}
	.css-kfjcvw-custom {
		display: flex;
		flex-direction: column;
		padding: 0.9rem;
		border-style: solid;
		background-color: rgb(24, 27, 31);
		border-color: rgb(24, 27, 31);
		border-width: 0px 3px 3px;
		border-radius: 0px 0px 8px 8px;
	}
    .svg-icon {
        color: rgb(84, 181, 197);
        font-size: 20px;
        text-align: center;
        opacity: 1;
        transition: opacity 0.2s ease-in-out;
    }
    .css-1grfcoa-custom {
        display: flex;
        font-weight: bold;
        font-size: 0.85rem;
    }
    .css-so1s8h-custom {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        font-size: 0.85rem;
        white-space: nowrap;
    }
    .css-so1s8h-custom.Excellent {
        color: rgb(81, 255, 0);
    }
    .css-so1s8h-custom.Good {
        color: rgb(0, 255, 55);
    }
    .css-so1s8h-custom.Medium {
        color: rgb(255, 162, 41);
    }
    .css-so1s8h-custom.Poor {
        color: rgb(255, 84, 41);
    }
    .css-so1s8h-custom.VeryPoor {
        color: rgb(255, 0, 0);
    }
    .css-w9ziq0-custom {
        display: flex;
        margin-bottom: 0px;
        -webkit-box-align: center;
        align-items: center;
        height: 50px;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    .css-1fttcpj-custom {
        display: flex;
        flex-direction: column;
    }
    .css-1bcpvc0-custom {
        display: flex;
        width: 100%;
        padding: 0.5rem 0.75rem;
        font-size: 1rem;
        line-height: 1.25;
        height: unset;
        background: rgb(5, 25, 29);
        border: 2px solid rgb(5, 60, 72);
        border-radius: 4px;
        color: rgb(106, 227, 249);
        box-shadow: none;
        transition: 250ms ease-in-out;
        outline: none !important;
    }
    .css-1alc1xs-custom {
        padding: 0px;
        margin: 0px;
        color: rgb(14, 155, 177);
        outline: none !important;
    }
    .css-1alc1xs-custom:hover {
        color: rgb(9, 93, 106);
        text-decoration: none;
    }
    .css-1yw163h-custom {
        font-size: 1.2em;
        margin-top: 0.25rem;
        word-break: break-all;
        text-align: left;
        margin-bottom: 0px;
        color: rgb(255, 255, 255);
    }
    .css-1yw163h-custom:hover {
        color: #1fd1ed;
        text-decoration: none;
    }
    .realiseStatus {
        display: flex;
        border: 1px solid var(--bs-primary);
        border-radius: 4px;
        background-color: rgba(31, 209, 237, 0);
        color: var(--bs-primary);
        padding: 2px 10px;
        margin: 2px 5px 0px 5px;
        transition: background-color 0.2s ease-in-out;
        font-size: 0.85rem;
        outline: none !important;
        cursor: default;
    }
    .realiseStatus.private{
        color: rgb(238, 84, 84);
        border-color: rgb(238, 84, 84);
    }
    .platform {
        z-index: 2;
        display: flex;
        flex-direction: row;
        gap: 0.25rem;
        border: 4px solid #505153;
        border-left-width: 0px;
        border-bottom-width: 0px;
        border-bottom-left-radius: 0.5rem;
        border-top-right-radius: 0.5rem;
        
        padding: 0.5rem;
        transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
        --tw-backdrop-saturate: saturate(2);
        
        --tw-backdrop-blur: blur(8px) !important;
        
        backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia) !important;
        transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1) !important;
        transition-duration: 150ms !important;
        top: 0px !important;
        right: 0px !important;
        position: absolute !important;
    }
`);

window.onload = async function () {
    try {
        showNotification('Initializing...', 'info');
        await Get_ID_And_Cookie();
        handleUrlChange(window.location.href);
        Button_CustomFavorites();
        showNotification('Everything is fine!', 'success');
    } catch (error) {
		showNotification(`Error while Initializing: ${error}`, 'error');
    }
};

async function Get_ID_And_Cookie() {
    try {
        authCookie = await getAuthCookieValue();
        const { responseText } = await SendRequest('GET', `https://api.vrchat.cloud/api/1/auth/user`, authCookie);
        const data = JSON.parse(responseText);
        userId = data.id;
    } catch (error) {
        showNotification(`Error while Get_ID_And_Cookie: ${error}`, 'error');
    }
}

history.pushState = function(state, title, url) {
    originalPushState.apply(history, arguments);
    handleUrlChange(url || window.location.href);
};

history.replaceState = function(state, title, url) {
    originalReplaceState.apply(history, arguments);
    handleUrlChange(url || window.location.href);
};

window.addEventListener('popstate', () => {
    handleUrlChange(window.location.href);
});

async function handleUrlChange(url) {
    try {
        const path = new URL(url, window.location.origin).pathname;
        showNotification(`Path: ${path}`, 'info');
    
        if (path === '/home/custom-favorites' && !isCustomFavoritesMenuOpen) {
            homeContentElement = await awaitForElement('.home-content');
            const firstChild = homeContentElement.firstElementChild;
            if (firstChild === null) {
                openCustomFavorites();
            } else {
                firstChild.style.display = 'none';
                openCustomFavorites();
            }
        } else if (path !== '/home/custom-favorites' && isCustomFavoritesMenuOpen) {
            homeContentElement = await awaitForElement('.home-content');
            const oldCreatedWindow = document.getElementById('custom-favorites-window');
            if (oldCreatedWindow) {
                oldCreatedWindow.remove();
            }
            if (homeContentElement.firstElementChild) {
                homeContentElement.firstElementChild.style.display = 'block';
                isCustomFavoritesMenuOpen = false;
            } else {
                showNotification('Home content element not found, trying to open again', 'error');
                handleUrlChange(url);
            }
        }
    } catch (error) {
        showNotification(
            `Error while handling URL change: ${error.message}\nStack trace: ${error.stack}`,
            'error'
        );
    }
}

async function awaitForElement(selector, timeout = 5000) {
    try {
        await awaitForLoadingToDisappear();

        const element = document.querySelector(selector);
        if (element) return element;

        return new Promise((resolve, reject) => {
            let interval, timeoutId;

            const checkElement = () => {
                const element = document.querySelector(selector);
                if (element) {
                    clearInterval(interval);
                    clearTimeout(timeoutId);
                    resolve(element);
                }
            };

            interval = setInterval(checkElement, 100);

            timeoutId = setTimeout(() => {
                clearInterval(interval);
                showNotification(`Timed out waiting for ${selector}`, 'error');
                reject(new Error(`Timed out waiting for ${selector}`));
            }, timeout);
        });
    } catch (error) {
        showNotification(`Error while waiting for ${selector}: ${error.message}`, 'error');
        throw error;
    }
}

async function awaitForLoadingToDisappear(maxWaitTime = 1000) {
    return new Promise((resolve) => {
        const startTime = Date.now();
        let timeoutId;

        const checkLoading = () => {
            const loadingElement = document.querySelector('[aria-label="Loading"]');
            
            if (!loadingElement) {
                clearTimeout(timeoutId);
                resolve();
            } else if (Date.now() - startTime > maxWaitTime) {
                clearTimeout(timeoutId);
                resolve();
            } else {
                timeoutId = setTimeout(checkLoading, 100);
            }
        };
        
        checkLoading();
    });
}

function showNotification(message, type = 'info', duration = 3000) {
    if (!document.getElementById('notification-container')) {
        const container = document.createElement('div');
        container.id = 'notification-container';
        document.body.appendChild(container);
    }
    const container = document.getElementById('notification-container');
    if (type === 'error') {
        console.error(message);
        duration = 0;
    }

    const notification = document.createElement('div');
    notification.className = `notification ${type}`;
    notification.innerHTML = `
        <div class="status-icon">${getIcon(type)}</div>
        <div class="message">${message}</div>
        <div class="progress-bar"></div>
    `;

    container.prepend(notification);

    enforceNotificationLimit(container);

    setTimeout(() => notification.classList.add('show'), 50);

    const progressBar = notification.querySelector('.progress-bar');
    if (duration > 0) {
        progressBar.style.transitionDuration = `${duration}ms`;
        requestAnimationFrame(() => {
            progressBar.style.transform = 'scaleX(0)';
        });
    } else {
        progressBar.style.display = 'none';
    }

    function close() {
        notification.classList.remove('show');
        notification.classList.add('hide');

        notification.addEventListener('transitionend', () => {
            notification.remove();
        }, { once: true });
    }

    notification.addEventListener('click', close);

    if (duration > 0) {
        setTimeout(close, duration);
    }
}

function enforceNotificationLimit(container) {
    const notifications = Array.from(container.children);
    const maxNotifications = 10;

    if (notifications.length > maxNotifications) {
        const excessCount = notifications.length - maxNotifications;
        for (let i = 0; i < excessCount; i++) {
            const oldNotification = notifications[i];
            oldNotification.classList.remove('show');
            oldNotification.classList.add('hide');
            oldNotification.addEventListener('transitionend', () => {
                oldNotification.remove();
            }, { once: true });
        }
    }
}

function getIcon(type) {
    const icons = {
        info: 'ℹ️',
        success: '✅',
        warning: '⚠️',
        error: '❌'
    };
    return icons[type] || '';
}

async function Button_CustomFavorites() {
    const button = document.createElement('a');
	button.id = 'VRChat_ButtonList';
	button.classList.add('btn-custom', 'css-yjay0l-custom');
    button.title = "Open Custom Favorite Avatars";

    const leftIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    leftIcon.setAttribute('viewBox', '0 0 60 60');
    leftIcon.setAttribute('width', '20');
    leftIcon.setAttribute('height', '20');
    leftIcon.setAttribute('fill', 'none');
    leftIcon.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
    leftIcon.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
    button.appendChild(leftIcon);

    const linearGradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
    linearGradient.setAttribute('id', 'paint0_linear_203_10527');
    linearGradient.setAttribute('gradientUnits', 'userSpaceOnUse');
    linearGradient.setAttribute('x1', '30');
    linearGradient.setAttribute('x2', '30');
    linearGradient.setAttribute('y1', '7');
    linearGradient.setAttribute('y2', '53');
    leftIcon.appendChild(linearGradient);

    const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
    stop1.setAttribute('offset', '0');
    stop1.setAttribute('stop-color', '#ce9ffc');
    linearGradient.appendChild(stop1);

    const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
    stop2.setAttribute('offset', '1');
    stop2.setAttribute('stop-color', '#7367f0');
    linearGradient.appendChild(stop2);

    const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
    g.setAttribute('fill', 'url(#paint0_linear_203_10527)');
    leftIcon.appendChild(g);

    const path1 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    path1.setAttribute('d', 'm14 44c-.803 0-1.557-.313-2.122-.88l-10.999-10.999c-.566-.564-.879-1.318-.879-2.121s.313-1.557.88-2.122l10.999-10.999c.564-.566 1.318-.879 2.121-.879 1.654 0 3 1.346 3 3 0 .803-.313 1.557-.88 2.122l-8.878 8.878 8.879 8.879c.567.564.879 1.318.879 2.121 0 1.654-1.346 3-3 3z');
    path1.setAttribute('fill', 'url(#paint0_linear_203_10527)');
    g.appendChild(path1);

    const path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    path2.setAttribute('d', 'm46 45c-1.654 0-3-1.346-3-3 0-.803.313-1.557.88-2.122l8.878-8.878-8.879-8.879c-.566-.566-.879-1.32-.879-2.121 0-1.654 1.346-3 3-3 .803 0 1.557.313 2.122.88l10.999 10.999c.567.566.879 1.32.879 2.121 0 .803-.313 1.557-.88 2.122l-10.999 10.999c-.564.567-1.318.879-2.121.879z');
    path2.setAttribute('fill', 'url(#paint0_linear_203_10527)');
    g.appendChild(path2);

    const path3 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    path3.setAttribute('d', 'm21 53c-1.654 0-3-1.346-3-3 0-.398.078-.787.23-1.155l18.001-40.002c.47-1.12 1.557-1.843 2.769-1.843 1.654 0 3 1.346 3 3 0 .398-.078.787-.23 1.155l-18.001 40.002c-.47 1.12-1.557 1.843-2.769 1.843z');
    path3.setAttribute('fill', 'url(#paint0_linear_203_10527)');
    g.appendChild(path3);

    const textDiv = document.createElement('div');
    textDiv.textContent = "Custom Favorites";
    button.appendChild(textDiv);

    const rightIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    rightIcon.setAttribute('aria-hidden', 'true');
    rightIcon.setAttribute('focusable', 'false');
    rightIcon.setAttribute('data-prefix', 'fas');
    rightIcon.setAttribute('data-icon', 'angle-right');
    rightIcon.classList.add('svg-inline--fa', 'fa-angle-right', 'css-1efeorg', 'e9fqopp0');
    rightIcon.setAttribute('role', 'presentation');
    rightIcon.setAttribute('viewBox', '0 0 320 512');
    rightIcon.innerHTML = '<path fill="currentColor" d="M278.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-160 160c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L210.7 256 73.4 118.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l160 160z"></path>';
    button.appendChild(rightIcon);

    const targetElement = await awaitForElement('div[role="group"].w-100.css-1bfow8s.btn-group-lg.btn-group-vertical');
    showNotification(`targetElement: ${targetElement}`, 'info');
	targetElement.appendChild(button);
	targetElement.insertBefore(button, targetElement.firstChild);

    button.addEventListener('click', async () => {
        history.pushState({ page: 'custom' }, "Custom Page", "/home/custom-favorites");
        document.title = 'Custom Favorites - VRChat';
	});
}

async function openCustomFavorites() {
    try {
        const HomeContent = homeContentElement;
    
    	const MainWindow = document.createElement('div');
    	MainWindow.id = 'custom-favorites-window';
        MainWindow.classList.add('pb-5', 'css-1fttcpj-custom');
        HomeContent.appendChild(MainWindow);
    
    	const Header = document.createElement('div');
        Header.classList.add('css-zjik7-custom');
        MainWindow.appendChild(Header);
    
    	const HeaderText = document.createElement('h2');
    	HeaderText.classList.add('css-w9ziq0-custom');
    	HeaderText.textContent = "Custom Favorites";
    	Header.appendChild(HeaderText);
    
    	const SearchBar = document.createElement('div');
        SearchBar.classList.add('css-zjik7-custom');
        MainWindow.appendChild(SearchBar);
    
    	const UserInput = document.createElement('input');
        UserInput.type = 'text';
        UserInput.setAttribute('aria-label', 'Search Favorites');
        UserInput.placeholder = 'Search Avatars';
        UserInput.classList.add('css-1bcpvc0-custom');
        UserInput.value = '';
        UserInput.addEventListener('input', () => {
            const query = UserInput.value.trim().toLowerCase();
            filterAvatars(query);
        });
    	SearchBar.appendChild(UserInput);
    
    	const DisplayFunctions = document.createElement('div');
        DisplayFunctions.classList.add('css-zjik7-custom');
        MainWindow.appendChild(DisplayFunctions);
    
    	const NamedList = document.createElement('div');
    	NamedList.classList.add('align-items-center', 'css-zjik7-custom');
    	DisplayFunctions.appendChild(NamedList);
    
    	const NameText = document.createElement('h2');
    	NameText.classList.add('css-w9ziq0-custom');
    	NameText.textContent = "Total Avatars";
    
    	const Counter = document.createElement('div');
    	Counter.classList.add('ms-2', 'css-14ngdq4-custom');
    
        const innerDiv1 = document.createElement('div');
        innerDiv1.classList.add('counter');
        innerDiv1.textContent = '0';
    
        const innerDiv2 = document.createElement('div');
        innerDiv2.classList.add('mx-1');
        innerDiv2.textContent = '/';
    
        const innerDiv3 = document.createElement('div');
        innerDiv3.textContent = '∞';
        Counter.append(innerDiv1, innerDiv2, innerDiv3);
        
    	NamedList.append(NameText, Counter);
    
    	const ButtonsSelection = document.createElement('div');
    	ButtonsSelection.classList.add('align-items-center', 'justify-content-center', 'justify-content-md-end', 'flex-column', 'flex-md-row', 'flex-1', 'css-zjik7-custom');
    	DisplayFunctions.appendChild(ButtonsSelection);
    
    	const Option1 = document.createElement('div');
    	Option1.classList.add('css-13sdljk-custom');
    	ButtonsSelection.appendChild(Option1);
    
    	const SaveCurrentAvatar = document.createElement('button');
    	SaveCurrentAvatar.textContent = 'Save Current Avatar';
    	SaveCurrentAvatar.classList.add('px-3', 'me-1', 'css-1vrq36y-custom');
    	Option1.appendChild(SaveCurrentAvatar);
    	SaveCurrentAvatar.addEventListener('click', async (e) => {
        	e.preventDefault();
    		AddAvatar(addMetod.current);
    	});
    
    	const Option2 = document.createElement('div');
    	Option2.classList.add('css-13sdljk-custom');
    	ButtonsSelection.appendChild(Option2);
    
    	const SaveByID = document.createElement('button');
    	SaveByID.textContent = 'Save By ID';
    	SaveByID.classList.add('px-3', 'me-1', 'css-1vrq36y-custom');
    	Option2.appendChild(SaveByID);
    	SaveByID.addEventListener('click', async (e) => {
        	e.preventDefault();
            showSaveByIDModal();
    	});
    
    	const AvatarsWindow = document.createElement('div');
    	AvatarsWindow.id = 'custom-avatars';
        AvatarsWindow.classList.add('tw-grid', 'tw-grid-cols-1', 'sm:tw-grid-cols-2', 'lg:tw-grid-cols-3', '3xl:tw-grid-cols-4', 'tw-grid-flow-row', 'tw-gap-4');
        MainWindow.appendChild(AvatarsWindow);
    
        savedAvatars = GM_getValue('savedAvatars', []);
        let delayBetweenCards = 50;
        savedAvatars.forEach((avatar, index) => {
            setTimeout(() => {
                createAvatarCard(avatar);
            }, index * delayBetweenCards);
        });
        updateCounter();
        isCustomFavoritesMenuOpen = true;
    } catch (error) {
        showNotification(`Error Message: ${error}`, 'error');
    }
}

function updateCounter() {
    const avatarWindow = document.getElementById('custom-avatars');
    const counter = document.querySelector('.counter');

    if (avatarWindow && counter) {
        const avatarCount = avatarWindow.children.length;
        counter.textContent = avatarCount;
    }
}

function filterAvatars(query) {
    const avatars = document.querySelectorAll('.AvatarCard');
    avatars.forEach(avatar => {
        const avatarName = avatar.querySelector('.css-1yw163h-custom').textContent.toLowerCase();
        const authorName = avatar.querySelector('.css-so1s8h-custom').textContent.toLowerCase();
        const isMatch = avatarName.includes(query) || authorName.includes(query);
        avatar.style.display = isMatch ? 'block' : 'none';
    });
}

async function getAuthCookieValue() {
    return new Promise((resolve) => {
        GM_cookie.list({
            domain: "vrchat.com",
            name: "auth"
        }, (cookies) => {
            resolve(cookies?.[0]?.value || null);
        });
    });
}

async function getUserID(keyName) {
    const data = JSON.parse(localStorage.getItem(keyName));
    if (!Array.isArray(data) || !data[0]?.user_id) {
        throw new Error('user_id not found in first item of array');
    }
    return data[0].user_id;
}

async function SendRequest(method, url, authCookie) {
    return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
            method,
            url,
            headers: {
                "Cookie": `auth=${authCookie}`
            },
            onload: async function(response) {
                if (response.status === 200) {
                    // Debug
                    const { responseText } = response;
                    const data = JSON.parse(responseText);
                    console.dir(data);
                    showNotification(`Request to ${url} completed with status ${response.status}`, 'success');
                    // -------

                    resolve(response);
                } else if (response.status === 404) {
                    showNotification(`Request failed!\nStatus: ${response.status}\nError: ${response.responseText}`, 'warning', 15000);
                    resolve(null);
                } else {
                    showNotification(`Request failed!\nStatus: ${response.status}\nError: ${response.responseText}`, 'warning', 15000);
                }
            }
        });
    });
}

async function GetPrivateAvatarInfo(method, url, authCookie) {
    return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
            method,
            url,
            headers: {
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
                "Cookie": `auth=${authCookie}`
            },
            onload: function (response) {
                const htmlContent = response.responseText;
                const parser = new DOMParser();
                const doc = parser.parseFromString(htmlContent, "text/html");

                const metaData = {
                    title: doc.title,
                    metaTags: Array.from(doc.querySelectorAll('meta')).map(tag => ({
                        name: tag.getAttribute('name') || tag.getAttribute('property') || tag.getAttribute('itemprop'),
                        content: tag.getAttribute('content')
                    })),
                    scripts: Array.from(doc.querySelectorAll('script')).map(script => script.src),
                    styles: Array.from(doc.querySelectorAll('link[rel="stylesheet"]')).map(link => link.href)
                };

                const ogImageTag = metaData.metaTags.find(tag => tag.name === 'og:image');
                const ogTitleTag = metaData.metaTags.find(tag => tag.name === 'og:title');

                const [name, author] = ogTitleTag.content.trim().split(/[\s]*by[\s]*/).map(x => x.trim());

                const result = {
                    PrivateImage: ogImageTag.content,
                    PrivateName: name,
                    PrivateAuthor: author
                };

                resolve(result);
            }
        });
    })
}

function FixDisplayTime(time) {
    let date = new Date(time);
    let day = String(date.getDate()).padStart(2, '0');
    let month = String(date.getMonth() + 1).padStart(2, '0');
    let year = date.getFullYear();
    let formattedDate = `${day}.${month}.${year}`;
    return formattedDate;
}

async function AddAvatar(metod, avatarID = null) {
    let link = null;

    if (metod == addMetod.current) {
        link = `https://api.vrchat.cloud/api/1/users/${userId}/avatar`;
    } else if (metod == addMetod.ID) {
        link = `https://api.vrchat.cloud/api/1/avatars/${avatarID}`
    }

    let isUnavailable = false;
    let unavailableData = null;
    let data = null;

    const response = await SendRequest('GET', `${link}`, authCookie);
    if (response) {
        const { responseText } = response;
        if (responseText) {
            data = JSON.parse(responseText);
        }
    } else {
        isUnavailable = true;
        let privateAvatarInfo = await GetPrivateAvatarInfo('GET', `https://vrchat.com/home/avatar/${avatarID}`, authCookie);

        unavailableData = {
            PrivateImage: privateAvatarInfo.PrivateImage,
            PrivateName: privateAvatarInfo.PrivateName,
            PrivateAuthor: privateAvatarInfo.PrivateAuthor
        };
    }

    let hasPC = false;
    let hasQuest = false;
    let pcPerformance = '';
    let questPerformance = '';

    if (!isUnavailable && Array.isArray(data.unityPackages) && data.unityPackages.length > 0) {
        data.unityPackages.forEach(pkg => {
            let category = '';
            if (pkg.platform === 'standalonewindows' && pkg.variant === 'security') {
                category = 'pc';
                hasPC = true;
                pcPerformance = pkg.performanceRating;
            } else if (pkg.platform === 'android' && pkg.variant === 'security') {
                category = 'quest';
                hasQuest = true;
                questPerformance = pkg.performanceRating;
            }
        });
    }
    let avatarData = null;

    if (!isUnavailable) {
        avatarData = {
            authorName: data.authorName,
            authorId: data.authorId,
            created_at: FixDisplayTime(data.created_at),
            description: data.description,
            avatarID: data.id,
            imageUrl: data.imageUrl,
            avatarName: data.name,
            releaseStatus: data.releaseStatus,
            updated_at: FixDisplayTime(data.updated_at),
            version: data.version,
    
            isPlatformPC: hasPC,
            PC_Performance: pcPerformance,
            isPlatformQuest: hasQuest,
            Quest_Performance: questPerformance,

            isUnavalibleAvatar: isUnavailable
        };
    } else {
        avatarData = {
            authorName: unavailableData.PrivateAuthor,
            authorId: 'Unknown',
            created_at: 'Unknown',
            description: 'Unknown',
            avatarID: avatarID,
            imageUrl: unavailableData.PrivateImage,
            avatarName: unavailableData.PrivateName,
            releaseStatus: 'private',
            updated_at: 'Unknown',
            version: 'Unknown',
    
            isPlatformPC: hasPC,
            PC_Performance: 'Unknown',
            isPlatformQuest: hasQuest,
            Quest_Performance: 'Unknown',

            isUnavalibleAvatar: isUnavailable
        };
    }

    if (!savedAvatars.some(a => a.avatarID === avatarData.avatarID)) {
        savedAvatars.push(avatarData);
        GM_setValue('savedAvatars', savedAvatars);
        createAvatarCard(avatarData);
        showNotification(`Saved Avatar: ${avatarData.avatarName}!`, 'success');
    } else {
        showNotification('This avatar is already saved!', 'warning', 7000);
    }
}

function createAvatarCard(avatar) {
    const AvatarsSelection = document.getElementById('custom-avatars');

    const AvatarCard = document.createElement('div');
    AvatarCard.classList.add('css-qcqlg7-custom', 'AvatarCard');
    AvatarCard.setAttribute('data-avatar-id', avatar.avatarID);
    AvatarsSelection.appendChild(AvatarCard);

	const ImageAndName = document.createElement('div');
    ImageAndName.classList.add('css-1brgsnm-custom');
    if (avatar.isUnavalibleAvatar) {
        ImageAndName.style.borderColor = 'rgb(238, 84, 84)';
    }
    if (avatar.PC_Performance === 'None') {
        ImageAndName.style.borderColor = 'rgb(235, 114, 33)';
    }
	AvatarCard.appendChild(ImageAndName);

    const linkElement = document.createElement('a');
    linkElement.setAttribute('aria-label', 'Avatar Image');
    linkElement.classList.add('css-1kj6np9-custom');
    linkElement.href = `/home/avatar/${avatar.avatarID}`;
	ImageAndName.appendChild(linkElement);

    
    const iconDiv = document.createElement('div');
    iconDiv.classList.add('platform');
    if (avatar.isUnavalibleAvatar) {
        iconDiv.style.display = 'none';
    }
    linkElement.appendChild(iconDiv);

    if (avatar.isPlatformPC) {
        const Windows = document.createElement('div');
        Windows.setAttribute('role', 'note');
        Windows.setAttribute('title', 'Is a Windows Avatar');
        Windows.classList.add('tw-flex', 'tw-items-center', 'tw-justify-center', 'tw-w-6', 'tw-h-6', 'tw-border', 'tw-border-solid', 'tw-border-current', 'tw-rounded-full');
        Windows.style.color = 'rgb(23, 120, 255)';
        iconDiv.appendChild(Windows);
    
        const PC_Icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        PC_Icon.setAttribute('aria-hidden', 'true');
        PC_Icon.setAttribute('focusable', 'false');
        PC_Icon.setAttribute('data-prefix', 'fab');
        PC_Icon.setAttribute('data-icon', 'windows');
        PC_Icon.classList.add('svg-inline--fa', 'fa-windows', 'css-1efeorg', 'e9fqopp0');
        PC_Icon.setAttribute('role', 'presentation');
        PC_Icon.setAttribute('viewBox', '0 0 448 512');
        Windows.appendChild(PC_Icon);
    
        const PC_IconPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        PC_IconPath.setAttribute('fill', 'currentColor');
        PC_IconPath.setAttribute('d', 'M0 93.7l183.6-25.3v177.4H0V93.7zm0 324.6l183.6 25.3V268.4H0v149.9zm203.8 28L448 480V268.4H203.8v177.9zm0-380.6v180.1H448V32L203.8 65.7z');
        PC_Icon.appendChild(PC_IconPath);
    }

    if (avatar.isPlatformQuest) {
        const Quest = document.createElement('div');
        Quest.setAttribute('role', 'note');
        Quest.setAttribute('title', 'Is an Android Avatar');
        Quest.classList.add('tw-flex', 'tw-items-center', 'tw-justify-center', 'tw-w-6', 'tw-h-6', 'tw-border', 'tw-border-solid', 'tw-border-current', 'tw-rounded-full');
        Quest.style.color = 'rgb(43, 207, 92)';
        iconDiv.appendChild(Quest);
    
        const Quest_Icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        Quest_Icon.setAttribute('aria-hidden', 'true');
        Quest_Icon.setAttribute('focusable', 'false');
        Quest_Icon.setAttribute('data-prefix', 'fab');
        Quest_Icon.setAttribute('data-icon', 'windows');
        Quest_Icon.classList.add('svg-inline--fa', 'fa-windows', 'css-1efeorg', 'e9fqopp0');
        Quest_Icon.setAttribute('role', 'presentation');
        Quest_Icon.setAttribute('viewBox', '0 0 576 512');
        Quest.appendChild(Quest_Icon);
    
        const Quest_IconPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        Quest_IconPath.setAttribute('fill', 'currentColor');
        Quest_IconPath.setAttribute('d', 'M420.55,301.93a24,24,0,1,1,24-24,24,24,0,0,1-24,24m-265.1,0a24,24,0,1,1,24-24,24,24,0,0,1-24,24m273.7-144.48,47.94-83a10,10,0,1,0-17.27-10h0l-48.54,84.07a301.25,301.25,0,0,0-246.56,0L116.18,64.45a10,10,0,1,0-17.27,10h0l47.94,83C64.53,202.22,8.24,285.55,0,384H576c-8.24-98.45-64.54-181.78-146.85-226.55');
        Quest_Icon.appendChild(Quest_IconPath);
    }

    const imgElement = document.createElement('img');
    imgElement.src = `${avatar.imageUrl}`;
    imgElement.alt = `${avatar.avatarName}`;
    imgElement.classList.add('avatarCardImage');
    linkElement.appendChild(imgElement);

    const avatarDisplayName = document.createElement('div');
    avatarDisplayName.classList.add('AvatarNameSelection');
    ImageAndName.appendChild(avatarDisplayName);

    const OpenAvatarPage = document.createElement('a');
    OpenAvatarPage.setAttribute('aria-label', 'Open Avatar Page');
    OpenAvatarPage.classList.add('css-1alc1xs-custom');
    OpenAvatarPage.href = `/home/avatar/${avatar.avatarID}`;
    avatarDisplayName.appendChild(OpenAvatarPage);

    const avatarH4 = document.createElement('h4');
    avatarH4.classList.add('css-1yw163h-custom');
    avatarH4.textContent = `${avatar.avatarName}`;
    if (avatarH4.textContent.length > 23) {
        const fontSize = 1.1 - (avatarH4.textContent.length - 23) * 0.02;
        avatarH4.style.fontSize = `${fontSize}rem`;
    }
    OpenAvatarPage.appendChild(avatarH4);

    const releaseStatus = document.createElement('h2');
    releaseStatus.classList.add('realiseStatus');
    if (avatar.releaseStatus === 'private') {
        if (avatar.isUnavalibleAvatar) {
            avatar.releaseStatus = avatar.authorName ? 'private/deleted' : 'Fully Deleted';
        }
    }
    if (['private', 'private/deleted', 'Fully Deleted'].includes(avatar.releaseStatus)) {
        releaseStatus.classList.add('private');
        if (avatar.authorId === userId) {
            releaseStatus.title = 'Only you can use this avatar';
        }
    }
    releaseStatus.textContent = avatar.releaseStatus;
    avatarDisplayName.appendChild(releaseStatus);

	const mainDiv = document.createElement('div');
    mainDiv.classList.add('css-kfjcvw-custom');
    if (avatar.isUnavalibleAvatar) {
        mainDiv.style.borderColor = 'rgb(238, 84, 84)';
    }
    if (avatar.PC_Performance === 'None') {
        mainDiv.style.borderColor = 'rgb(235, 114, 33)';
    }
	AvatarCard.appendChild(mainDiv);

    const innerDiv = document.createElement('div');
    innerDiv.classList.add('css-1il99ht-custom');
    mainDiv.appendChild(innerDiv);

    const userSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    userSvg.classList.add('svg-icon');
    userSvg.setAttribute('viewBox', '0 0 448 512');
    userSvg.setAttribute('color', '#54b5c5');
    userSvg.setAttribute('width', '20');
    innerDiv.appendChild(userSvg);

    const userPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    userPath.setAttribute('fill', 'currentColor');
    userPath.setAttribute('d', 'M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512l388.6 0c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304l-91.4 0z');
    userSvg.appendChild(userPath);

    const authorText = document.createElement('div');
    authorText.classList.add('css-1grfcoa-custom');
    authorText.textContent = 'Author';
    innerDiv.appendChild(authorText);

    const userLink = document.createElement('div');
    userLink.classList.add('css-so1s8h-custom');
    innerDiv.appendChild(userLink);

    const link = document.createElement('a');
    if (!avatar.isUnavalibleAvatar) {
        link.href = `/home/user/${avatar.authorId}`;
    }
    link.textContent = `${avatar.authorName}`;
    userLink.appendChild(link);

    const cloudSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    cloudSvg.classList.add('svg-icon');
    cloudSvg.setAttribute('viewBox', '0 0 640 512');
    cloudSvg.setAttribute('color', '#54b5c5');
    cloudSvg.setAttribute('width', '20');
    innerDiv.appendChild(cloudSvg);

    const cloudPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    cloudPath.setAttribute('fill', 'currentColor');
    cloudPath.setAttribute('d', 'M144 480C64.5 480 0 415.5 0 336c0-62.8 40.2-116.2 96.2-135.9c-.1-2.7-.2-5.4-.2-8.1c0-88.4 71.6-160 160-160c59.3 0 111 32.2 138.7 80.2C409.9 102 428.3 96 448 96c53 0 96 43 96 96c0 12.2-2.3 23.8-6.4 34.6C596 238.4 640 290.1 640 352c0 70.7-57.3 128-128 128l-368 0zm79-217c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l39-39L296 392c0 13.3 10.7 24 24 24s24-10.7 24-24l0-134.1 39 39c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-80-80c-9.4-9.4-24.6-9.4-33.9 0l-80 80z');
    cloudSvg.appendChild(cloudPath);

    const updatedText = document.createElement('div');
    updatedText.classList.add('css-1grfcoa-custom');
    updatedText.textContent = 'Last Updated';
    innerDiv.appendChild(updatedText);

    const dateDiv = document.createElement('div');
    dateDiv.classList.add('text-start', 'css-so1s8h-custom');
    dateDiv.textContent = `${avatar.updated_at}`;
    dateDiv.setAttribute('title', `Created: ${avatar.created_at}\nLast Update: ${avatar.updated_at}\nAvatar Version: ${avatar.version}`);
    innerDiv.appendChild(dateDiv);

    const perfomanceElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    perfomanceElement.classList.add('svg-icon');
    perfomanceElement.setAttribute('viewBox', '0 0 512 512');
    perfomanceElement.setAttribute('color', '#54b5c5');
    perfomanceElement.setAttribute('width', '20');
    innerDiv.appendChild(perfomanceElement);

    const performanceSVG = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    performanceSVG.setAttribute('fill', 'currentColor');
    performanceSVG.setAttribute('d', 'M159.3 5.4c7.8-7.3 19.9-7.2 27.7 .1c27.6 25.9 53.5 53.8 77.7 84c11-14.4 23.5-30.1 37-42.9c7.9-7.4 20.1-7.4 28 .1c34.6 33 63.9 76.6 84.5 118c20.3 40.8 33.8 82.5 33.8 111.9C448 404.2 348.2 512 224 512C98.4 512 0 404.1 0 276.5c0-38.4 17.8-85.3 45.4-131.7C73.3 97.7 112.7 48.6 159.3 5.4zM225.7 416c25.3 0 47.7-7 68.8-21c42.1-29.4 53.4-88.2 28.1-134.4c-4.5-9-16-9.6-22.5-2l-25.2 29.3c-6.6 7.6-18.5 7.4-24.7-.5c-16.5-21-46-58.5-62.8-79.8c-6.3-8-18.3-8.1-24.7-.1c-33.8 42.5-50.8 69.3-50.8 99.4C112 375.4 162.6 416 225.7 416z');
    perfomanceElement.appendChild(performanceSVG);

    const perfomance = document.createElement('div');
    perfomance.classList.add('css-1grfcoa-custom');
    perfomance.textContent = 'Performance';
    innerDiv.appendChild(perfomance);

    const PCText = document.createElement('div');
    PCText.classList.add('text-start', 'css-so1s8h-custom');
    PCText.textContent = `PC`;
    innerDiv.appendChild(PCText);

    const raiting = document.createElement('div');
    if (avatar.PC_Performance === 'Excellent') {
        raiting.classList.add('text-start', 'css-so1s8h-custom', 'Excellent');
    } else if (avatar.PC_Performance === 'Good') {
        raiting.classList.add('text-start', 'css-so1s8h-custom', 'Good');
    } else if (avatar.PC_Performance === 'Medium') {
        raiting.classList.add('text-start', 'css-so1s8h-custom', 'Medium');
    } else if (avatar.PC_Performance === 'Poor') {
        raiting.classList.add('text-start', 'css-so1s8h-custom', 'Poor');
    } else if (avatar.PC_Performance === 'VeryPoor') {
        raiting.classList.add('text-start', 'css-so1s8h-custom', 'VeryPoor');
    }
    if (avatar.PC_Performance !== 'None') {
        raiting.textContent = `${avatar.PC_Performance}`;
    } else {
        raiting.textContent = 'Security Failed';
        raiting.style.color = 'red';
        raiting.style.textDecoration = 'underline';
        raiting.style.textDecorationLine = 'spelling-error';
    }
    raiting.style.marginLeft = '10px';
    PCText.appendChild(raiting);

    if (avatar.PC_Performance !== 'None' && !avatar.isUnavalibleAvatar) {
        const verypoorImage = document.createElement('img');
        if (avatar.PC_Performance === 'Excellent') {
            verypoorImage.src = 'https://dtuitjyhwcl5y.cloudfront.net/b7e99cd3c42a6f1ff2e6f3faaada0e75366945997a7fa5e7e014d26b1d100ef7.svg';
        } else if (avatar.PC_Performance === 'Good') {
            verypoorImage.src = 'https://dtuitjyhwcl5y.cloudfront.net/db3f587335a6602a84d0f0f18d6fbb10904973d0ddb659009f0fc56b3d1f026b.svg';
        } else if (avatar.PC_Performance === 'Medium') {
            verypoorImage.src = 'https://dtuitjyhwcl5y.cloudfront.net/24001ed5aa8ebabaa63a09ffb88ccecccc4c5feb1b4179579e8e6c9f1fed3f16.svg';
        } else if (avatar.PC_Performance === 'Poor') {
            verypoorImage.src = 'https://dtuitjyhwcl5y.cloudfront.net/467c01a863f0a61d30a09465f743678c95a5e6ae6d439b2fecd257464ec111d0.svg';
        } else if (avatar.PC_Performance === 'VeryPoor') {
            verypoorImage.src = 'https://dtuitjyhwcl5y.cloudfront.net/b4bf11dfbd8c3076cb66e8457b3f78659854700e79d5256516e205e37af89247.svg';
        }
        verypoorImage.alt = 'Avatar Icon';
        verypoorImage.style.width = '20px';
        verypoorImage.style.height = '20px';
        verypoorImage.style.marginLeft = '10px';
        verypoorImage.classList.add('css-1il99ht-custom');
        raiting.appendChild(verypoorImage);
    }

    const QuestText = document.createElement('div');
    QuestText.classList.add('text-start', 'css-so1s8h-custom');
    QuestText.textContent = `Quest`;
    PCText.appendChild(QuestText);

    const Qraiting = document.createElement('div');
    if (avatar.Quest_Performance === 'Excellent') {
        Qraiting.classList.add('text-start', 'css-so1s8h-custom', 'Excellent');
    } else if (avatar.Quest_Performance === 'Good') {
        Qraiting.classList.add('text-start', 'css-so1s8h-custom', 'Good');
    } else if (avatar.Quest_Performance === 'Medium') {
        Qraiting.classList.add('text-start', 'css-so1s8h-custom', 'Medium');
    } else if (avatar.Quest_Performance === 'Poor') {
        Qraiting.classList.add('text-start', 'css-so1s8h-custom', 'Poor');
    } else if (avatar.Quest_Performance === 'VeryPoor') {
        Qraiting.classList.add('text-start', 'css-so1s8h-custom', 'VeryPoor');
    }
    if (avatar.Quest_Performance) {
        Qraiting.textContent = `${avatar.Quest_Performance}`;
    } else {
        Qraiting.textContent = 'None';
    }
    Qraiting.style.marginLeft = '10px';
    QuestText.appendChild(Qraiting);

    if (avatar.Quest_Performance && avatar.Quest_Performance !== 'None' && !avatar.isUnavalibleAvatar) {
        const QverypoorImage = document.createElement('img');
        if (avatar.Quest_Performance === 'Excellent') {
            QverypoorImage.src = 'https://dtuitjyhwcl5y.cloudfront.net/b7e99cd3c42a6f1ff2e6f3faaada0e75366945997a7fa5e7e014d26b1d100ef7.svg';
        } else if (avatar.Quest_Performance === 'Good') {
            QverypoorImage.src = 'https://dtuitjyhwcl5y.cloudfront.net/db3f587335a6602a84d0f0f18d6fbb10904973d0ddb659009f0fc56b3d1f026b.svg';
        } else if (avatar.Quest_Performance === 'Medium') {
            QverypoorImage.src = 'https://dtuitjyhwcl5y.cloudfront.net/24001ed5aa8ebabaa63a09ffb88ccecccc4c5feb1b4179579e8e6c9f1fed3f16.svg';
        } else if (avatar.Quest_Performance === 'Poor') {
            QverypoorImage.src = 'https://dtuitjyhwcl5y.cloudfront.net/467c01a863f0a61d30a09465f743678c95a5e6ae6d439b2fecd257464ec111d0.svg';
        } else if (avatar.Quest_Performance === 'VeryPoor') {
            QverypoorImage.src = 'https://dtuitjyhwcl5y.cloudfront.net/b4bf11dfbd8c3076cb66e8457b3f78659854700e79d5256516e205e37af89247.svg';
        }
        QverypoorImage.alt = 'Avatar Icon';
        QverypoorImage.style.width = '20px';
        QverypoorImage.style.height = '20px';
        QverypoorImage.style.marginLeft = '10px';
        QverypoorImage.classList.add('css-1il99ht-custom');
        Qraiting.appendChild(QverypoorImage);
    }

    const ButtonsNew = document.createElement('div');
    ButtonsNew.classList.add('avatarCardTogglesSelection');
    if (avatar.isUnavalibleAvatar) {
        ButtonsNew.style.justifyContent = 'flex-end';
    }
    mainDiv.appendChild(ButtonsNew);

    if (!avatar.isUnavalibleAvatar) {
        const selectAvatar = document.createElement('button');
        selectAvatar.textContent = 'Select Avatar';
        selectAvatar.classList.add('px-3', 'avatarCardToggles');
        selectAvatar.addEventListener('click', async (e) => {
            e.preventDefault();
            changeSelectedAvatar(avatar.avatarID);
        });
        ButtonsNew.appendChild(selectAvatar);
    }

	const deleteAvatar = document.createElement('button');
	deleteAvatar.textContent = 'Remove';
	deleteAvatar.style.color = '#ee5454';
	deleteAvatar.style.border = '2px solid #ee5454';
	deleteAvatar.classList.add('px-3', 'avatarCardToggles', 'Remove');
	deleteAvatar.addEventListener('click', async (e) => {
    	e.preventDefault();
        const avatarCard = e.target.closest('.AvatarCard');
        if (avatarCard) {
            const avatarID = avatarCard.getAttribute('data-avatar-id');
            savedAvatars = savedAvatars.filter(a => a.avatarID !== avatarID);
            GM_setValue('savedAvatars', savedAvatars);
            avatarCard.remove();
            showNotification(`Avatar removed: ${avatar.avatarName}!`, 'info');
            updateCounter();
        } else {
            showNotification('Failed to remove avatar.', 'error');
        }
    });
    ButtonsNew.appendChild(deleteAvatar);

    updateCounter();
}

async function changeSelectedAvatar(avatarID) {
    const authCookie = await getAuthCookieValue();
    await SendRequest('PUT', `https://api.vrchat.cloud/api/1/avatars/${avatarID}/select`, authCookie);
    showNotification(`Avatar selected: ${avatarID}!`, 'success');
}

function showSaveByIDModal() {
    const modal = document.createElement('div');
    modal.id = 'save-by-id-modal';
    modal.classList.add('modal-overlay');
    modal.innerHTML = `
        <div class="modal-content">
            <h3>Save New Avatar</h3>
            <label>Avatar URL or ID:</label>
            <input type="text" id="avatar-url" placeholder="Enter URL or ID" />
            <label>For example: avtr_26187637-0c30-4a09-86e1-bc928c07309e</label>
            <div class="modal-buttons">
                <button id="save-avatar-btn" disabled>Save</button>
                <button id="cancel-btn">Cancel</button>
            </div>
        </div>
    `;

    GM_addStyle(`
    /* Анимации */
    @keyframes modalEnter {
        0% { opacity: 0; transform: scale(1.15); }
        100% { opacity: 1; transform: scale(1); }
    }

    @keyframes modalExit {
        0% { opacity: 1; transform: scale(1); }
        100% { opacity: 0; transform: scale(1.15); }
    }

    .modal-overlay {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: rgba(0, 0, 0, 0.7);
        display: flex;
        justify-content: center;
        align-items: center;
        z-index: 2000;
        backdrop-filter: blur(4px);
        animation: modalEnter 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    }

    .modal-overlay.exit {
        animation: modalExit 0.2s ease-out forwards;
    }

    .modal-content {
        background: rgb(24, 27, 31);
        color: #e0e0e0;
        padding: 1.5rem;
        border-radius: 8px;
        box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
        width: 512px;
        border: 2px solid rgb(37, 42, 48);
        position: relative;
    }

    .modal-content h3 {
        margin: 0 0 1.25rem 0;
        font-size: 1.5rem;
        color: rgb(106, 227, 249);
        text-align: center;
        font-weight: 500;
    }

    .modal-content label {
        display: block;
        margin-bottom: 0.5rem;
        font-size: 0.9rem;
        color: rgb(160, 160, 160);
    }

    .modal-content input {
        width: 100%;
        padding: 0.75rem 1rem;
        margin: 0.5rem 0 1.25rem 0;
        border: 2px solid rgb(6, 75, 92);
        border-radius: 4px;
        background: rgb(7, 36, 43);
        color: #e0e0e0;
        font-size: 1rem;
        transition: all 0.2s ease-in-out;
    }

    .modal-content input:focus {
        outline: none;
        border-color: rgb(8, 108, 132);
        box-shadow: 0 0 0 3px rgba(106, 227, 249, 0.1);
    }

    .modal-content input:hover {
        border-color: rgb(8, 108, 132);
    }

    .modal-buttons {
        display: flex;
        gap: 0.75rem;
        margin-top: 1rem;
    }

    #save-avatar-btn, #cancel-btn {
        flex: 1;
        padding: 0.75rem 1.25rem;
        border: 2px solid transparent;
        border-radius: 4px;
        cursor: pointer;
        font-weight: 500;
        transition: all 0.2s ease-in-out;
        background-color: rgb(37, 42, 48);
        color: rgb(106, 227, 249);
    }

    #save-avatar-btn {
        background-color: rgb(6, 75, 92);
        border-color: rgb(8, 108, 132);
    }

    #save-avatar-btn:hover {
        background-color: rgb(8, 108, 132);
        border-color: rgb(255, 255, 255);
        transform: scale(1.05);
    }

    #cancel-btn {
        color: rgb(238, 84, 84);
        background-color: rgb(7, 36, 43);
        border: 3px solid rgb(5, 60, 72);
    }

    #cancel-btn:hover {
        background-color: rgb(5, 25, 29);
        border-color: rgb(5, 25, 29)
        transform: scale(1.05);
    }

    /* Адаптация под ваши стили кнопок */
    .modal-buttons button {
        --bs-btn-border-radius: 4px;
        --bs-btn-padding-y: 0.75rem;
        --bs-btn-padding-x: 1.25rem;
        --bs-btn-font-size: 1rem;
        border-width: 2px;
    }

    /* Стили фокуса */
    .modal-content input:focus {
        background: linear-gradient(#242424, #242424) padding-box,
                    linear-gradient(135deg, rgba(106, 227, 249, 0.4), rgba(8, 108, 132, 0.4)) border-box;
        border: 2px solid transparent;
    }

    /* Добавляем стили для валидации */
    .modal-content input.valid {
        border-color: #4CAF50 !important;
    }

    .modal-content input.invalid {
        border-color: #FF5722 !important;
    }

    /* Принудительное переопределение стилей при валидации */
    .modal-content input:hover {
        border-color: inherit;
    }

    #save-avatar-btn:disabled {
        background-color: rgb(37, 42, 48);
        border-color: transparent;
        cursor: not-allowed;
        transform: none;
    }

    #save-avatar-btn:disabled:hover {
        background-color: rgb(37, 42, 48);
        border-color: transparent;
        box-shadow: none;
    }
`);

    document.body.appendChild(modal);

    const saveBtn = modal.querySelector('#save-avatar-btn');
    const cancelBtn = modal.querySelector('#cancel-btn');
    const inputField = modal.querySelector('#avatar-url');

    const urlPattern = /^https:\/\/vrchat\.com\/home\/avatar\/avtr_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
    const idPattern = /^avtr_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

    const validateInput = () => {
        const value = inputField.value.trim();
        const isValidURL = urlPattern.test(value);
        const isValidID = idPattern.test(value);
        const isValid = isValidURL || isValidID;

        inputField.classList.toggle('valid', isValid);
        inputField.classList.toggle('invalid', !isValid);
        saveBtn.disabled = !isValid;
    };

    inputField.addEventListener('input', validateInput);

    const handleClose = () => {
        modal.classList.add('exit');
        setTimeout(() => {
            modal.remove();
        }, 200);
    };

    saveBtn.addEventListener('click', async () => {
        const value = inputField.value.trim();
        let avatarID;

        if (urlPattern.test(value)) {
        avatarID = value.split('/').pop();
        } else if (idPattern.test(value)) {
            avatarID = value;
        } else {
            showNotification('Invalid URL or ID format!', 'error');
            return;
        }

        try {
            AddAvatar(addMetod.ID, avatarID);
            handleClose();
        } catch (error) {
            showNotification(`Failed to add avatar: ${error.message}`, 'error');
        }

        handleClose();
    });

    cancelBtn.addEventListener('click', () => {
        handleClose();
    });

    modal.addEventListener('click', (event) => {
        if (event.target === modal) {
            handleClose();
        }
    });
}