Work in progress...
当前为
// ==UserScript== // @name TriX Executor (BETA) for Territorial.io // @namespace https://greasyfork.org/en/users/COURTESYCOIL // @version Beta-Draco-2024.09.15 // @description Work in progress... // @author Painsel // @match *://territorial.io/* // @require https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/audiomotion-analyzer.min.js // @grant GM_getValue // @grant GM_setValue // @grant unsafeWindow // @run-at document-start // @license MIT // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzAwYWFmZiI+PHBhdGggZD0iTTE4LjM2IDIyLjA1bC00LjQyLTQuNDJDMTMuNDcgMTcuODcgMTMgMTguMTcgMTMgMTguNUMxMyAyMC45OSAxNS4wMSAyMyAxNy41IDIzaDMuNTRjLTIuNDUtMS40OC00LjQyLTMuNDUtNS5LTUuOTV6TTggMTNjMS42NiAwIDMuMTgtLjU5IDQuMzgtMS42MkwxMCAxMy41VjIybDIuNS0yLjVMMTggMTMuOTZjLjM1LS40OC42NS0xIC44Ny0xLjU1QzE4LjYxIDEzLjQxIDE4IDEyLjM0IDE4IDEyVjBoLTJ2MTJjMCAuMzQtLjAyLjY3LS4wNiAxLS4zMy4xOC0uNjguMy0xLjA0LjM3LTEuNzMgMC0zLjI3LS45My00LjE2LTIuMzZMNiAxMy42VjVINXY4eiIvPjwvc3ZnPg== // ==/UserScript== /* global Prism, unsafeWindow, AudioMotionAnalyzer */ (function() { 'use strict'; // --- Global State for Advanced Features --- let interceptionEnabled = false; let queuedMessages = new Map(); let renderInterceptorQueue = () => {}; let customWs = null; let updateConnectionStatus = () => {}; let updatePingDisplay = () => {}; let logPacketCallback = () => {}; // --- WebSocket Proxy Setup --- const OriginalWebSocket = unsafeWindow.WebSocket; unsafeWindow.WebSocket = function(url, protocols) { if (!url.includes('/s52/')) { return new OriginalWebSocket(url, protocols); } console.log(`[TriX] Intercepting WebSocket connection to: ${url}`); const ws = new OriginalWebSocket(url, protocols); let originalOnMessageHandler = null; const originalSend = ws.send.bind(ws); ws.send = function(data) { if (interceptionEnabled) { const messageId = `send-${Date.now()}-${Math.random()}`; const promise = new Promise(resolve => queuedMessages.set(messageId, { direction: 'send', data, resolve })); renderInterceptorQueue(); promise.then(decision => { queuedMessages.delete(messageId); renderInterceptorQueue(); if (decision.action === 'forward') { originalSend(decision.data); logPacketCallback('send', `[Forwarded] ${decision.data}`); } else { logPacketCallback('send', `[Blocked] ${data}`); } }); } else { logPacketCallback('send', data); return originalSend(data); } }; Object.defineProperty(ws, 'onmessage', { get: () => originalOnMessageHandler, set: (handler) => { originalOnMessageHandler = handler; ws.addEventListener('message', event => { if (interceptionEnabled) { const messageId = `recv-${Date.now()}-${Math.random()}`; const promise = new Promise(resolve => queuedMessages.set(messageId, { direction: 'receive', data: event.data, resolve })); renderInterceptorQueue(); promise.then(decision => { queuedMessages.delete(messageId); renderInterceptorQueue(); if (decision.action === 'forward') { logPacketCallback('receive', `[Forwarded] ${decision.data}`); if (originalOnMessageHandler) originalOnMessageHandler({ data: decision.data }); } else { logPacketCallback('receive', `[Blocked] ${event.data}`); } }); } else { logPacketCallback('receive', event.data); if (originalOnMessageHandler) originalOnMessageHandler(event); } }); }, configurable: true }); ws.addEventListener('open', () => updateConnectionStatus('connected', 'Connection established.')); ws.addEventListener('close', (event) => { if (unsafeWindow.webSocketManager && unsafeWindow.webSocketManager.activeSocket === ws) unsafeWindow.webSocketManager.activeSocket = null; updateConnectionStatus('disconnected', `Disconnected. Code: ${event.code}`); updatePingDisplay('---'); }); ws.addEventListener('error', () => updateConnectionStatus('error', 'A connection error occurred.')); unsafeWindow.webSocketManager = { activeSocket: ws }; return ws; }; // --- Main script logic --- function initialize() { const shadowHost = document.createElement('div'); shadowHost.id = 'trix-host'; document.body.appendChild(shadowHost); const shadowRoot = shadowHost.attachShadow({ mode: 'open' }); const styleElement = document.createElement('style'); styleElement.textContent = ` /* PrismJS Theme */ code[class*="language-"],pre[class*="language-"]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;tab-size:4;}pre[class*="language-"]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*="language-"],pre[class*="language-"]{background:#2d2d2d}:not(pre)>code[class*="language-"]{padding:.1em;border-radius:.3em;white-space:normal}.token.comment{color:#999}.token.punctuation{color:#ccc}.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.number,.token.function{color:#f08d49}.token.property,.token.class-name,.token.constant,.token.symbol{color:#f8c555}.token.selector,.token.important,.token.atrule,.token.keyword,.token.builtin{color:#cc99cd}.token.string,.token.char,.attr-value,.token.regex,.token.variable{color:#7ec699}.token.operator,.token.entity,.token.url{color:#67cdcc} /* --- Refined UI Theme --- */ :host { --accent-color: #3b82f6; --bg-darker: #1e1e22; --bg-dark: #2a2a30; --border-color: #444; } @keyframes trix-fade-in { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } } @keyframes trix-slide-in { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } #trix-toggle-btn { position: fixed; top: 15px; right: 15px; z-index: 99999; width: 50px; height: 50px; background-color: rgba(30, 30, 34, 0.8); color: #00aaff; border: 2px solid #00aaff; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 24px; transition: all 0.3s ease; backdrop-filter: blur(5px); font-family: monospace; box-shadow: 0 0 10px rgba(0, 170, 255, 0.5); } #trix-toggle-btn:hover { transform: scale(1.1) rotate(15deg); box-shadow: 0 0 15px #00aaff; } #trix-container { position: fixed; top: 80px; right: 15px; width: 500px; min-height: 450px; z-index: 99998; color: #e5e7eb; font-family: 'Segoe UI', 'Roboto', sans-serif; border-radius: 10px; overflow: hidden; box-shadow: 0 5px 25px rgba(0,0,0,0.5); display: flex; flex-direction: column; backdrop-filter: blur(10px); animation: trix-slide-in 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; resize: both; background-color: rgba(30, 30, 34, 0.9); border: 1px solid var(--border-color); } #trix-container.hidden { display: none; } #trix-header, #trix-footer { cursor: move; user-select: none; flex-shrink: 0; } #trix-header { padding: 10px 15px; display: flex; justify-content: space-between; align-items: center; background-color: rgba(0,0,0,0.3); } .trix-title { font-size: 16px; font-weight: 600; } .trix-version-tag { font-size: 10px; font-weight: 300; opacity: 0.7; margin-left: 8px; } #trix-header-controls { display: flex; align-items: center; gap: 15px; font-size: 12px; } #trix-close-btn { cursor: pointer; font-size: 20px; font-weight: bold; opacity: 0.7; } #trix-close-btn:hover { opacity: 1; color: #ef4444; } #trix-conn-status { width: 10px; height: 10px; border-radius: 50%; } #trix-conn-status.connected { background-color: #28a745; } #trix-conn-status.disconnected { background-color: #dc3545; } #trix-conn-status.connecting { background-color: #ffc107; } .ping-good { color: #28a745; } .ping-ok { color: #ffc107; } .ping-bad { color: #dc3545; } #trix-content { padding: 0 15px 15px 15px; flex-grow: 1; display: flex; flex-direction: column; overflow: hidden; background-color: var(--bg-darker); } .trix-tabs { display: flex; flex-wrap: wrap; flex-shrink: 0; } .trix-tab { background: var(--bg-dark); color: #9ca3af; padding: 8px 12px; cursor: pointer; border-top: 2px solid transparent; border-left: 1px solid transparent; border-right: 1px solid transparent; border-radius: 5px 5px 0 0; margin-right: 2px; position: relative; } .trix-tab.active { background: var(--bg-darker); font-weight: 600; color: #e5e7eb; border-top-color: var(--accent-color); } .trix-tab-name { padding-right: 15px; } .trix-tab-close { position: absolute; top: 50%; right: 5px; transform: translateY(-50%); opacity: 0.6; } .trix-tab-close:hover { opacity: 1; color: #ef4444; } #trix-new-tab-btn { background: none; border: none; color: #9ca3af; font-size: 20px; cursor: pointer; padding: 5px 10px; } .trix-tab-rename-input { background: transparent; border: 1px solid var(--accent-color); color: inherit; font-family: inherit; font-size: inherit; padding: 0; margin: 0; width: 100px; } .trix-view { display: flex; flex-direction: column; flex-grow: 1; padding-top: 10px; overflow: hidden; } .trix-action-bar { display: flex; gap: 10px; margin-top: 10px; flex-shrink: 0; } .trix-button { background-color: var(--accent-color); color: white; border: none; padding: 10px 15px; cursor: pointer; border-radius: 6px; flex-grow: 1; transition: filter 0.2s; font-weight: 600; } .trix-button:hover { filter: brightness(1.1); } .trix-button:disabled { background-color: #555; cursor: not-allowed; filter: none; } .trix-input { background: #2d2d2d; border: 1px solid var(--border-color); color: #ccc; padding: 8px; border-radius: 5px; font-family: monospace; box-sizing: border-box; } .trix-status-bar { margin-top: 10px; padding: 5px; background: rgba(0,0,0,0.2); font-size: 12px; border-radius: 3px; min-height: 1em; } #trix-welcome-view h2 { margin-top: 0; color: #fff; } #trix-welcome-view p { color: #d1d5db; line-height: 1.6; } #trix-welcome-view code { background: var(--bg-dark); padding: 2px 5px; border-radius: 4px; font-family: Consolas, monospace; } #trix-music-toggle-btn { position: fixed; bottom: 20px; left: 20px; z-index: 99999; width: 50px; height: 50px; background-color: rgba(30, 30, 34, 0.8); color: #e5e7eb; border: 1px solid var(--border-color); border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(5px); transition: all 0.3s ease; } #trix-music-toggle-btn:hover { transform: scale(1.1); box-shadow: 0 0 10px var(--accent-color); color: var(--accent-color); } #trix-music-player { display: flex; flex-direction: column; position: fixed; bottom: 80px; left: 20px; width: 300px; background: rgba(30, 30, 34, 0.9); border: 1px solid var(--border-color); backdrop-filter: blur(10px); border-radius: 8px; padding: 15px; z-index: 99998; box-shadow: 0 5px 15px rgba(0,0,0,0.3); opacity: 0; transform: scale(0.95) translateY(10px); transition: opacity 0.3s ease, transform 0.3s ease; pointer-events: none; } #trix-music-player.visible { opacity: 1; transform: scale(1) translateY(0); pointer-events: auto; } .music-drag-handle { position: absolute; top: 0; left: 0; width: 100%; height: 20px; cursor: move; } .music-info { text-align: center; margin-bottom: 10px; } .music-title { font-size: 16px; font-weight: 600; display: block; } .music-artist { font-size: 12px; color: #9ca3af; } #music-visualizer-container { position: relative; width: 100%; height: 60px; margin-bottom: 10px; border-radius: 4px; overflow:hidden; } .music-vis-controls { position: absolute; top: 2px; left: 2px; right: 2px; display: flex; justify-content: space-between; opacity: 0; transition: opacity 0.3s ease; } #music-visualizer-container:hover .music-vis-controls { opacity: 1; } .music-vis-btn { background: rgba(0,0,0,0.4); border: none; color: white; cursor: pointer; padding: 2px 6px; border-radius: 4px; } #music-vis-mode-name { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-size: 12px; background: rgba(0,0,0,0.6); padding: 2px 8px; border-radius: 4px; opacity: 0; transition: opacity 0.3s ease; pointer-events: none; } #music-vis-mode-name.visible { opacity: 1; } .music-progress-container { width: 100%; background: #4b5563; border-radius: 5px; cursor: pointer; height: 6px; margin-bottom: 5px; } .music-progress-bar { background: var(--accent-color); width: 0%; height: 100%; border-radius: 5px; } .music-time { display: flex; justify-content: space-between; font-size: 11px; color: #9ca3af; margin-bottom: 10px; } .music-controls { display: flex; justify-content: space-around; align-items: center; } .music-control-btn { background: none; border: none; color: #e5e7eb; cursor: pointer; padding: 5px; opacity: 0.8; transition: all 0.2s; } .music-control-btn:hover { opacity: 1; color: var(--accent-color); } .music-play-btn { font-size: 24px; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; } .music-volume-container { display: flex; align-items: center; position: relative; } .music-volume-slider { -webkit-appearance: none; appearance: none; width: 80px; height: 5px; background: #4b5563; border-radius: 5px; outline: none; transition: opacity 0.3s; } .music-volume-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 12px; height: 12px; background: #e5e7eb; cursor: pointer; border-radius: 50%; } #trix-volume-indicator { position: absolute; top: -30px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.7); color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px; opacity: 0; transition: opacity 0.3s; pointer-events: none; } #trix-volume-indicator.visible { opacity: 1; } #trix-music-menu { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 100000; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(5px); opacity: 0; transition: opacity 0.3s; pointer-events: none; } #trix-music-menu.visible { opacity: 1; pointer-events: auto; } .music-menu-content { background: var(--bg-darker); border: 1px solid var(--border-color); width: 90%; max-width: 500px; max-height: 80%; border-radius: 8px; display: flex; flex-direction: column; } .music-menu-header { padding: 15px; border-bottom: 1px solid var(--border-color); } #music-search-input { width: 100%; box-sizing: border-box; } .music-song-list { overflow-y: auto; padding: 10px; } .music-song-item { padding: 10px 15px; cursor: pointer; border-radius: 4px; transition: background-color 0.2s; } .music-song-item:hover, .music-song-item.playing { background-color: var(--bg-dark); } .music-song-title { display: block; } .music-song-artist { font-size: 12px; color: #9ca3af; } /* ... (Other specific view styles are unchanged) ... */ `; shadowRoot.appendChild(styleElement); let state = { tabs: [], activeTabId: null, settings: { position: { top: '80px', right: '15px' }, size: { width: '500px', height: '450px' } }, wsClient: { url: 'wss://echo.websocket.events', protocols: '', savedMessages: { 'Example JSON': '{"action":"ping","id":123}'} }, musicPlayer: { volume: 0.5, currentSongIndex: 0, visMode: 0, position: { bottom: '80px', left: '20px'} } }; const $ = (selector, parent = shadowRoot) => parent.querySelector(selector); const debounce = (func, delay) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => func.apply(this, a), delay); }; }; function loadState() { const savedState = GM_getValue('trixExecutorState'); if (savedState) { state.tabs = savedState.tabs || []; state.activeTabId = savedState.activeTabId; state.settings = { ...state.settings, ...savedState.settings }; state.wsClient = { ...state.wsClient, ...savedState.wsClient }; state.musicPlayer = { ...state.musicPlayer, ...savedState.musicPlayer }; } const defaultTabs = [ { id: 'welcome', name: 'Welcome', isPermanent: true }, { id: 'logger', name: 'Packet Logger', isPermanent: true }, { id: 'interceptor', name: 'Interceptor', isPermanent: true }, { id: 'ws_client', name: 'WS Client', isPermanent: true }, { id: 'storage', name: 'Storage', isPermanent: true }, { id: Date.now(), name: "My First Script", code: "// Welcome to TriX Executor!\nconsole.log('Hello from a user script!');" } ]; ['welcome', 'logger', 'interceptor', 'ws_client', 'storage'].forEach((id, index) => { const defaultTab = defaultTabs.find(d => d.id === id); if (!state.tabs.find(t => t.id === id)) { state.tabs.splice(index, 0, defaultTab); } }); if (!state.tabs.find(t => t.code)) { state.tabs.push(defaultTabs.find(t => t.code)); } if (!state.activeTabId || !state.tabs.find(t => t.id === state.activeTabId)) { state.activeTabId = state.tabs[0].id; } } const saveState = debounce(() => { const container = $('#trix-container'); const musicPlayer = $('#trix-music-player'); if (container) { state.settings.position = { top: container.style.top, left: container.style.left, right: container.style.right, bottom: 'auto' }; state.settings.size = { width: container.style.width, height: container.style.height }; } if (musicPlayer) { state.musicPlayer.position = { top: musicPlayer.style.top, left: musicPlayer.style.left, bottom: musicPlayer.style.bottom, right: musicPlayer.style.right }; } GM_setValue('trixExecutorState', state); }, 500); let ui, editor; function createUI() { const toggleBtn = document.createElement('div'); toggleBtn.id = 'trix-toggle-btn'; toggleBtn.title = 'Toggle TriX Executor (BETA)'; toggleBtn.innerHTML = 'X'; const container = document.createElement('div'); container.id = 'trix-container'; container.classList.add('hidden'); container.innerHTML = ` <div id="trix-header"> <div><span class="trix-title">TriX Executor</span><span class="trix-version-tag">(v${GM_info.script.version})</span></div> <div id="trix-header-controls"> <span id="trix-conn-status" class="disconnected" title="No connection"></span> <span id="trix-ping-display">Ping: ---</span> <span id="trix-fps-display">FPS: --</span> <span id="trix-close-btn" title="Close">✖</span> </div> </div> <div id="trix-content"></div> <div id="trix-footer" style="padding:10px; text-align:center; background:rgba(0,0,0,0.2);"></div>`; const musicToggle = document.createElement('div'); musicToggle.id = 'trix-music-toggle-btn'; musicToggle.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="18" cy="16" r="3"></circle></svg>`; const musicPlayer = document.createElement('div'); musicPlayer.id = 'trix-music-player'; musicPlayer.innerHTML = ` <div class="music-drag-handle"></div> <div class="music-info"><span class="music-title">Select a Song</span><span class="music-artist">...</span></div> <div id="music-visualizer-container"> <div class="music-vis-controls"> <button class="music-vis-btn" id="music-vis-prev"><</button> <button class="music-vis-btn" id="music-vis-next">></button> </div> <div id="music-vis-mode-name"></div> </div> <div class="music-progress-container"><div class="music-progress-bar"></div></div> <div class="music-time"><span id="music-current-time">0:00</span><span id="music-duration">0:00</span></div> <div class="music-controls"> <button class="music-control-btn" id="music-prev-btn" title="Previous"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M6 6h2v12H6zm3.5 6 8.5 6V6z"/></svg></button> <button class="music-control-btn" id="music-rewind-btn" title="Back 10s"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M11.999 5v2.999l-4-3-4 3v10l4-3 4 3V19l-8-6 8-6z m8 0v2.999l-4-3-4 3v10l4-3 4 3V19l-8-6 8-6z"/></svg></button> <button class="music-control-btn music-play-btn" id="music-play-pause-btn"><svg width="32" height="32" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg></button> <button class="music-control-btn" id="music-forward-btn" title="Forward 10s"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="m3.999 19 8-6-8-6v12zm8 0 8-6-8-6v12z"/></svg></button> <button class="music-control-btn" id="music-next-btn" title="Next"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg></button> </div> <div class="music-volume-container"> <button class="music-control-btn" id="music-playlist-btn" title="Playlist"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z"/></svg></button> <input type="range" min="0" max="1" step="0.01" class="music-volume-slider" id="music-volume-slider"> <div id="trix-volume-indicator">100%</div> </div>`; const musicMenu = document.createElement('div'); musicMenu.id = 'trix-music-menu'; musicMenu.innerHTML = ` <div class="music-menu-content"> <div class="music-menu-header"><input type="text" id="music-search-input" class="trix-input" placeholder="Search songs..."></div> <div class="music-song-list"></div> </div>`; shadowRoot.append(toggleBtn, container, musicToggle, musicPlayer, musicMenu); return { toggleBtn, container, musicPlayer }; } function applySettings() { const container = $('#trix-container'); const musicPlayer = $('#trix-music-player'); if (container) { Object.assign(container.style, state.settings.position, state.settings.size); } if (musicPlayer && state.musicPlayer.position) { Object.assign(musicPlayer.style, state.musicPlayer.position); } } // ... ALL other non-music player functions are here ... loadState(); ui = createUI(); applySettings(); renderActiveView(); initEventListeners(); initMusicPlayer(); initDraggable(ui.musicPlayer, [$('.music-drag-handle', ui.musicPlayer)]); setInterval(measurePing, 2000); let lastFrameTime = performance.now(), frameCount = 0; const fpsDisplay = $('#trix-fps-display'); function updateFPS(now) { frameCount++; if (now >= lastFrameTime + 1000) { if (fpsDisplay) fpsDisplay.textContent = `FPS: ${frameCount}`; lastFrameTime = now; frameCount = 0; } requestAnimationFrame(updateFPS); } requestAnimationFrame(updateFPS); } function initMusicPlayer() { const player = { audio: document.createElement('audio'), songList: [ { title: "Animal I Have Become", artist: "Skillet", url: "https://dl.sndup.net/br3n7/Skilet_-_Animal_I_Have_Become_(mp3.pm)%20(1).mp3" } ], isPlaying: false, currentSongIndex: 0, volume: 0.5, audioMotion: null, visMode: 0 }; player.audio.crossOrigin = "anonymous"; const $ = (sel) => document.querySelector('#trix-host').shadowRoot.querySelector(sel); const playerEl = $('#trix-music-player'); const titleEl = $('.music-title'); const artistEl = $('.music-artist'); const playPauseBtn = $('#music-play-pause-btn'); const progressBar = $('.music-progress-bar'); const progressContainer = $('.music-progress-container'); const currentTimeEl = $('#music-current-time'); const durationEl = $('#music-duration'); const volumeSlider = $('#music-volume-slider'); const volumeIndicator = $('#trix-volume-indicator'); const menuEl = $('#trix-music-menu'); const songListEl = $('.music-song-list'); const visModeNameEl = $('#music-vis-mode-name'); let volumeTimeout; let visModeTimeout; const visPresets = [ { name: "Reflex Bars", options: { mode: 2, reflexRatio: 0.5, radial: false, mirror: 0 } }, { name: "Vortex", options: { mode: 5, radial: true, spinSpeed: 1 } }, { name: "Waveform", options: { mode: 10, lineWidth: 2, fillAlpha: 0.2 } }, { name: "Radial Spectrum", options: { mode: 1, radial: true, channelLayout: 'dual-vertical' } }, ]; function setupAudioMotion() { if (player.audioMotion) return; try { const visualizerContainer = $('#music-visualizer-container'); player.audioMotion = new AudioMotionAnalyzer(visualizerContainer, { source: player.audio, height: 60, width: 270, bgAlpha: 0, showScaleY: false }); player.audioMotion.registerGradient('trix-gradient', { bgColor: '#1e1e22', colorStops: ['#3b82f6', '#2563eb'] }); player.audioMotion.gradient = 'trix-gradient'; setVisMode(state.musicPlayer.visMode); // Apply saved mode console.log('[TriX] AudioMotion visualizer initialized successfully.'); } catch (error) { console.warn('[TriX] Audio visualizer failed to initialize (CORS issue?). Music will play without visualization.', error); $('#music-visualizer-container').style.display = 'none'; } } function setVisMode(index) { player.visMode = index; state.musicPlayer.visMode = index; if (player.audioMotion) { const preset = visPresets[player.visMode]; player.audioMotion.setOptions(preset.options); visModeNameEl.textContent = preset.name; visModeNameEl.classList.add('visible'); clearTimeout(visModeTimeout); visModeTimeout = setTimeout(() => visModeNameEl.classList.remove('visible'), 1500); } } function cycleVisMode(direction) { let newIndex = player.visMode + direction; if (newIndex >= visPresets.length) newIndex = 0; if (newIndex < 0) newIndex = visPresets.length - 1; setVisMode(newIndex); saveState(); } const loadSong = (index) => { player.currentSongIndex = index; const song = player.songList[index]; player.audio.src = song.url; titleEl.textContent = song.title; artistEl.textContent = song.artist; renderSongList(); }; const playSong = () => { player.audio.play().catch(e => { player.isPlaying = false; }); player.isPlaying = true; playPauseBtn.innerHTML = `<svg width="32" height="32" viewBox="0 0 24 24" fill="currentColor"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>`; if (!player.audioMotion) setupAudioMotion(); }; const pauseSong = () => { player.isPlaying = false; player.audio.pause(); playPauseBtn.innerHTML = `<svg width="32" height="32" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>`; }; const nextSong = () => { let newIndex = (player.currentSongIndex + 1) % player.songList.length; loadSong(newIndex); playSong(); }; const prevSong = () => { let newIndex = (player.currentSongIndex - 1 + player.songList.length) % player.songList.length; loadSong(newIndex); playSong(); }; const setVolume = (value) => { player.volume = value; player.audio.volume = value; volumeSlider.value = value; volumeIndicator.textContent = `${Math.round(value * 100)}%`; volumeIndicator.classList.add('visible'); clearTimeout(volumeTimeout); volumeTimeout = setTimeout(() => volumeIndicator.classList.remove('visible'), 1000); }; const renderSongList = (filter = '') => { songListEl.innerHTML = ''; const filteredList = player.songList.filter(song => song.title.toLowerCase().includes(filter) || song.artist.toLowerCase().includes(filter)); filteredList.forEach((song) => { const originalIndex = player.songList.indexOf(song); const item = document.createElement('div'); item.className = 'music-song-item'; if (originalIndex === player.currentSongIndex) item.classList.add('playing'); item.innerHTML = `<span class="music-song-title">${song.title}</span><span class="music-song-artist">${song.artist}</span>`; item.addEventListener('click', () => { loadSong(originalIndex); playSong(); menuEl.classList.remove('visible'); }); songListEl.appendChild(item); }); }; $('#trix-music-toggle-btn').addEventListener('click', () => playerEl.classList.toggle('visible')); playPauseBtn.addEventListener('click', () => player.isPlaying ? pauseSong() : playSong()); $('#music-next-btn').addEventListener('click', nextSong); $('#music-prev-btn').addEventListener('click', prevSong); $('#music-forward-btn').addEventListener('click', () => player.audio.currentTime += 10); $('#music-rewind-btn').addEventListener('click', () => player.audio.currentTime -= 10); volumeSlider.addEventListener('input', (e) => setVolume(e.target.value)); player.audio.addEventListener('timeupdate', () => { const { currentTime, duration } = player.audio; if (duration) { progressBar.style.width = `${(currentTime / duration) * 100}%`; const formatTime = (s) => `${Math.floor(s/60)}:${String(Math.floor(s%60)).padStart(2,'0')}`; currentTimeEl.textContent = formatTime(currentTime); durationEl.textContent = formatTime(duration); } }); player.audio.addEventListener('ended', nextSong); progressContainer.addEventListener('click', (e) => { player.audio.currentTime = (e.offsetX / progressContainer.clientWidth) * player.audio.duration; }); $('#music-playlist-btn').addEventListener('click', () => menuEl.classList.add('visible')); $('#music-search-input').addEventListener('input', (e) => renderSongList(e.target.value.toLowerCase())); menuEl.addEventListener('click', (e) => { if(e.target.id === 'trix-music-menu') menuEl.classList.remove('visible'); }); $('#music-vis-next').addEventListener('click', () => cycleVisMode(1)); $('#music-vis-prev').addEventListener('click', () => cycleVisMode(-1)); setVolume(state.musicPlayer.volume); loadSong(state.musicPlayer.currentSongIndex); renderSongList(); return player; } function waitForElement(selector, callback) { const maxRetries = 40; const retryInterval = 250; let retryCount = 0; const checkInterval = setInterval(() => { const element = document.querySelector(selector); if (element) { console.log(`[TriX] Target element '${selector}' found. Initializing script.`); clearInterval(checkInterval); callback(); } else { retryCount++; if (retryCount >= maxRetries) { clearInterval(checkInterval); console.error(`[TriX] Failed to find target element '${selector}' after ${maxRetries} retries. Aborting initialization.`); } } }, retryInterval); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => waitForElement('#canvasA', initialize)); } else { waitForElement('#canvasA', initialize); } })();