TriX Executor's VOICE ChatBox (for territorial.io)!
// ==UserScript== // @name TrixBox (VOICE CHAT) // @namespace http://tampermonkey.net/ // @version 0.0.3 // @description TriX Executor's VOICE ChatBox (for territorial.io)! // @author Painsel // @match https://territorial.io/* // @match https://fxclient.github.io/FXclient/* // @grant GM_addStyle // @require https://unpkg.com/[email protected]/dist/peerjs.min.js // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // --- CSS STYLES --- // Using GM_addStyle as requested, ensuring consistent styling. const css = ` #voice-overlay { position: fixed; bottom: 20px; right: 20px; width: 300px; background: #1e1e1e; color: white; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); z-index: 99999; font-family: 'Inter', sans-serif; border: 1px solid #333; overflow: hidden; transition: all 0.3s ease; } #voice-header { background: #2d2d2d; padding: 10px 15px; font-weight: bold; display: flex; justify-content: space-between; align-items: center; cursor: move; /* Drag logic would be added here */ } #voice-body { padding: 15px; } .voice-btn { width: 100%; padding: 8px; margin-top: 10px; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; transition: background 0.2s ease; } .btn-green { background: #10b981; color: white; } .btn-green:hover { background: #0e9f6e; } .btn-red { background: #ef4444; color: white; } .btn-red:hover { background: #d03939; } .btn-yellow { background: #f59e0b; color: white; } .btn-yellow:hover { background: #d97706; } .btn-gray { background: #4b5563; color: white; } .btn-gray:hover { background: #374151; } .voice-input { width: 100%; padding: 8px; background: #333; border: 1px solid #444; color: white; border-radius: 4px; box-sizing: border-box; font-size: 14px; } .status-dot { height: 10px; width: 10px; background-color: #bbb; border-radius: 50%; display: inline-block; margin-right: 5px; } .status-connected { background-color: #10b981; } .status-calling { background-color: #3b82f6; } /* Blue for calling */ .status-ringing { background-color: #f59e0b; } /* Yellow for incoming */ .status-error { background-color: #ef4444; } `; // Inject CSS using GM_addStyle if (typeof GM_addStyle !== 'undefined') { GM_addStyle(css); } else { const style = document.createElement('style'); style.textContent = css; document.head.appendChild(style); } // --- HTML UI --- const overlay = document.createElement('div'); overlay.id = 'voice-overlay'; overlay.innerHTML = ` <div id="voice-header"> <span><span id="status-indicator" class="status-dot"></span> Voice Overlay</span> <button id="minimize-btn" style="background:none;border:none;color:#aaa;cursor:pointer;font-size:16px;line-height:1;">—</button> </div> <div id="voice-body"> <div style="font-size: 12px; color: #aaa; margin-bottom: 5px;">MY ID:</div> <input type="text" id="my-id" class="voice-input" readonly value="Initializing PeerJS..."> <div id="connection-controls"> <div style="font-size: 12px; color: #aaa; margin-top: 15px; margin-bottom: 5px;">CONNECT TO PEER:</div> <input type="text" id="friend-id" class="voice-input" placeholder="Paste friend's ID here"> <button id="connect-btn" class="voice-btn btn-green">Call Peer</button> </div> <button id="mute-btn" class="voice-btn btn-yellow" disabled style="display:none;">🎙️ Mute Microphone</button> <button id="disconnect-btn" class="voice-btn btn-red" style="display:none;">End Call</button> <div id="msg-area" style="margin-top:10px; font-size: 11px; color: #888; text-align: center;">Waiting for initialization...</div> <!-- This is where incoming call buttons appear --> <div id="incoming-call-prompt" style="display:none; text-align:center;"> <div class="voice-btn btn-yellow" style="margin-top: 5px; cursor:default;">Incoming Call!</div> <div class="flex space-x-2" style="display:flex;"> <button id="accept-call-btn" class="voice-btn btn-green flex-1" style="margin-right:5px;">Accept</button> <button id="reject-call-btn" class="voice-btn btn-red flex-1" style="margin-left:5px;">Reject</button> </div> </div> </div> `; document.body.appendChild(overlay); // Hidden audio element for remote stream const audio = document.createElement('audio'); audio.id = 'remote-audio'; audio.autoplay = true; document.body.appendChild(audio); // --- LOGIC --- let peer = null; let localStream = null; let activeCall = null; let isMuted = false; // Element getters const getEl = (id) => document.getElementById(id); // Initialize PeerJS const myPeerId = "trix_user_" + Math.floor(Math.random() * 100000); try { // PeerJS connects to a default signaling server hosted by PeerJS peer = new Peer(myPeerId); peer.on('open', (id) => { getEl('my-id').value = id; getEl('status-indicator').className = 'status-dot status-connected'; updateStatus("Online & Ready. Share your ID."); }); peer.on('call', (call) => { // Non-blocking UI prompt replaces the browser's 'confirm' handleIncomingCallUI(call); }); peer.on('error', (err) => { console.error("PeerJS Error:", err); updateStatus(`Error: ${err.type}`); if (activeCall) endCallUI(); }); } catch (e) { updateStatus("Error initializing PeerJS."); console.error("PeerJS init failed (possible CSP or network block):", e); } // --- Core Functions --- // Get Mic Function async function getMic() { if (localStream) return localStream; try { localStream = await navigator.mediaDevices.getUserMedia({ audio: true }); return localStream; } catch (err) { updateStatus("Error: Mic permission denied or not found."); console.error("Mic access error:", err); // Non-blocking UI feedback instead of alert throw err; } } // Toggle Mute Function function toggleMute() { if (!localStream) return; isMuted = !isMuted; localStream.getAudioTracks().forEach(track => { track.enabled = !isMuted; }); const btn = getEl('mute-btn'); if (isMuted) { btn.textContent = '🔇 Unmute Microphone'; btn.classList.remove('btn-yellow'); btn.classList.add('btn-gray'); updateStatus("Microphone muted."); } else { btn.textContent = '🎙️ Mute Microphone'; btn.classList.remove('btn-gray'); btn.classList.add('btn-yellow'); updateStatus("Microphone unmuted."); } } // Handle Incoming Call UI (Replaces browser confirm) function handleIncomingCallUI(call) { if (activeCall) { console.log("Busy, rejecting incoming call."); return; // Already in a call } updateStatus(`Incoming call from ${call.peer}!`); getEl('incoming-call-prompt').style.display = 'block'; getEl('connection-controls').style.display = 'none'; // Clear previous listeners to prevent multiple responses getEl('accept-call-btn').replaceWith(getEl('accept-call-btn').cloneNode(true)); getEl('reject-call-btn').replaceWith(getEl('reject-call-btn').cloneNode(true)); const acceptBtn = getEl('accept-call-btn'); const rejectBtn = getEl('reject-call-btn'); const resetUI = () => { getEl('incoming-call-prompt').style.display = 'none'; getEl('connection-controls').style.display = 'block'; updateStatus("Online & Ready. Share your ID."); }; acceptBtn.addEventListener('click', async () => { try { await getMic(); call.answer(localStream); handleStream(call); resetUI(); } catch (e) { // Mic failure handled in getMic() resetUI(); call.close(); } }, { once: true }); rejectBtn.addEventListener('click', () => { call.close(); resetUI(); }, { once: true }); } // Handle Active Call Stream function handleStream(call) { activeCall = call; getEl('connect-btn').style.display = 'none'; getEl('disconnect-btn').style.display = 'block'; getEl('mute-btn').style.display = 'block'; getEl('status-indicator').className = 'status-dot status-connected'; updateStatus("Connected to " + call.peer); call.on('stream', (remoteStream) => { audio.srcObject = remoteStream; }); call.on('close', () => { endCallUI(); }); call.on('error', (err) => { console.error("Call Error:", err); updateStatus("Call Error. Ending..."); endCallUI(); }); } function endCallUI() { if(activeCall) { activeCall.close(); activeCall = null; } if(localStream) { localStream.getTracks().forEach(track => track.stop()); localStream = null; } // Reset UI state getEl('connect-btn').style.display = 'block'; getEl('disconnect-btn').style.display = 'none'; getEl('mute-btn').style.display = 'none'; getEl('status-indicator').className = 'status-dot status-connected'; // Back to peer online audio.srcObject = null; // Reset mute button state isMuted = false; getEl('mute-btn').textContent = '🎙️ Mute Microphone'; getEl('mute-btn').classList.remove('btn-gray'); getEl('mute-btn').classList.add('btn-yellow'); // Ensure controls are visible after call is over getEl('connection-controls').style.display = 'block'; getEl('incoming-call-prompt').style.display = 'none'; updateStatus("Call Ended. Ready."); } function updateStatus(msg) { getEl('msg-area').innerText = msg; } // --- EVENT LISTENERS --- getEl('connect-btn').addEventListener('click', async () => { const friendId = getEl('friend-id').value.trim(); if (!friendId) return updateStatus("Please enter a friend's ID."); try { updateStatus("Calling " + friendId + "..."); getEl('status-indicator').className = 'status-dot status-calling'; await getMic(); const call = peer.call(friendId, localStream); handleStream(call); } catch (e) { // Error handled in getMic() getEl('status-indicator').className = 'status-dot status-connected'; } }); getEl('disconnect-btn').addEventListener('click', () => { endCallUI(); }); getEl('mute-btn').addEventListener('click', () => { toggleMute(); }); // Simple Minimize Logic const body = getEl('voice-body'); const header = getEl('voice-header'); getEl('minimize-btn').addEventListener('click', () => { if (body.style.display === 'none') { body.style.display = 'block'; getEl('minimize-btn').textContent = '—'; header.style.borderBottom = '1px solid #333'; } else { body.style.display = 'none'; getEl('minimize-btn').textContent = '◻'; header.style.borderBottom = 'none'; } }); })();