// ==UserScript==
// @name Enhanced VRChat Website Tools
// @namespace http://tampermonkey.net/
// @version 2024-04-10
// @description Modern UI overlay with improved avatar search and tools
// @author Snoofz
// @match https://vrchat.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=vrchat.com
// @grant none
// ==/UserScript==
(function() {
'use strict';
// Icon paths using Font Awesome paths
const iconMaps = {
"star": "M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z",
"world": "M352 256c0 22.2-1.2 43.6-3.3 64H163.3c-2.2-20.4-3.3-41.8-3.3-64s1.2-43.6 3.3-64H348.7c2.2 20.4 3.3 41.8 3.3 64zm28.8-64H503.9c5.3 20.5 8.1 41.9 8.1 64s-2.8 43.5-8.1 64H380.8c2.1-20.6 3.2-42 3.2-64s-1.1-43.4-3.2-64zm112.6-32H376.7c-10-63.9-29.8-117.4-55.3-151.6c78.3 20.7 142 77.5 171.9 151.6zm-149.1 0H167.7c6.1-36.4 15.5-68.6 27-94.7c10.5-23.6 22.2-40.7 33.5-51.5C239.4 3.2 248.7 0 256 0s16.6 3.2 27.8 13.8c11.3 10.8 23 27.9 33.5 51.5c11.6 26 20.9 58.2 27 94.7zm-209 0H18.6C48.6 85.9 112.2 29.1 190.6 8.4C165.1 42.6 145.3 96.1 135.3 160zM8.1 192H131.2c-2.1 20.6-3.2 42-3.2 64s1.1 43.4 3.2 64H8.1C2.8 299.5 0 278.1 0 256s2.8-43.5 8.1-64zM194.7 446.6c-11.6-26-20.9-58.2-27-94.6H344.3c-6.1 36.4-15.5 68.6-27 94.6c-10.5 23.6-22.2 40.7-33.5 51.5C272.6 508.8 263.3 512 256 512s-16.6-3.2-27.8-13.8c-11.3-10.8-23-27.9-33.5-51.5zM135.3 352c10 63.9 29.8 117.4 55.3 151.6C112.2 482.9 48.6 426.1 18.6 352H135.3zm358.1 0c-30 74.1-93.6 130.9-171.9 151.6c25.5-34.2 45.2-87.7 55.3-151.6H493.4z",
"home": "M575.8 255.5c0 18-15 32.1-32 32.1h-32l.7 160.2c0 2.7-.2 5.4-.5 8.1V472c0 22.1-17.9 40-40 40H456c-1.1 0-2.2 0-3.3-.1c-1.4 .1-2.8 .1-4.2 .1H416 392c-22.1 0-40-17.9-40-40V448 384c0-17.7-14.3-32-32-32H256c-17.7 0-32 14.3-32 32v64 24c0 22.1-17.9 40-40 40H160 128.1c-1.5 0-3-.1-4.5-.2c-1.2 .1-2.4 .2-3.6 .2H104c-22.1 0-40-17.9-40-40V360c0-.9 0-1.9 .1-2.8V287.6H32c-18 0-32-14-32-32.1c0-9 3-17 10-24L266.4 8c7-7 15-8 22-8s15 2 21 7L564.8 231.5c8 7 12 15 11 24z",
"search": "M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z",
"cogwheel": "M308.5 135.3c7.1-6.3 9.9-16.2 6.2-25c-2.3-5.3-4.8-10.5-7.6-15.5L304 89.4c-3-5-6.3-9.9-9.8-14.6c-5.7-7.6-15.7-10.1-24.7-7.1l-28.2 9.3c-10.7-8.8-23-16-36.2-20.9L199 27.1c-1.9-9.3-9.1-16.7-18.5-17.8C173.9 8.4 167.2 8 160.4 8h-.7c-6.8 0-13.5 .4-20.1 1.2c-9.4 1.1-16.6 8.6-18.5 17.8L115 56.1c-13.3 5-25.5 12.1-36.2 20.9L50.5 67.8c-9-3-19-.5-24.7 7.1c-3.5 4.7-6.8 9.6-9.9 14.6l-3 5.3c-2.8 5-5.3 10.2-7.6 15.6c-3.7 8.7-.9 18.6 6.2 25l22.2 19.8C32.6 161.9 32 168.9 32 176s.6 14.1 1.7 20.9L11.5 216.7c-7.1 6.3-9.9 16.2-6.2 25c2.3 5.3 4.8 10.5 7.6 15.6l3 5.2c3 5.1 6.3 9.9 9.9 14.6c5.7 7.6 15.7 10.1 24.7 7.1l28.2-9.3c10.7 8.8 23 16 36.2 20.9l6.1 29.1c1.9 9.3 9.1 16.7 18.5 17.8c6.7 .8 13.5 1.2 20.4 1.2s13.7-.4 20.4-1.2c9.4-1.1 16.6-8.6 18.5-17.8l6.1-29.1c13.3-5 25.5-12.1 36.2-20.9l28.2 9.3c9 3 19 .5 24.7-7.1c3.5-4.7 6.8-9.5 9.8-14.6l3.1-5.4c2.8-5 5.3-10.2 7.6-15.5c3.7-8.7 .9-18.6-6.2-25l-22.2-19.8c1.1-6.8 1.7-13.8 1.7-20.9s-.6-14.1-1.7-20.9l22.2-19.8zM112 176a48 48 0 1 1 96 0 48 48 0 1 1 -96 0zM504.7 500.5c6.3 7.1 16.2 9.9 25 6.2c5.3-2.3 10.5-4.8 15.5-7.6l5.4-3.1c5-3 9.9-6.3 14.6-9.8c7.6-5.7 10.1-15.7 7.1-24.7l-9.3-28.2c8.8-10.7 16-23 20.9-36.2l29.1-6.1c9.3-1.9 16.7-9.1 17.8-18.5c.8-6.7 1.2-13.5 1.2-20.4s-.4-13.7-1.2-20.4c-1.1-9.4-8.6-16.6-17.8-18.5L583.9 307c-5-13.3-12.1-25.5-20.9-36.2l9.3-28.2c3-9 .5-19-7.1-24.7c-4.7-3.5-9.6-6.8-14.6-9.9l-5.3-3c-5-2.8-10.2-5.3-15.6-7.6c-8.7-3.7-18.6-.9-25 6.2l-19.8 22.2c-6.8-1.1-13.8-1.7-20.9-1.7s-14.1 .6-20.9 1.7l-19.8-22.2c-6.3-7.1-16.2-9.9-25-6.2c-5.3 2.3-10.5 4.8-15.6 7.6l-5.2 3c-5.1 3-9.9 6.3-14.6 9.9c-7.6 5.7-10.1 15.7-7.1 24.7l9.3 28.2c-8.8 10.7-16 23-20.9 36.2L315.1 313c-9.3 1.9-16.7 9.1-17.8 18.5c-.8 6.7-1.2 13.5-1.2 20.4s.4 13.7 1.2 20.4c1.1 9.4 8.6 16.6 17.8 18.5l29.1 6.1c5 13.3 12.1 25.5 20.9 36.2l-9.3 28.2c-3 9-.5 19 7.1 24.7c4.7 3.5 9.5 6.8 14.6 9.8l5.4 3.1c5 2.8 10.2 5.3 15.5 7.6c8.7 3.7 18.6 .9 25-6.2l19.8-22.2c6.8 1.1 13.8 1.7 20.9 1.7s14.1-.6 20.9-1.7l19.8 22.2zM464 304a48 48 0 1 1 0 96 48 48 0 1 1 0-96z",
"shield": "M256 0c4.6 0 9.2 1 13.4 2.9L457.7 82.8c22 9.3 38.4 31 38.3 57.2c-.5 99.2-41.3 280.7-213.6 363.2c-16.7 8-36.1 8-52.8 0C57.3 420.7 16.5 239.2 16 140c-.1-26.2 16.3-47.9 38.3-57.2L242.7 2.9C246.8 1 251.4 0 256 0zm0 66.8V444.8C394 378 431.1 230.1 432 141.4L256 66.8l0 0z",
"envelope": "M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48H48zM0 176V384c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V176L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z",
"user": "M256 288A144 144 0 1 0 256 0a144 144 0 1 0 0 288zm-94.7 32C72.7 320 0 392.7 0 481.3c0 17 13.8 30.7 30.7 30.7H481.3c17 0 30.7-13.8 30.7-30.7C512 392.7 439.3 320 350.7 320H161.3z",
"times": "M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"
};
// WebSocket setup
let ws = null;
let messageCount = 0;
let displayName = "";
let trustRankColor = "";
let trustRankName = "";
// CSS for the new UI
const modalCSS = `
.vrc-tools-modal {
position: fixed;
top: 100px;
left: 100px;
width: 800px;
background-color: #1a1a1a;
border-radius: 8px;
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.5);
z-index: 9999;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: #f0f0f0;
overflow: hidden;
resize: both;
min-width: 400px;
min-height: 300px;
max-width: 1200px;
max-height: 800px;
border: 1px solid #333;
}
.vrc-tools-header {
background-color: #2a2a2a;
padding: 12px 20px;
cursor: move;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #333;
}
.vrc-tools-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #fff;
}
.vrc-tools-close {
background: none;
border: none;
color: #aaa;
font-size: 20px;
cursor: pointer;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
border-radius: 50%;
transition: background-color 0.2s;
}
.vrc-tools-close:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
}
.vrc-tools-tabs {
display: flex;
background-color: #2a2a2a;
border-bottom: 1px solid #333;
}
.vrc-tools-tab {
padding: 12px 20px;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s, background-color 0.2s;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
}
.vrc-tools-tab.active {
opacity: 1;
background-color: #333;
border-bottom: 2px solid #5865f2;
}
.vrc-tools-tab:hover:not(.active) {
opacity: 0.9;
background-color: #333;
}
.vrc-tools-tab-icon {
width: 16px;
height: 16px;
}
.vrc-tools-content {
padding: 20px;
height: calc(100% - 120px);
overflow-y: auto;
}
.vrc-tools-tab-content {
display: none;
height: 100%;
}
.vrc-tools-tab-content.active {
display: block;
}
.vrc-tools-search-container {
margin-bottom: 20px;
}
.vrc-tools-search-input {
width: 100%;
padding: 10px 15px;
border-radius: 5px;
border: 1px solid #444;
background-color: #222;
color: #fff;
font-size: 16px;
outline: none;
transition: border-color 0.2s;
}
.vrc-tools-search-input:focus {
border-color: #5865f2;
}
.vrc-tools-btn {
background-color: #5865f2;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s;
margin-right: 10px;
margin-bottom: 10px;
}
.vrc-tools-btn:hover {
background-color: #4752c4;
}
.vrc-tools-btn.secondary {
background-color: #4f545c;
}
.vrc-tools-btn.secondary:hover {
background-color: #5d6269;
}
.vrc-tools-btn.danger {
background-color: #ed4245;
}
.vrc-tools-btn.danger:hover {
background-color: #c03537;
}
.vrc-tools-avatar-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
margin-top: 20px;
}
.vrc-tools-avatar-card {
background-color: #2a2a2a;
border-radius: 8px;
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
border: 1px solid #333;
}
.vrc-tools-avatar-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
}
.vrc-tools-avatar-thumbnail {
width: 100%;
aspect-ratio: 1/1;
object-fit: cover;
display: block;
}
.vrc-tools-avatar-info {
padding: 15px;
}
.vrc-tools-avatar-name {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 600;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.vrc-tools-avatar-author {
margin: 0;
font-size: 14px;
color: #aaa;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.vrc-tools-avatar-actions {
display: flex;
margin-top: 10px;
gap: 8px;
}
.vrc-tools-avatar-btn {
flex: 1;
padding: 6px 0;
font-size: 13px;
background-color: #36393f;
border: none;
border-radius: 4px;
color: #fff;
cursor: pointer;
transition: background-color 0.2s;
}
.vrc-tools-avatar-btn:hover {
background-color: #40444b;
}
.vrc-tools-avatar-btn.primary {
background-color: #5865f2;
}
.vrc-tools-avatar-btn.primary:hover {
background-color: #4752c4;
}
.vrc-tools-settings-section {
margin-bottom: 30px;
}
.vrc-tools-settings-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 15px;
color: #fff;
border-bottom: 1px solid #333;
padding-bottom: 10px;
}
.vrc-tools-settings-option {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.vrc-tools-settings-label {
flex: 1;
font-size: 15px;
}
.vrc-tools-settings-description {
color: #aaa;
font-size: 13px;
margin-top: 5px;
}
/* Chat styles */
.vrc-tools-chat-container {
height: 100%;
display: flex;
flex-direction: column;
}
.vrc-tools-chat-log {
flex: 1;
overflow-y: auto;
padding: 15px;
background-color: #2a2a2a;
border-radius: 8px;
margin-bottom: 15px;
}
.vrc-tools-chat-message {
margin-bottom: 10px;
line-height: 1.5;
}
.vrc-tools-chat-input-container {
display: flex;
gap: 10px;
}
.vrc-tools-chat-input {
flex: 1;
padding: 12px 15px;
border-radius: 5px;
border: 1px solid #444;
background-color: #222;
color: #fff;
font-size: 16px;
outline: none;
}
.vrc-tools-chat-input:focus {
border-color: #5865f2;
}
.vrc-tools-chat-send {
padding: 0 20px;
background-color: #5865f2;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.vrc-tools-chat-send:hover {
background-color: #4752c4;
}
/* Toggle button styles */
.vrc-tools-toggle {
position: fixed;
bottom: 20px;
right: 20px;
width: 50px;
height: 50px;
background-color: #5865f2;
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3);
z-index: 9998;
transition: background-color 0.2s;
}
.vrc-tools-toggle:hover {
background-color: #4752c4;
}
.vrc-tools-toggle svg {
width: 24px;
height: 24px;
}
/* Loading indicator */
.vrc-tools-loading {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.vrc-tools-spinner {
border: 4px solid rgba(255, 255, 255, 0.1);
border-radius: 50%;
border-top: 4px solid #5865f2;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Toast notification */
.vrc-tools-toast {
position: fixed;
top: 20px;
right: 20px;
background-color: #2a2a2a;
color: white;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
z-index: 9999;
display: flex;
align-items: center;
gap: 12px;
max-width: 350px;
border-left: 4px solid #5865f2;
animation: slideIn 0.3s ease-out forwards;
}
.vrc-tools-toast.error {
border-left-color: #ed4245;
}
.vrc-tools-toast.success {
border-left-color: #43b581;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
.vrc-tools-toast-icon {
flex-shrink: 0;
}
.vrc-tools-toast-content {
flex: 1;
}
.vrc-tools-toast-title {
font-weight: 600;
margin-bottom: 5px;
}
.vrc-tools-toast-close {
background: none;
border: none;
color: #aaa;
cursor: pointer;
padding: 0;
font-size: 18px;
transition: color 0.2s;
}
.vrc-tools-toast-close:hover {
color: white;
}
`;
async function fetchWorldData() {
try {
// This would normally fetch from the API, but we'll use the provided data
const response = await fetch("https://vrchat.com/api/1/worlds?avatarSpecific=false&maxUnityVersion=2022.3.22f1&releaseStatus=public&organization=vrchat&sort=shuffle&featured=false&tag=system_approved&order=descending&n=50&offset=0");
const data = await response.json();
return data;
} catch (error) {
console.error("Error fetching world data:", error);
return [];
}
}
// Function to display worlds in the World grid
function displayWorlds(worlds, worldGrid) {
worldGrid.innerHTML = '';
if (worlds.length === 0) {
worldGrid.innerHTML = '<p>No worlds found.</p>';
return;
}
worlds.forEach(world => {
const card = document.createElement('div');
card.className = 'vrc-tools-avatar-card';
const img = document.createElement('img');
img.className = 'vrc-tools-avatar-thumbnail';
img.src = world.thumbnailImageUrl || world.imageUrl || 'https://via.placeholder.com/300?text=No+Image';
img.alt = world.name;
const info = document.createElement('div');
info.className = 'vrc-tools-avatar-info';
const name = document.createElement('h4');
name.className = 'vrc-tools-avatar-name';
name.textContent = world.name;
const author = document.createElement('p');
author.className = 'vrc-tools-avatar-author';
author.textContent = `By: ${world.authorName}`;
const details = document.createElement('p');
details.style.fontSize = '12px';
details.style.color = '#aaa';
details.textContent = `Capacity: ${world.capacity} | Favorites: ${world.favorites}`;
const actions = document.createElement('div');
actions.className = 'vrc-tools-avatar-actions';
const viewButton = document.createElement('button');
viewButton.className = 'vrc-tools-avatar-btn primary';
viewButton.textContent = 'View World';
viewButton.addEventListener('click', () => {
window.open(`https://vrchat.com/home/world/${world.id}`, '_blank');
});
actions.appendChild(viewButton);
info.appendChild(name);
info.appendChild(author);
info.appendChild(details);
info.appendChild(actions);
card.appendChild(img);
card.appendChild(info);
worldGrid.appendChild(card);
});
}
// Create and inject the CSS
function injectCSS() {
const style = document.createElement('style');
style.textContent = modalCSS;
document.head.appendChild(style);
}
// Create SVG icon
function createSVGIcon(pathData, className = '') {
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("aria-hidden", "true");
svg.setAttribute("focusable", "false");
svg.setAttribute("role", "presentation");
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
svg.setAttribute("viewBox", "0 0 512 512");
if (className) {
svg.setAttribute("class", className);
}
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("fill", "currentColor");
path.setAttribute("d", pathData);
svg.appendChild(path);
return svg;
}
let worldSearchInput;
// Create the modal
function createModal() {
const modal = document.createElement('div');
modal.className = 'vrc-tools-modal';
modal.style.display = 'none';
// Create the header
const header = document.createElement('div');
header.className = 'vrc-tools-header';
const title = document.createElement('h2');
title.className = 'vrc-tools-title';
title.textContent = 'VRChat Enhanced Tools';
const closeButton = document.createElement('button');
closeButton.className = 'vrc-tools-close';
closeButton.appendChild(createSVGIcon(iconMaps["times"]));
header.appendChild(title);
header.appendChild(closeButton);
// Create tabs
const tabs = document.createElement('div');
tabs.className = 'vrc-tools-tabs';
const tabsConfig = [
{ id: 'avatars', title: 'Avatar Search', icon: 'search' },
{ id: 'chat', title: 'Live Chat', icon: 'envelope' },
{ id: 'world', title: 'World Browser', icon: 'world' },
{ id: 'settings', title: 'Settings', icon: 'cogwheel' }
];
tabsConfig.forEach((tabConfig, index) => {
const tab = document.createElement('div');
tab.className = 'vrc-tools-tab' + (index === 0 ? ' active' : '');
tab.dataset.tab = tabConfig.id;
tab.appendChild(createSVGIcon(iconMaps[tabConfig.icon], 'vrc-tools-tab-icon'));
const tabTitle = document.createElement('span');
tabTitle.textContent = tabConfig.title;
tab.appendChild(tabTitle);
tabs.appendChild(tab);
});
// Create content container
const contentContainer = document.createElement('div');
contentContainer.className = 'vrc-tools-content';
// Create tab contents
const avatarsContent = document.createElement('div');
avatarsContent.className = 'vrc-tools-tab-content active';
avatarsContent.dataset.tab = 'avatars';
const chatContent = document.createElement('div');
chatContent.className = 'vrc-tools-tab-content';
chatContent.dataset.tab = 'chat';
const worldContent = document.createElement('div');
worldContent.className = 'vrc-tools-tab-content';
worldContent.dataset.tab = 'world';
const settingsContent = document.createElement('div');
settingsContent.className = 'vrc-tools-tab-content';
settingsContent.dataset.tab = 'settings';
// Avatar Search Content
const searchContainer = document.createElement('div');
searchContainer.className = 'vrc-tools-search-container';
const searchInput = document.createElement('input');
searchInput.className = 'vrc-tools-search-input';
searchInput.type = 'text';
searchInput.placeholder = 'Search avatars by name...';
searchContainer.appendChild(searchInput);
const searchButtonContainer = document.createElement('div');
searchButtonContainer.style.marginTop = '15px';
searchButtonContainer.style.display = 'flex';
searchButtonContainer.style.flexWrap = 'wrap';
const searchButton = document.createElement('button');
searchButton.className = 'vrc-tools-btn';
searchButton.textContent = 'Search';
const clearButton = document.createElement('button');
clearButton.className = 'vrc-tools-btn secondary';
clearButton.textContent = 'Clear';
searchButtonContainer.appendChild(searchButton);
searchButtonContainer.appendChild(clearButton);
const avatarGrid = document.createElement('div');
avatarGrid.className = 'vrc-tools-avatar-grid';
avatarsContent.appendChild(searchContainer);
avatarsContent.appendChild(searchButtonContainer);
avatarsContent.appendChild(avatarGrid);
// Chat Content
const chatContainer = document.createElement('div');
chatContainer.className = 'vrc-tools-chat-container';
const chatLog = document.createElement('div');
chatLog.className = 'vrc-tools-chat-log';
chatLog.id = 'chatLog';
const chatInputContainer = document.createElement('div');
chatInputContainer.className = 'vrc-tools-chat-input-container';
const chatInput = document.createElement('input');
chatInput.className = 'vrc-tools-chat-input';
chatInput.id = 'chatInput';
chatInput.type = 'text';
chatInput.placeholder = 'Type your message...';
const chatSendButton = document.createElement('button');
chatSendButton.className = 'vrc-tools-chat-send';
chatSendButton.textContent = 'Send';
chatInputContainer.appendChild(chatInput);
chatInputContainer.appendChild(chatSendButton);
chatContainer.appendChild(chatLog);
chatContainer.appendChild(chatInputContainer);
chatContent.appendChild(chatContainer);
// World Browser Content
const worldSearchContainer = document.createElement('div');
worldSearchContainer.className = 'vrc-tools-search-container';
worldSearchInput = document.createElement('input');
worldSearchInput.className = 'vrc-tools-search-input';
worldSearchInput.type = 'text';
worldSearchInput.placeholder = 'Search worlds by name...';
worldSearchContainer.appendChild(worldSearchInput);
window.worldGrid = document.createElement('div');
worldGrid.className = 'vrc-tools-avatar-grid';
worldGrid.innerHTML = '<div class="vrc-tools-loading"><div class="vrc-tools-spinner"></div></div>';
worldContent.appendChild(worldSearchContainer);
worldContent.appendChild(worldGrid);
// Settings Content
const settingsSection = document.createElement('div');
settingsSection.className = 'vrc-tools-settings-section';
const settingsTitle = document.createElement('h3');
settingsTitle.className = 'vrc-tools-settings-title';
settingsTitle.textContent = 'General Settings';
const settingsOption1 = document.createElement('div');
settingsOption1.className = 'vrc-tools-settings-option';
const settingsLabel1 = document.createElement('div');
settingsLabel1.className = 'vrc-tools-settings-label';
settingsLabel1.textContent = 'Enable notifications';
const settingsDescription1 = document.createElement('div');
settingsDescription1.className = 'vrc-tools-settings-description';
settingsDescription1.textContent = 'Show notifications for chat messages and friend requests';
settingsLabel1.appendChild(settingsDescription1);
const settingsToggle1 = document.createElement('input');
settingsToggle1.type = 'checkbox';
settingsToggle1.checked = true;
settingsOption1.appendChild(settingsLabel1);
settingsOption1.appendChild(settingsToggle1);
const settingsOption2 = document.createElement('div');
settingsOption2.className = 'vrc-tools-settings-option';
const settingsLabel2 = document.createElement('div');
settingsLabel2.className = 'vrc-tools-settings-label';
settingsLabel2.textContent = 'Dark mode';
const settingsDescription2 = document.createElement('div');
settingsDescription2.className = 'vrc-tools-settings-description';
settingsDescription2.textContent = 'Enable dark mode interface';
settingsLabel2.appendChild(settingsDescription2);
const settingsToggle2 = document.createElement('input');
settingsToggle2.type = 'checkbox';
settingsToggle2.checked = true;
settingsOption2.appendChild(settingsLabel2);
settingsOption2.appendChild(settingsToggle2);
settingsSection.appendChild(settingsTitle);
settingsSection.appendChild(settingsOption1);
settingsSection.appendChild(settingsOption2);
settingsContent.appendChild(settingsSection);
// Append all tab contents to content container
contentContainer.appendChild(avatarsContent);
contentContainer.appendChild(chatContent);
contentContainer.appendChild(worldContent);
contentContainer.appendChild(settingsContent);
// Assemble the modal
modal.appendChild(header);
modal.appendChild(tabs);
modal.appendChild(contentContainer);
// Create toggle button
const toggleButton = document.createElement('button');
toggleButton.className = 'vrc-tools-toggle';
toggleButton.appendChild(createSVGIcon(iconMaps["star"]));
// Append to document
document.body.appendChild(modal);
document.body.appendChild(toggleButton);
// Add event listeners
closeButton.addEventListener('click', () => {
modal.style.display = 'none';
});
toggleButton.addEventListener('click', () => {
modal.style.display = modal.style.display === 'none' ? 'block' : 'none';
});
// Tab switching
tabs.querySelectorAll('.vrc-tools-tab').forEach(tab => {
tab.addEventListener('click', () => {
tabs.querySelectorAll('.vrc-tools-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
const tabId = tab.dataset.tab;
if (tabId === 'world') {
const worldGrid = document.querySelector('.vrc-tools-tab-content[data-tab="world"] .vrc-tools-avatar-grid');
worldGrid.innerHTML = '<div class="vrc-tools-loading"><div class="vrc-tools-spinner"></div></div>';
// Display a toast notification to show the source of the data
showToast('Loading Worlds', 'Fetching worlds from VRChat API: /api/1/worlds with system_approved tag', 'info');
fetchWorldData().then(worlds => {
displayWorlds(worlds, worldGrid);
}).catch(error => {
worldGrid.innerHTML = '<p>Error loading worlds. Please try again.</p>';
console.error("Error loading worlds:", error);
});
}
contentContainer.querySelectorAll('.vrc-tools-tab-content').forEach(content => {
if (content.dataset.tab === tabId) {
content.classList.add('active');
} else {
content.classList.remove('active');
}
});
});
});
// Make modal draggable
makeDraggable(modal, header);
// Add chat functionality
setupChat(chatInput, chatSendButton, chatLog);
// Add avatar search functionality
setupAvatarSearch(searchInput, searchButton, clearButton, avatarGrid);
return modal;
}
// Make an element draggable by its header
function makeDraggable(element, handle) {
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
handle.onmousedown = dragMouseDown;
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
// Get mouse position at startup
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
// Call function on cursor move
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
// Calculate new cursor position
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
// Set the element's new position
element.style.top = (element.offsetTop - pos2) + "px";
element.style.left = (element.offsetLeft - pos1) + "px";
}
function closeDragElement() {
// Stop moving when mouse button is released
document.onmouseup = null;
document.onmousemove = null;
}
}
// Setup chat functionality
function setupChat(chatInput, chatSendButton, chatLog) {
// Connect to WebSocket
if (!ws) {
ws = new WebSocket('wss://piano.ourworldofpixels.com');
ws.addEventListener("open", () => {
ws.send(JSON.stringify([{
"m": "hi"
}]));
// Show a toast notification
showToast('Chat Connected', 'Successfully connected to the chat server.', 'success');
});
ws.addEventListener("message", (e) => {
try {
const data = JSON.parse(e.data)[0];
if (data.m === "hi") {
ws.send(JSON.stringify([{
m: "ch",
_id: "vrchat-tools",
set: undefined
}]));
// Keep connection alive
setInterval(() => {
ws.send(JSON.stringify([{
m: "t",
e: Date.now()
}]));
}, 20000);
}
if (data.m === "n" && data.n[0].n === "customChat") {
const chatData = JSON.parse(data.n[1].n);
const username = chatData.username;
const msg = chatData.msg;
const rankName = chatData.rankName;
const rankColor = chatData.rankColor;
if (msg && msg !== "") {
addChatMessage(rankName, rankColor, username, msg);
}
}
} catch (error) {
console.error("WebSocket error:", error);
}
});
ws.addEventListener("error", (e) => {
showToast('Connection Error', 'Failed to connect to chat server. Please try again later.', 'error');
console.error("WebSocket error:", e);
});
ws.addEventListener("close", () => {
console.log("WebSocket closed");
});
}
// Send message function
function sendChatMessage() {
const message = chatInput.value.trim();
if (!message) return;
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify([{
m: "n",
t: Date.now(),
n: [{
n: "customChat",
v: 0
}, {
n: JSON.stringify({
username: displayName,
msg: message,
rankName: trustRankName,
rankColor: trustRankColor
}),
d: 10,
v: 20
}]
}]));
addChatMessage(trustRankName, trustRankColor, displayName, message);
chatInput.value = "";
} else {
showToast('Not Connected', 'Chat is not connected. Please try refreshing the page.', 'error');
}
}
// Add message to chat log
function addChatMessage(rankName, rankColor, username, message) {
const messageElement = document.createElement('div');
messageElement.className = 'vrc-tools-chat-message';
const rankSpan = document.createElement('span');
rankSpan.style.color = rankColor;
rankSpan.style.fontWeight = 'bold';
rankSpan.textContent = `[${rankName}] `;
const nameSpan = document.createElement('span');
nameSpan.style.color = '#800080';
nameSpan.style.fontWeight = 'bold';
nameSpan.textContent = `${username}: `;
const messageSpan = document.createElement('span');
messageSpan.textContent = message;
messageElement.appendChild(rankSpan);
messageElement.appendChild(nameSpan);
messageElement.appendChild(messageSpan);
chatLog.appendChild(messageElement);
chatLog.scrollTop = chatLog.scrollHeight;
// Limit chat messages
while (chatLog.childNodes.length > 50) {
chatLog.removeChild(chatLog.firstChild);
}
}
// Add event listeners
chatInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendChatMessage();
}
});
chatSendButton.addEventListener('click', sendChatMessage);
}
// Setup avatar search functionality
function setupAvatarSearch(searchInput, searchButton, clearButton, avatarGrid) {
// Search function
async function searchAvatars() {
const searchTerm = searchInput.value.trim();
if (!searchTerm) return;
avatarGrid.innerHTML = '<div class="vrc-tools-loading"><div class="vrc-tools-spinner"></div></div>';
try {
const avatars = await fetchAndParseAvatarData(searchTerm);
displayAvatars(JSON.parse(avatars));
} catch (error) {
avatarGrid.innerHTML = '<p>Error searching avatars. Please try again.</p>';
console.error("Search error:", error);
}
}
// Display avatars in grid
function displayAvatars(avatars) {
avatarGrid.innerHTML = '';
if (avatars.length === 0) {
avatarGrid.innerHTML = '<p>No avatars found matching your search.</p>';
return;
}
avatars.forEach(avatar => {
const card = document.createElement('div');
card.className = 'vrc-tools-avatar-card';
const img = document.createElement('img');
img.className = 'vrc-tools-avatar-thumbnail';
img.src = avatar.imageUrl || 'https://via.placeholder.com/300?text=No+Image';
img.alt = avatar.avatarName;
const info = document.createElement('div');
info.className = 'vrc-tools-avatar-info';
const name = document.createElement('h4');
name.className = 'vrc-tools-avatar-name';
name.textContent = avatar.avatarName;
const author = document.createElement('p');
author.className = 'vrc-tools-avatar-author';
author.textContent = `By: ${avatar.authorName}`;
const actions = document.createElement('div');
actions.className = 'vrc-tools-avatar-actions';
const viewButton = document.createElement('button');
viewButton.className = 'vrc-tools-avatar-btn';
viewButton.textContent = 'View';
viewButton.addEventListener('click', () => {
window.open(`https://vrchat.com/home/avatar/${avatar.avatarId}`, '_blank');
});
const downloadButton = document.createElement('button');
downloadButton.className = 'vrc-tools-avatar-btn primary';
downloadButton.textContent = 'VRCA';
downloadButton.addEventListener('click', () => {
if (avatar.vrcaUrl) {
window.open(avatar.vrcaUrl, '_blank');
} else {
showToast('Not Available', 'VRCA file is not available for this avatar.', 'error');
}
});
actions.appendChild(viewButton);
actions.appendChild(downloadButton);
info.appendChild(name);
info.appendChild(author);
info.appendChild(actions);
card.appendChild(img);
card.appendChild(info);
avatarGrid.appendChild(card);
});
}
// Add event listeners
searchButton.addEventListener('click', searchAvatars);
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
searchAvatars();
}
});
clearButton.addEventListener('click', () => {
searchInput.value = '';
avatarGrid.innerHTML = '';
});
}
// Show toast notification
function showToast(title, message, type = 'info') {
const toast = document.createElement('div');
toast.className = `vrc-tools-toast ${type}`;
const iconPath = type === 'success' ? iconMaps["star"] :
type === 'error' ? iconMaps["times"] : iconMaps["world"];
const icon = createSVGIcon(iconPath, 'vrc-tools-toast-icon');
const content = document.createElement('div');
content.className = 'vrc-tools-toast-content';
const titleElement = document.createElement('div');
titleElement.className = 'vrc-tools-toast-title';
titleElement.textContent = title;
const messageElement = document.createElement('div');
messageElement.textContent = message;
const closeButton = document.createElement('button');
closeButton.className = 'vrc-tools-toast-close';
closeButton.appendChild(createSVGIcon(iconMaps["times"]));
content.appendChild(titleElement);
content.appendChild(messageElement);
toast.appendChild(icon);
toast.appendChild(content);
toast.appendChild(closeButton);
document.body.appendChild(toast);
closeButton.addEventListener('click', () => {
toast.style.animation = 'slideOut 0.3s forwards';
setTimeout(() => {
document.body.removeChild(toast);
}, 300);
});
// Auto remove after 5 seconds
setTimeout(() => {
if (document.body.contains(toast)) {
toast.style.animation = 'slideOut 0.3s forwards';
setTimeout(() => {
if (document.body.contains(toast)) {
document.body.removeChild(toast);
}
}, 300);
}
}, 5000);
}
// Fetch user data from VRChat API
async function fetchUserData() {
try {
const response = await fetch("https://vrchat.com/api/1/auth/user", {
headers: {
"accept": "*/*",
"accept-language": "en-US,en;q=0.9",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin"
},
method: "GET",
credentials: "include"
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const user = await response.json();
displayName = user.displayName;
// Determine trust rank
const tags = user.tags;
if (tags.includes("admin_moderator")) {
trustRankColor = "#FF0000";
trustRankName = "Administrator";
} else if (tags.includes("system_trust_veteran") && tags.includes("system_trust_trusted")) {
trustRankColor = "#800080";
trustRankName = "Trusted";
} else if (tags.includes("system_trust_trusted") && tags.includes("system_trust_known") && !tags.includes("system_trust_basic")) {
trustRankColor = "#FFD700";
trustRankName = "Known";
} else if (tags.includes("system_trust_basic") && tags.includes("system_trust_known")) {
trustRankColor = "#90EE90";
trustRankName = "User";
} else if (tags.includes("system_trust_basic")) {
trustRankColor = "#ADD8E6";
trustRankName = "New User";
} else {
trustRankColor = "#F5F5F5";
trustRankName = "Visitor";
}
return user;
} catch (error) {
console.error("Error fetching user data:", error);
return null;
}
}
// Initialize the extension
async function init() {
injectCSS();
// Fetch user data
await fetchUserData();
// Create and add toggle button
const toggleBtn = document.createElement('button');
toggleBtn.className = 'vrc-tools-toggle';
toggleBtn.title = 'VRChat Enhanced Tools';
toggleBtn.appendChild(createSVGIcon(iconMaps["star"]));
document.body.appendChild(toggleBtn);
// Create modal (initially hidden)
const modal = createModal();
// Add click event to toggle button
toggleBtn.addEventListener('click', () => {
modal.style.display = modal.style.display === 'none' ? 'block' : 'none';
});
}
// Wait for page to load
window.addEventListener('load', () => {
// Only run on VRChat website
if (window.location.hostname === 'vrchat.com') {
setTimeout(init, 2000); // Delay to ensure page is fully loaded
}
});
async function fetchAndParseAvatarData(searchName) {
try {
const url = 'https://raw.githubusercontent.com/Snoofz/Snowly-VRC-Tools/main/JxLN772OoP.json';
const response = await fetch(url);
const data = await response.json();
const filteredAvatars = data.filter(avatar =>
avatar.avatarName.toLowerCase().includes(searchName.toLowerCase())
);
const avatarData = filteredAvatars.map(avatar => ({
imageUrl: avatar.thumbnailUrl,
avatarName: avatar.avatarName,
avatarId: avatar.avatarId,
avatarDescription: `Wearer: ${avatar.wearer}, Stealer: ${avatar.stealer}`,
vrcaUrl: avatar.vrca,
authorName: avatar.authorName
}));
return JSON.stringify(avatarData, null, 2);
} catch (error) {
console.error('Error fetching or parsing data:', error);
}
}
function parseHTMLToJSON(html) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const results = [];
const avatarCards = doc.querySelectorAll('.card-dark');
avatarCards.forEach(card => {
const name = card.querySelector('.card-text').textContent.trim();
const description = card.querySelector('.card-text.text-break').textContent.trim();
const id = card.querySelector('.card-text.text-muted').textContent.trim();
const author = card.querySelector('.text-muted').nextElementSibling.textContent.trim();
const thumbnail = card.querySelector('.card-img-top').src;
const avatar = {
name: name,
description: description,
id: id,
author: author,
thumbnail: thumbnail
};
results.push(avatar);
});
return JSON.stringify(results, null, 2);
}
function fetchUserLocation(username) {
fetch("https://vrchat.com/api/1/users/usr_afcc65d6-fc6f-4fbd-b1a6-d57db556a7b4", {
"headers": {
"accept": "*/*",
"accept-language": "en-US,en;q=0.9",
"sec-ch-ua": "\"Not A(Brand\";v=\"99\", \"Google Chrome\";v=\"121\", \"Chromium\";v=\"121\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin"
},
"referrer": "https://vrchat.com/home/user/usr_afcc65d6-fc6f-4fbd-b1a6-d57db556a7b4",
"referrerPolicy": "strict-origin-when-cross-origin",
"body": null,
"method": "GET",
"mode": "cors",
"credentials": "include"
}).then(res => res.json()).then(json => {
if (json.location == "offline" || json.location == "private") {
}
});
}
})();