// ==UserScript==
// @name Disconnect Tools
// @namespace http://tampermonkey.net/
// @license MIT
// @version 2025-04-03v0.9.3
// @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 sortSettings = GM_getValue('sortSettings', { sortBy: 'default', isReversed: false });
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;
GM_addStyle(`
#notification-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 3050;
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;
}
}
`);
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;
transition: all .15s ease-in-out;
}
.css-1vrq36y-custom.syncButton {
width: 35px;
height: 35px;
padding: 0px;
}
.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;
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%;
min-width: 10%;
padding: 0.5rem 0.75rem;
font-size: 1rem;
line-height: 1.25;
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;
}
.sync {
z-index: 2;
display: flex;
flex-direction: row;
gap: 0.25rem;
border: 4px solid #505153;
border-right-width: 0px;
border-bottom-width: 0px;
// border-bottom-right-radius: 0.5rem;
// border-top-left-radius: 0.5rem;
padding: 0.2rem;
// 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;
left: 0px !important;
position: absolute !important;
}
.me-1-custom{
margin-left: 0.25rem;
}
@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;
}
#avatar-search-modal .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: 600px;
max-height: 80vh;
overflow-y: auto;
border: 2px solid rgb(37, 42, 48);
position: relative;
}
#avatar-search-modal h3 {
margin: 0 0 1.25rem 0;
font-size: 1.5rem;
color: rgb(106, 227, 249);
text-align: center;
font-weight: 500;
}
#search-results {
max-height: 400px;
overflow-y: auto;
padding: 1rem;
border: 2px solid rgb(5, 60, 72);
border-radius: 8px;
background-color: rgb(24, 27, 31);
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-thumb {
background-color: rgb(6, 75, 92);
border-radius: 4px;
}
&::-webkit-scrollbar-track {
background-color: rgb(37, 42, 48);
border-radius: 4px;
}
}
#avatar-search-modal #search-results {
display: flex;
flex-direction: column;
align-items: center;
max-height: 400px;
overflow-y: auto;
padding: 0.5rem 0;
border-top: 1px solid rgb(5, 60, 72);
border-bottom: 1px solid rgb(5, 60, 72);
transition: all 0.2s ease-in-out;
}
.search-result-card {
display: flex;
align-items: center;
background: rgb(37, 42, 48);
padding: 0.75rem;
border-radius: 4px;
transition: all 0.2s ease-in-out;
}
.search-result-card:hover {
background: rgb(10, 57, 69);
}
.search-result-card img {
width: 65px;
height: 50px;
border-radius: 5px;
margin-right: 10px;
object-fit: cover;
transition: all 0.2s ease-in-out;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12);
}
.search-result-card img:hover {
transform: scale(2.5) translate(20%, 20%);
box-shadow: 0 8px 10px rgb(0, 0, 0);
}
.search-result-card .info {
flex-grow: 1;
}
.search-result-card h4 {
margin: 0;
font-size: 1rem;
color: rgb(106, 227, 249);
}
.search-result-card p {
margin: 0.25rem 0 0 0;
font-size: 0.9rem;
color: rgb(160, 160, 160);
}
.search-result-card button {
padding: 0.5rem 1rem;
border: 2px solid rgb(6, 75, 92);
border-radius: 4px;
background: rgb(6, 75, 92);
color: #e0e0e0;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s ease-in-out;
max-width: 150px;
min-width: 150px;
}
.search-result-card button:hover {
background: rgb(8, 108, 132);
border-color: rgb(8, 108, 132);
transform: scale(1.05);
}
#avatar-search-modal #close-modal-btn {
display: block;
margin: 1.5rem auto 0;
padding: 0.35rem 1.5rem;
border: 2px solid rgb(6, 75, 92);
border-radius: 4px;
background: rgb(6, 75, 92);
color: #e0e0e0;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s ease-in-out;
}
#avatar-search-modal #close-modal-btn:hover {
background: rgba(8, 108, 132, 0.25);
border-color: rgb(8, 108, 132);
transform: scale(1.05);
}
#pagination-container {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 1rem;
}
#pagination-container button {
margin: 0 5px;
}
`);
GM_addStyle(`
.sync-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);
}
.sync-modal-overlay.exit {
animation: modalExit 0.2s ease-out forwards;
}
.modal-sync {
background: rgb(24, 27, 31);
color: #e0e0e0;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
width: 600px;
max-height: 80vh;
overflow-y: auto;
border: 2px solid rgb(37, 42, 48);
position: relative;
}
.modal-sync-title {
margin: 0 0 1rem 0;
font-size: 1.5rem;
color: rgb(106, 227, 249);
text-align: center;
font-weight: 500;
}
#sync-changes-list {
max-height: 470px;
overflow-y: auto;
padding: 10px;
// margin: 1rem 0;
// padding: 1rem;
// background: rgb(7, 36, 43);
// border-radius: 4px;
}
.sync-change-item {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
border-radius: 6px;
background: linear-gradient(135deg, rgba(20, 40, 50, 0.8), rgba(10, 25, 30, 0.8));
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
margin-bottom: 0.5rem;
transition: all 0.3s ease-in-out;
border: 2px solid transparent;
border-color: rgb(0, 40, 55);
}
.sync-change-item:hover {
background: linear-gradient(135deg, rgba(25, 50, 60, 0.9), rgba(15, 35, 40, 0.9));
border-color: rgba(106, 227, 249, 0.5);
transform: scale(1.03);
}
.sync-change-label {
font-size: 1rem;
color: rgb(140, 230, 250);
font-weight: bold;
// margin-left: 0.5rem;
// text-transform: capitalize;
// letter-spacing: 0.5px;
width: 100%;
}
.sync-change-checkbox {
appearance: none;
width: 1.5rem;
height: 1.5rem;
border-radius: 4px;
background: rgb(10, 30, 35);
border: 2px solid rgb(15, 50, 60);
outline: none;
cursor: pointer;
transition: all 0.3s ease-in-out;
position: relative;
margin-right: 0.75rem;
}
.sync-change-checkbox:checked {
// background: linear-gradient(135deg, rgb(106, 227, 249), rgb(30, 150, 170));
background: rgb(90, 255, 60);
border-color: rgb(255, 255, 255);
box-shadow: 0 0 8px rgb(255, 255, 255);
}
.sync-field-name {
font-size: 1rem;
color: rgb(160, 180, 200);
font-weight: bold;
// margin-right: 1rem;
// text-transform: uppercase;
letter-spacing: 1px;
}
.sync-field-value {
font-size: 0.9rem;
color: rgb(180, 200, 220);
word-break: break-word;
line-height: 1.4;
max-width: 200px;
// white-space: nowrap;
// overflow: hidden;
// text-overflow: ellipsis;
transition: all 0.2s ease-in-out;
}
// .sync-field-value:hover {
// color: rgb(140, 230, 250);
// text-decoration: underline;
// }
.modal-sync-buttons {
display: flex;
justify-content: flex-end;
gap: 1rem;
margin-top: 1rem;
}
#sync-apply-changes,
#sync-cancel-changes {
padding: 0.4rem 1.2rem;
border: 2px solid transparent;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease-in-out;
background-color: rgb(6, 75, 92);
color: rgb(106, 227, 249);
}
#sync-apply-changes:hover {
background-color: rgb(8, 108, 132);
border-color: rgb(45, 162, 183);
// transform: scale(1.05);
}
#sync-cancel-changes {
background-color: rgb(7, 36, 43);
color: rgb(238, 84, 84);
border: 3px solid rgb(5, 60, 72);
}
#sync-cancel-changes:hover {
background-color: rgb(5, 25, 29);
border-color: rgb(21, 46, 63);
// transform: scale(1.05);
}
@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); }
}
#sync-changes-list::-webkit-scrollbar {
width: 8px;
}
#sync-changes-list::-webkit-scrollbar-track {
background: rgb(7, 36, 43);
}
#sync-changes-list::-webkit-scrollbar-thumb {
background: rgba(106, 227, 249, 0.5);
border-radius: 4px;
}
.sync-change-checkbox-container {
display: flex;
align-items: center;
}
.sync-change-arrow {
margin-top: auto;
margin-bottom: auto;
}
.sync-image {
width: 150px;
// height: 150px;
border-radius: 10px;
transition: all 0.2s ease-in-out;
}
.sync-image:hover {
transform: scale(1.4) translateY(-12px);
}
`);
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 sortContainer = document.createElement('div');
sortContainer.classList.add('d-flex', 'align-items-center', 'gap-2');
sortContainer.style.flex = 'none';
const searchInDBButton = document.createElement('button');
searchInDBButton.textContent = 'Search in Database';
searchInDBButton.classList.add('px-3', 'css-1vrq36y-custom');
searchInDBButton.style.flex = 'none';
searchInDBButton.style.marginLeft = '10px';
sortContainer.appendChild(searchInDBButton);
searchInDBButton.addEventListener('click', () => {
const query = UserInput.value.trim();
showAvatarSearchModal(query || undefined);
});
const sortLabel = document.createElement('span');
sortLabel.textContent = 'Sorting By:';
sortLabel.style.color = '#888';
sortLabel.style.marginLeft = '0.5rem';
sortLabel.style.marginRight = '0.5rem';
const sortSelect = document.createElement('select');
sortSelect.classList.add('css-1bcpvc0-custom');
sortSelect.style.width = '220px';
sortSelect.innerHTML = `
<option value="default">Date Added</option>
<option value="lastUpdated">Last Updated</option>
<option value="name">Name</option>
<option value="performance">Performance</option>
`;
const reverseButton = document.createElement('img');
reverseButton.src = 'https://img.icons8.com/?size=28&id=Ne3MRho4pubZ&format=png&color=6ae3f9';
reverseButton.alt = 'Reverse Icon';
reverseButton.classList.add('css-1vrq36y-custom');
reverseButton.style.cursor = 'pointer';
sortContainer.append(sortLabel, sortSelect, reverseButton);
SearchBar.append(UserInput, sortContainer);
let currentSort = sortSettings.sortBy;
let isReversed = sortSettings.isReversed;
sortSelect.addEventListener('change', () => {
currentSort = sortSelect.value;
sortAvatars(currentSort, isReversed);
saveSortSettings(currentSort, isReversed);
});
reverseButton.addEventListener('click', () => {
isReversed = !isReversed;
sortAvatars(currentSort, isReversed);
if (isReversed) {
reverseButton.src = 'https://img.icons8.com/?size=28&id=r1k2t6YcvxL1&format=png&color=6ae3f9';
} else {
reverseButton.src = 'https://img.icons8.com/?size=28&id=Ne3MRho4pubZ&format=png&color=6ae3f9';
}
saveSortSettings(currentSort, isReversed);
});
sortSelect.value = currentSort;
reverseButton.src = isReversed
? 'https://img.icons8.com/?size=28&id=r1k2t6YcvxL1&format=png&color=6ae3f9'
: 'https://img.icons8.com/?size=28&id=Ne3MRho4pubZ&format=png&color=6ae3f9';
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-custom', '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-custom', '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;
let avatarCreationPromises = savedAvatars.map((avatar, index) => {
return new Promise(resolve => {
setTimeout(() => {
createAvatarCard(avatar);
resolve();
}, index * delayBetweenCards);
});
});
await Promise.all(avatarCreationPromises);
updateCounter();
sortAvatars(currentSort, isReversed);
isCustomFavoritesMenuOpen = true;
} catch (error) {
showNotification(`Error Message: ${error}`, 'error');
}
}
function parseCustomDate(dateString) {
if (!dateString) return new Date(0);
const dateParts = dateString.split(', ');
if (dateParts.length === 2) {
const [date, time] = dateParts;
const [day, month, year] = date.split('.').map(Number);
const [hours, minutes, seconds] = time.split(':').map(Number);
return new Date(year, month - 1, day, hours, minutes, seconds);
} else if (dateString.includes('.')) {
const [day, month, year] = dateString.split('.').map(Number);
return new Date(year, month - 1, day);
} else if (dateString.includes('-')) {
return new Date(dateString);
}
return new Date(dateString);
}
function sortAvatars(sortBy, reverse) {
const avatarsContainer = document.getElementById('custom-avatars');
const avatars = Array.from(avatarsContainer.children);
avatars.sort((a, b) => {
const avatarA = savedAvatars.find(av => av.avatarID === a.dataset.avatarId);
const avatarB = savedAvatars.find(av => av.avatarID === b.dataset.avatarId);
if (!avatarA || !avatarB) return 0;
let comparison = 0;
switch (sortBy) {
case 'default':
const dateA = parseCustomDate(avatarA.dateAdded);
const dateB = parseCustomDate(avatarB.dateAdded);
comparison = dateB - dateA;
break;
case 'lastUpdated':
const updatedA = parseCustomDate(avatarA.updated_at);
const updatedB = parseCustomDate(avatarB.updated_at);
comparison = updatedB - updatedA;
break;
case 'name':
comparison = avatarA.avatarName.localeCompare(avatarB.avatarName);
break;
case 'performance':
const order = { 'Excellent': 5, 'Good': 4, 'Medium': 3, 'Poor': 2, 'VeryPoor': 1, 'None': 0 };
comparison = (order[avatarB.PC_Performance] || 0) - (order[avatarA.PC_Performance] || 0);
break;
}
return reverse ? -comparison : comparison;
});
avatarsContainer.innerHTML = '';
avatars.forEach(avatar => avatarsContainer.appendChild(avatar));
}
function saveSortSettings(currentSort, isReversed) {
sortSettings = { sortBy: currentSort, isReversed: isReversed };
GM_setValue('sortSettings', sortSettings);
}
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;
let currentTime = new Date().toLocaleString();
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,
dateAdded: currentTime
};
} 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,
dateAdded: currentTime
};
}
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 syncContainer = document.createElement('div');
syncContainer.classList.add('sync');
linkElement.appendChild(syncContainer);
const syncImage = document.createElement('img');
syncImage.src = 'https://img.icons8.com/?size=28&id=YyqIYbMdZ1i5&format=png&color=6ae3f9';
syncImage.classList.add('css-1vrq36y-custom', 'syncButton');
syncImage.addEventListener('click', async (event) => {
event.stopPropagation();
event.preventDefault();
await syncAvatar(avatar.avatarID);
});
syncContainer.appendChild(syncImage);
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');
if (avatar.isUnavalibleAvatar && !avatar.authorName) {
imgElement.src = 'https://assets.vrchat.com/default/unavailable-avatar.png';
} else{
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}`;
avatarH4.title = `Description:\n${avatar.description}`;
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.style.marginRight = '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();
sortAvatars(sortSettings.sortBy, sortSettings.isReversed);
}
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');
}
async function syncAvatar(avatarID) {
try {
const { response } = await SendRequest('GET', `https://api.vrchat.cloud/api/1/avatars/${avatarID}`, authCookie);
const newData = JSON.parse(response);
const savedAvatar = savedAvatars.find(a => a.avatarID === avatarID);
let changes = [];
let hasPC = false;
let hasQuest = false;
let pcPerformance = '';
let questPerformance = '';
if (Array.isArray(newData.unityPackages) && newData.unityPackages.length > 0) {
newData.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;
}
});
}
if (savedAvatar.authorName !== newData.authorName) {
changes.push({
field: 'authorName',
oldVal: savedAvatar.authorName,
newVal: newData.authorName,
apply: false
});
}
if (savedAvatar.authorId !== newData.authorId) {
changes.push({
field: 'authorId',
oldVal: savedAvatar.authorId,
newVal: newData.authorId,
apply: false
});
}
if(savedAvatar.created_at !== FixDisplayTime(newData.created_at)) {
changes.push({
field: 'created_at',
oldVal: savedAvatar.created_at,
newVal: FixDisplayTime(newData.created_at),
apply: false
});
}
if (savedAvatar.description !== newData.description) {
changes.push({
field: 'description',
oldVal: savedAvatar.description,
newVal: newData.description,
apply: false
});
}
if (savedAvatar.avatarID !== newData.id) {
changes.push({
field: 'avatarID',
oldVal: savedAvatar.avatarID,
newVal: newData.id,
apply: false
});
}
if (savedAvatar.imageUrl !== newData.imageUrl) {
changes.push({
field: 'imageUrl',
oldVal: savedAvatar.imageUrl,
newVal: newData.imageUrl,
apply: false
});
}
if(savedAvatar.avatarName !== newData.name) {
changes.push({
field: 'avatarName',
oldVal: savedAvatar.avatarName,
newVal: newData.name,
apply: false
});
}
if (savedAvatar.releaseStatus !== newData.releaseStatus) {
changes.push({
field: 'releaseStatus',
oldVal: savedAvatar.releaseStatus,
newVal: newData.releaseStatus,
apply: false
});
}
if(savedAvatar.updated_at !== FixDisplayTime(newData.updated_at)) {
changes.push({
field: 'updated_at',
oldVal: savedAvatar.updated_at,
newVal: FixDisplayTime(newData.updated_at),
apply: false
});
}
if(savedAvatar.version !== newData.version) {
changes.push({
field: 'version',
oldVal: savedAvatar.version,
newVal: newData.version,
apply: false
});
}
if(savedAvatar.isPlatformPC !== hasPC) {
changes.push({
field: 'isPlatformPC',
oldVal: savedAvatar.isPlatformPC,
newVal: hasPC,
apply: false
});
}
if(savedAvatar.PC_Performance !== pcPerformance) {
changes.push({
field: 'PC_Performance',
oldVal: savedAvatar.PC_Performance,
newVal: pcPerformance,
apply: false
});
}
if(savedAvatar.isPlatformQuest !== hasQuest) {
changes.push({
field: 'isPlatformQuest',
oldVal: savedAvatar.isPlatformQuest,
newVal: hasQuest,
apply: false
});
}
if (savedAvatar.Quest_Performance !== questPerformance) {
changes.push({
field: 'Quest_Performance',
oldVal: savedAvatar.Quest_Performance,
newVal: questPerformance,
apply: false
});
}
if (changes.length === 0) {
showNotification('No changes detected!', 'success');
return;
}
showSyncConfirmation(changes, avatarID);
} catch (error) {
showNotification(`Error during sync: ${error.message}`, 'error');
}
}
function showSyncConfirmation(changes, avatarID) {
const modal = document.createElement('div');
modal.className = 'sync-modal-overlay';
modal.innerHTML = `
<div class="modal-sync" style="width: 600px">
<h3 class="modal-sync-title">Changes detected</h3>
<div id="sync-changes-list""></div>
<div class="modal-sync-buttons">
<button id="sync-apply-changes">Apply selected</button>
<button id="sync-cancel-changes">Cancel</button>
</div>
</div>
`;
const changesList = modal.querySelector('#sync-changes-list');
changes.forEach(change => {
const div = document.createElement('div');
div.className = 'sync-change-item';
if (change.field === 'imageUrl') {
div.innerHTML = `
<label class="sync-change-label">
<div class="sync-change-checkbox-container" style="margin-bottom: 0.5rem;">
<input type="checkbox" class="sync-change-checkbox">
<span class="sync-field-name">${change.field}</span>
</div>
<div class="sync-change-checkbox-container" style="justify-content: space-around; align-items: center;">
<div class="sync-field-value">
${change.oldVal ? `<img src="${change.oldVal}" alt="Old Image" class="sync-image"">` : 'No image'}
</div>
<p class="sync-change-arrow">➤</p>
<div class="sync-field-value">
${change.newVal ? `<img src="${change.newVal}" alt="New Image" class="sync-image">` : 'No image'}
</div>
</div>
</label>
`;
} else {
div.innerHTML = `
<label class="sync-change-label">
<div class="sync-change-checkbox-container" style="margin-bottom: 0.5rem;">
<input type="checkbox" class="sync-change-checkbox">
<span class="sync-field-name">${change.field}</span>
</div>
<div class="sync-change-checkbox-container" style="justify-content: space-around;">
<div class="sync-field-value">${change.oldVal}</div>
<p class="sync-change-arrow">➤</p>
<div class="sync-field-value">${change.newVal}</div>
</div>
</label>
`;
}
changesList.appendChild(div);
});
document.body.appendChild(modal);
modal.querySelector('#sync-apply-changes').addEventListener('click', () => {
const selected = Array.from(modal.querySelectorAll('.sync-change-checkbox:checked'))
.map(cb => changes[Array.from(changesList.children).indexOf(cb.closest('.sync-change-item'))]);
if (selected.length === 0) {
showNotification('Select somethig first!', 'warning');
return;
}
const avatarIndex = savedAvatars.findIndex(a => a.avatarID === avatarID);
selected.forEach(change => {
savedAvatars[avatarIndex][change.field] = change.newVal;
});
GM_setValue('savedAvatars', savedAvatars);
updateAvatarCard(avatarID);
handleClose();
});
const handleClose = () => {
modal.classList.add('exit');
setTimeout(() => {
modal.remove();
}, 200);
};
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
handleClose();
}
});
modal.querySelector('#sync-cancel-changes').addEventListener('click', () => {
handleClose();
});
}
function updateAvatarCard(avatarID) {
const avatarCard = document.querySelector(`[data-avatar-id="${avatarID}"]`);
if (avatarCard) {
avatarCard.remove();
const avatarData = savedAvatars.find(a => a.avatarID === avatarID);
createAvatarCard(avatarData);
showNotification('Avatar Updated!', '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);
};
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
handleClose();
}
});
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();
});
inputField.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
saveBtn.click();
}
});
cancelBtn.addEventListener('click', () => {
handleClose();
});
modal.addEventListener('click', (event) => {
const modalContent = modal.querySelector('.modal-content');
const modalRect = modalContent.getBoundingClientRect();
const safeZone = 150;
const isClickOutsideSafeZone =
event.clientX < modalRect.left - safeZone ||
event.clientX > modalRect.right + safeZone ||
event.clientY < modalRect.top - safeZone ||
event.clientY > modalRect.bottom + safeZone;
if (isClickOutsideSafeZone) {
handleClose();
}
});
}
let allAvatars = [];
let totalPages = 0;
let currentPage = 1;
const avatarsPerPage = 20;
function showAvatarSearchModal(initialQuery = '') {
const modal = document.createElement('div');
modal.id = 'avatar-search-modal';
modal.classList.add('modal-overlay');
modal.innerHTML = `
<div class="modal-content" id="avatar-search-modal-content">
<h3>Search Avatars</h3>
<div style="display: flex; align-items: center;">
<input
type="text"
id="search-input"
placeholder="Enter search query"
value="${initialQuery}"
style="width: 100%; height: 2.5rem; padding: 0.75rem; margin-bottom: 1rem; margin-right: 10px; border: 2px solid rgb(6, 75, 92); border-radius: 4px; background: rgb(7, 36, 43); color: #e0e0e0;"
>
<button
id="search-btn"
class="css-1vrq36y-custom"
style="margin: 0 auto 1rem; height: 40px; padding: 0rem 1.5rem; border-radius: 4px; background: rgb(6, 75, 92);"
>
Search
</button>
</div>
<div id="pagination-container"></div>
<div id="search-results" style="max-height: 400px; overflow-y: auto;">The results will appear here</div>
<button id="close-modal-btn">Close</button>
</div>
`;
document.body.appendChild(modal);
const searchInput = modal.querySelector('#search-input');
const searchBtn = modal.querySelector('#search-btn');
const resultsContainer = modal.querySelector('#search-results');
const closeModalBtn = modal.querySelector('#close-modal-btn');
searchInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') searchBtn.click();
});
const handleClose = () => {
modal.classList.add('exit');
setTimeout(() => modal.remove(), 200);
};
closeModalBtn.addEventListener('click', handleClose);
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
handleClose();
}
});
modal.addEventListener('click', (event) => {
const modalContent = modal.querySelector('.modal-content');
const modalRect = modalContent.getBoundingClientRect();
const safeZone = 150;
const isClickOutsideSafeZone =
event.clientX < modalRect.left - safeZone ||
event.clientX > modalRect.right + safeZone ||
event.clientY < modalRect.top - safeZone ||
event.clientY > modalRect.bottom + safeZone;
if (isClickOutsideSafeZone) {
handleClose();
}
});
searchBtn.addEventListener('click', async () => {
const query = searchInput.value.trim();
if (!query) {
showNotification('Please enter a search query', 'warning');
return;
}
allAvatars = [];
performAvatarSearch(query, resultsContainer, 1);
});
if (initialQuery) {
performAvatarSearch(initialQuery, resultsContainer, 1);
}
}
async function performAvatarSearch(query, container, page = 1) {
if (container) {
container.scrollTo({
top: 0,
behavior: 'smooth'
});
}
if (!query.trim()) {
showNotification('Please enter a search query', 'warning');
return;
}
if (allAvatars.length === 0) {
try {
const { response } = await SendRequest('GET', `https://vrcx.vrcdb.com/avatars/Avatar/VRCX?search=${encodeURIComponent(query)}`);
const data = JSON.parse(response);
if (data.length === 0) {
container.innerHTML = '<p style="margin-top: inherit;">No results found</p>';
return;
}
allAvatars = data;
totalPages = Math.ceil(allAvatars.length / avatarsPerPage);
} catch (error) {
showNotification(`Error while searching: ${error.message}`, 'error');
return;
}
}
container.innerHTML = '';
const startIdx = (page - 1) * avatarsPerPage;
const endIdx = startIdx + avatarsPerPage;
const currentAvatars = allAvatars.slice(startIdx, endIdx);
currentAvatars.forEach(avatar => {
const avatarCard = document.createElement('div');
avatarCard.classList.add('search-result-card');
avatarCard.style.display = 'flex';
avatarCard.style.alignItems = 'center';
avatarCard.style.marginBottom = '10px';
avatarCard.style.width = '95%';
const img = document.createElement('img');
img.src = avatar.imageUrl || 'https://assets.vrchat.com/default/unavailable-avatar.png';
avatarCard.appendChild(img);
const infoDiv = document.createElement('div');
infoDiv.style.flexGrow = 1;
infoDiv.style.width = 'min-content';
const name = document.createElement('h4');
name.textContent = avatar.avatarName;
name.title = `Description:\n${avatar.description}`;
name.style.margin = '0';
infoDiv.appendChild(name);
const author = document.createElement('p');
author.textContent = `Created by: ${avatar.authorName}`;
author.style.margin = '0';
author.style.fontSize = '0.9em';
author.style.color = '#aaa';
infoDiv.appendChild(author);
avatarCard.appendChild(infoDiv);
const addButton = document.createElement('button');
addButton.textContent = 'Add to Favorites';
addButton.classList.add('css-1vrq36y-custom');
addButton.addEventListener('click', () => {
AddAvatar(addMetod.ID, avatar.id);
showNotification(`Added ${avatar.avatarName} to favorites`, 'success');
});
avatarCard.appendChild(addButton);
container.appendChild(avatarCard);
});
addPaginationButtons(container, totalPages, page);
}
function addPaginationButtons(container, totalPages, currentPage) {
const paginationContainer = document.getElementById('pagination-container');
paginationContainer.innerHTML = '';
const createButton = (text, disabled, onClick) => {
const btn = document.createElement('button');
btn.textContent = text;
btn.classList.add('css-1vrq36y-custom');
btn.style.margin = '0 5px';
btn.disabled = disabled;
btn.addEventListener('click', onClick);
return btn;
};
paginationContainer.appendChild(createButton('Previous', currentPage === 1, () => {
performAvatarSearch(document.getElementById('search-input').value.trim(), container, currentPage - 1);
}));
const pageInfo = document.createElement('span');
pageInfo.textContent = `Page ${currentPage} of ${totalPages}`;
pageInfo.style.margin = '0 10px';
paginationContainer.appendChild(pageInfo);
paginationContainer.appendChild(createButton('Next', currentPage >= totalPages, () => {
performAvatarSearch(document.getElementById('search-input').value.trim(), container, currentPage + 1);
}));
}