您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Modern UI overlay with improved avatar search and tools
当前为
// ==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") { } }); } })();