TriX Executor's ChatBox (for territorial.io)!
当前为
// ==UserScript==
// @name TrixBox
// @namespace http://tampermonkey.net/
// @version 0.6.9
// @description TriX Executor's ChatBox (for territorial.io)!
// @author Painsel
// @match https://territorial.io/*
// @match https://fxclient.github.io/FXclient/*
// @grant GM_xmlhttpRequest
// @grant GM_info
// ==/UserScript==
(function() {
'use strict';
const CHATTABLE_HEADER_HEIGHT = 45; // pixels
// --- Theme Colors (for UI elements outside the iframe) ---
const theme = {
icon: '#2f3136',
modalBg: '#2f3136',
text: '#dcddde',
buttonPrimary: '#5865f2',
buttonSecondary: '#4f545c'
};
const SCRIPT_UPDATE_URL = 'https://update.greasyfork.org/scripts/555536/TrixBox.meta.js';
const SCRIPT_INSTALL_URL = 'https://update.greasyfork.org/scripts/555536/TrixBox.user.js';
// --- 1. UPDATE CHECKING LOGIC ---
const showUpdateModal = () => {
const modalStyle = `
.trixbox-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 100000; display: flex; align-items: center; justify-content: center; }
.trixbox-modal-content { background: ${theme.modalBg}; color: ${theme.text}; padding: 25px; border-radius: 10px; text-align: center; font-family: sans-serif; box-shadow: 0 5px 15px rgba(0,0,0,0.5); }
.trixbox-modal-content h2 { margin-top: 0; }
.trixbox-modal-content p { margin: 15px 0; }
.trixbox-modal-btn { color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; margin: 0 10px; font-size: 14px; }
.trixbox-modal-btn.primary { background: ${theme.buttonPrimary}; }
.trixbox-modal-btn.secondary { background: ${theme.buttonSecondary}; }
`;
const styleSheet = document.createElement("style");
styleSheet.innerText = modalStyle;
document.head.appendChild(styleSheet);
const modalOverlay = document.createElement('div');
modalOverlay.className = 'trixbox-modal-overlay';
modalOverlay.innerHTML = `
<div class="trixbox-modal-content">
<h2>OUTDATED VERSION</h2>
<p>You are using an Outdated version of TrixBox.<br>Please update for the best experience!</p>
<button id="trixbox-update-btn" class="trixbox-modal-btn primary">Update Now!</button>
<button id="trixbox-later-btn" class="trixbox-modal-btn secondary">Remind Me Later!</button>
</div>
`;
document.body.appendChild(modalOverlay);
document.getElementById('trixbox-update-btn').onclick = () => { window.location.href = SCRIPT_INSTALL_URL; };
document.getElementById('trixbox-later-btn').onclick = () => { modalOverlay.remove(); };
};
const checkForUpdates = () => {
try {
const localVersion = GM_info.script.version;
const lastSeenVersion = localStorage.getItem('trixbox-last-version');
// Show changelog if version has changed
if (lastSeenVersion !== localVersion) {
localStorage.setItem('trixbox-last-version', localVersion);
setTimeout(showChangelog, 500);
}
GM_xmlhttpRequest({
method: 'GET',
url: SCRIPT_UPDATE_URL,
onload: function(response) {
if (response.status !== 200) return;
const remoteVersionMatch = response.responseText.match(/@version\s+([0-9.]+)/);
if (remoteVersionMatch && remoteVersionMatch[1]) {
if (remoteVersionMatch[1] > localVersion) {
showUpdateModal();
}
}
}
});
} catch (e) { console.error('TrixBox: Update check failed.', e); }
};
const showChangelog = () => {
const modalStyle = `
.trixbox-changelog-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 100002; display: flex; align-items: center; justify-content: center; }
.trixbox-changelog-content { background: ${theme.modalBg}; color: ${theme.text}; padding: 25px; border-radius: 10px; font-family: sans-serif; box-shadow: 0 5px 15px rgba(0,0,0,0.5); width: 400px; max-height: 70vh; overflow-y: auto; }
.trixbox-changelog-content h2 { margin-top: 0; text-align: center; color: ${theme.buttonPrimary}; }
.trixbox-changelog-section { margin: 15px 0; }
.trixbox-changelog-version { font-weight: bold; color: ${theme.buttonPrimary}; margin-bottom: 8px; }
.trixbox-changelog-list { list-style: none; padding-left: 0; margin: 5px 0; }
.trixbox-changelog-list li { margin: 5px 0; padding-left: 20px; position: relative; }
.trixbox-changelog-list li:before { content: "✓"; position: absolute; left: 0; color: ${theme.buttonPrimary}; }
.trixbox-changelog-btn { width: 100%; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; margin-top: 15px; font-size: 14px; background: ${theme.buttonPrimary}; }
.trixbox-changelog-btn:hover { opacity: 0.9; }
`;
const styleSheet = document.createElement("style");
styleSheet.innerText = modalStyle;
document.head.appendChild(styleSheet);
const overlay = document.createElement('div');
overlay.className = 'trixbox-changelog-overlay';
overlay.innerHTML = `
<div class="trixbox-changelog-content">
<h2>What's New in v0.6.7</h2>
<div class="trixbox-changelog-section">
<div class="trixbox-changelog-version">v0.6.7 - Configurable Settings</div>
<ul class="trixbox-changelog-list">
<li>Configurable hotkey for disabling chat focus in main settings</li>
<li>New TrixBox settings header with toggle controls</li>
<li>Click-outside toggle for New TrixBox window</li>
<li>Persistent settings via localStorage</li>
<li>Enhanced settings modals with better UX</li>
</ul>
</div>
<div class="trixbox-changelog-section">
<div class="trixbox-changelog-version">Previous - v0.6.4 Features</div>
<ul class="trixbox-changelog-list">
<li>TrixBox Voice Chat - Triple chat system with voice support</li>
<li>Auto-launch voice chat connection via postMessage API</li>
<li>Microphone and camera permissions enabled</li>
<li>Enhanced UI with three chat selection options</li>
<li>Mobile optimizations with navbar-safe positioning</li>
</ul>
</div>
<button class="trixbox-changelog-btn">Got it!</button>
</div>
`;
document.body.appendChild(overlay);
overlay.querySelector('button').addEventListener('click', () => {
overlay.remove();
});
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
overlay.remove();
}
});
};
// --- 2. CREATE THE TOGGLE ICON ---
const toggleIcon = document.createElement('div');
toggleIcon.id = 'trixbox-toggle-icon';
toggleIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="${theme.text}" width="28px" height="28px"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/></svg>`;
Object.assign(toggleIcon.style, {
backgroundColor: theme.icon,
position: 'fixed', bottom: '20px', right: '20px', width: '50px', height: '50px',
borderRadius: '50%', display: 'flex', alignItems: 'center',
justifyContent: 'center', cursor: 'pointer', zIndex: '99998',
boxShadow: '0 2px 8px rgba(0,0,0,0.3)', transition: 'transform 0.2s ease-in-out'
});
toggleIcon.onmouseover = () => { toggleIcon.style.transform = 'scale(1.1)'; };
toggleIcon.onmouseout = () => { toggleIcon.style.transform = 'scale(1.0)'; };
document.body.appendChild(toggleIcon);
// --- 3. CREATE THE CHATBOX ---
const chatContainer = document.createElement('div');
chatContainer.id = 'trixbox-container';
Object.assign(chatContainer.style, {
position: 'fixed', bottom: 'auto', top: '70px', right: '15px', width: '350px', height: '500px',
zIndex: '99999', display: 'none', flexDirection: 'column',
boxShadow: '0 4px 12px rgba(0,0,0,0.3)', borderRadius: '8px', overflow: 'hidden',
touchAction: 'manipulation', WebkitUserSelect: 'none'
});
const dragHeader = document.createElement('div');
dragHeader.id = 'trixbox-header';
Object.assign(dragHeader.style, {
height: '30px', backgroundColor: 'rgb(210, 210, 255)', cursor: 'move',
userSelect: 'none', display: 'flex', alignItems: 'center',
justifyContent: 'space-between', padding: '0 5px 0 15px',
position: 'relative', zIndex: '1'
});
const headerTitle = document.createElement('span');
headerTitle.textContent = 'TrixBox Chat';
Object.assign(headerTitle.style, {
color: 'white', fontWeight: 'bold', fontSize: '14px',
textShadow: '1px 1px 0 rgb(160, 160, 230), 2px 2px 0 rgba(0, 0, 0, 0.15)'
});
dragHeader.appendChild(headerTitle);
const headerButtons = document.createElement('div');
headerButtons.style.display = 'flex';
headerButtons.style.alignItems = 'center';
const roomSelector = document.createElement('select');
roomSelector.id = 'room-selector';
roomSelector.className = 'top-bar-dropdown';
Object.assign(roomSelector.style, {
padding: '4px 8px', marginRight: '8px', backgroundColor: 'rgb(220, 220, 240)',
color: 'rgb(80, 80, 120)', border: '1px solid rgb(160, 160, 200)',
borderRadius: '3px', cursor: 'pointer', fontSize: '12px', fontWeight: 'bold'
});
const generalOption = document.createElement('option');
generalOption.value = '15234533';
generalOption.textContent = 'General';
roomSelector.appendChild(generalOption);
const loungeOption = document.createElement('option');
loungeOption.value = '21517181';
loungeOption.textContent = 'Lounge';
roomSelector.appendChild(loungeOption);
roomSelector.addEventListener('change', (e) => {
const newRoomId = e.target.value;
chatIframe.src = `https://iframe.chat/embed?chat=${newRoomId}`;
// Reinitialize Chattable for the new room
setTimeout(initializeChattable, 500);
});
headerButtons.appendChild(roomSelector);
const settingsButton = document.createElement('button');
settingsButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(80, 80, 120)" width="18px" height="18px"><path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.69-1.62-0.92L14.4,2.23C14.38,2,14.17,1.84,13.92,1.84H9.92 c-0.25,0-0.47,0.16-0.54,0.39L9.04,4.95C8.45,5.18,7.92,5.49,7.42,5.87L5.03,4.91c-0.22-0.08-0.47,0-0.59,0.22L2.52,8.45 c-0.11,0.2-0.06,0.47,0.12,0.61l2.03,1.58C4.59,11.36,4.56,11.68,4.56,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.69,1.62,0.92l0.34,2.72 c0.07,0.23,0.29,0.39,0.54,0.39h3.99c0.25,0,0.47-0.16,0.54-0.39l0.34-2.72c0.59-0.23,1.12-0.54,1.62-0.92l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.11-0.2,0.06-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/></svg>`;
Object.assign(settingsButton.style, {
background: 'none', border: 'none', cursor: 'pointer',
padding: '0 8px', lineHeight: '1', display: 'flex', alignItems: 'center'
});
settingsButton.title = 'Settings';
headerButtons.appendChild(settingsButton);
const reloadButton = document.createElement('button');
reloadButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(80, 80, 120)" width="18px" height="18px"><path d="M7 7v10c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V7M17 7V5c0-1.1-.9-2-2-2H9c-1.1 0-2 .9-2 2v2M7 11h10M9 14h6M7 17h10" stroke="rgb(80, 80, 120)" stroke-width="1.5" fill="none"/><path d="M16 4l3-3m0 6l-3-3" stroke="rgb(80, 80, 120)" stroke-width="1.5" fill="none" stroke-linecap="round"/></svg>`;
Object.assign(reloadButton.style, {
background: 'none', border: 'none', cursor: 'pointer',
padding: '0 8px', lineHeight: '1', display: 'flex', alignItems: 'center'
});
reloadButton.title = 'Reload Chat';
reloadButton.addEventListener('click', (e) => {
e.stopPropagation();
const currentSrc = chatIframe.src;
chatIframe.src = '';
setTimeout(() => {
chatIframe.src = currentSrc;
setTimeout(initializeChattable, 1000);
}, 100);
});
headerButtons.appendChild(reloadButton);
const closeButton = document.createElement('button');
closeButton.innerHTML = '×';
Object.assign(closeButton.style, {
background: 'none', border: 'none', color: 'rgb(80, 80, 120)',
fontSize: '24px', lineHeight: '1', cursor: 'pointer', padding: '0 8px'
});
headerButtons.appendChild(closeButton);
dragHeader.appendChild(headerButtons);
chatContainer.appendChild(dragHeader);
const iframeWrapper = document.createElement('div');
Object.assign(iframeWrapper.style, {
flexGrow: '1', overflow: 'hidden', position: 'relative'
});
chatContainer.appendChild(iframeWrapper);
// Load Chattable library (ensures up-to-date version)
const chatLibraryScript = document.createElement('script');
chatLibraryScript.src = 'https://iframe.chat/scripts/main.min.js';
document.head.appendChild(chatLibraryScript);
// Also add direct script tag for redundancy and automatic updates
const chattableScript = document.createElement('script');
chattableScript.src = 'https://iframe.chat/scripts/main.min.js';
document.head.appendChild(chattableScript);
const chatIframe = document.createElement('iframe');
chatIframe.src = 'https://iframe.chat/embed?chat=15234533';
chatIframe.id = 'chattable';
Object.assign(chatIframe.style, {
border: 'none', backgroundColor: 'transparent', position: 'absolute',
height: `calc(100% + ${CHATTABLE_HEADER_HEIGHT}px)`, width: '100%',
top: `-${CHATTABLE_HEADER_HEIGHT}px`, left: '0',
touchAction: 'manipulation', WebkitTouchCallout: 'none'
});
iframeWrapper.appendChild(chatIframe);
document.body.appendChild(chatContainer);
// Mobile input fix: Prevent enter key from creating new lines
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
try {
const iframeDoc = chatIframe.contentDocument || chatIframe.contentWindow.document;
if (iframeDoc) {
const inputField = iframeDoc.querySelector('textarea, input[type="text"], [contenteditable]');
if (inputField) {
inputField.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
// Trigger send by submitting the form or finding send button
const sendBtn = iframeDoc.querySelector('[class*="send"], button[aria-label*="send"], button:last-of-type');
if (sendBtn) sendBtn.click();
}
});
}
}
} catch (e) {
console.warn('TrixBox: Mobile input fix - cross-origin iframe, cannot modify input behavior');
}
}, 2000);
});
// --- 4. ADD FUNCTIONALITY ---
toggleIcon.addEventListener('click', (e) => {
e.stopPropagation();
showChatSelectionModal();
});
const showChatSelectionModal = () => {
const modalStyle = `
.trixbox-selection-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 100001; display: flex; align-items: center; justify-content: center; }
.trixbox-selection-content { background: ${theme.modalBg}; color: ${theme.text}; padding: 25px; border-radius: 10px; text-align: center; font-family: sans-serif; box-shadow: 0 5px 15px rgba(0,0,0,0.5); }
.trixbox-selection-content h2 { margin-top: 0; margin-bottom: 20px; }
.trixbox-selection-btn-group { display: flex; flex-direction: column; gap: 10px; }
.trixbox-selection-btn { flex: 1; color: white; border: none; padding: 12px 20px; border-radius: 5px; cursor: pointer; font-size: 14px; font-weight: bold; width: 100%; }
.trixbox-selection-btn.main { background: ${theme.buttonPrimary}; }
.trixbox-selection-btn.new { background: #7289da; }
.trixbox-selection-btn.voice { background: #43b581; }
.trixbox-selection-btn:hover { opacity: 0.9; }
`;
const styleSheet = document.createElement("style");
styleSheet.innerText = modalStyle;
document.head.appendChild(styleSheet);
const overlay = document.createElement('div');
overlay.className = 'trixbox-selection-overlay';
overlay.innerHTML = `
<div class="trixbox-selection-content">
<h2>Select Chat</h2>
<div class="trixbox-selection-btn-group">
<button id="trixbox-main-btn" class="trixbox-selection-btn main">TrixBox Main</button>
<button id="trixbox-new-btn" class="trixbox-selection-btn new">New TrixBox</button>
<button id="trixbox-voice-btn" class="trixbox-selection-btn voice">Voice Chat</button>
</div>
</div>
`;
document.body.appendChild(overlay);
document.getElementById('trixbox-main-btn').addEventListener('click', () => {
overlay.remove();
chatContainer.style.display = 'flex';
toggleIcon.style.display = 'none';
});
document.getElementById('trixbox-new-btn').addEventListener('click', () => {
overlay.remove();
showNewTrixBox();
});
document.getElementById('trixbox-voice-btn').addEventListener('click', () => {
overlay.remove();
showVoiceChat();
});
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
overlay.remove();
}
});
};
const showNewTrixBoxSettingsModal = (newChatContainer) => {
const modalStyle = `
.trixbox-new-settings-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 100001; display: flex; align-items: center; justify-content: center; }
.trixbox-new-settings-content { background: ${theme.modalBg}; color: ${theme.text}; padding: 25px; border-radius: 10px; text-align: left; font-family: sans-serif; box-shadow: 0 5px 15px rgba(0,0,0,0.5); width: 350px; max-height: 80vh; overflow-y: auto; }
.trixbox-new-settings-content h2 { margin-top: 0; text-align: center; }
.trixbox-new-settings-group { margin: 20px 0; }
.trixbox-new-settings-group label { display: block; margin-bottom: 8px; font-weight: bold; }
.trixbox-new-settings-toggle { display: flex; align-items: center; gap: 10px; margin: 10px 0; }
.trixbox-toggle-switch { width: 50px; height: 24px; background: #4f545c; border: none; border-radius: 12px; cursor: pointer; position: relative; transition: background 0.3s; }
.trixbox-toggle-switch.active { background: ${theme.buttonPrimary}; }
.trixbox-toggle-switch::after { content: ''; position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; background: white; border-radius: 50%; transition: left 0.3s; }
.trixbox-toggle-switch.active::after { left: 28px; }
.trixbox-new-settings-btn-group { display: flex; gap: 10px; margin-top: 20px; }
.trixbox-new-settings-btn { flex: 1; color: white; border: none; padding: 10px; border-radius: 5px; cursor: pointer; font-size: 14px; }
.trixbox-new-settings-btn.save { background: ${theme.buttonPrimary}; }
.trixbox-new-settings-btn.cancel { background: ${theme.buttonSecondary}; }
`;
const styleSheet = document.createElement("style");
styleSheet.innerText = modalStyle;
document.head.appendChild(styleSheet);
const overlay = document.createElement('div');
overlay.className = 'trixbox-new-settings-overlay';
overlay.innerHTML = `
<div class="trixbox-new-settings-content">
<h2>New TrixBox Settings</h2>
<div class="trixbox-new-settings-group">
<label>Close on Click Outside</label>
<button id="trixbox-new-close-toggle" class="trixbox-toggle-switch"></button>
<small style="color: #99aab5; font-size: 12px; display: block; margin-top: 5px;">Toggle: On/Off</small>
</div>
<div class="trixbox-new-settings-btn-group">
<button id="trixbox-new-settings-save" class="trixbox-new-settings-btn save">Save</button>
<button id="trixbox-new-settings-cancel" class="trixbox-new-settings-btn cancel">Cancel</button>
</div>
</div>
`;
document.body.appendChild(overlay);
const closeToggle = document.getElementById('trixbox-new-close-toggle');
const saveBtn = document.getElementById('trixbox-new-settings-save');
const cancelBtn = document.getElementById('trixbox-new-settings-cancel');
const isCloseOnClick = localStorage.getItem('trixbox-new-close-on-click') !== 'false';
if (isCloseOnClick) {
closeToggle.classList.add('active');
}
closeToggle.addEventListener('click', () => {
closeToggle.classList.toggle('active');
});
saveBtn.addEventListener('click', () => {
const isEnabled = closeToggle.classList.contains('active');
localStorage.setItem('trixbox-new-close-on-click', isEnabled ? 'true' : 'false');
overlay.remove();
});
cancelBtn.addEventListener('click', () => {
overlay.remove();
});
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
overlay.remove();
}
});
};
const showNewTrixBox = () => {
const newChatContainer = document.createElement('div');
newChatContainer.id = 'trixbox-new-container';
Object.assign(newChatContainer.style, {
position: 'fixed', bottom: 'auto', top: '70px', right: '15px', width: '350px', height: '500px',
zIndex: '99999', display: 'flex', flexDirection: 'column',
boxShadow: '0 4px 12px rgba(0,0,0,0.3)', borderRadius: '8px', overflow: 'hidden'
});
const newDragHeader = document.createElement('div');
Object.assign(newDragHeader.style, {
height: '30px', backgroundColor: 'rgb(210, 210, 255)', cursor: 'move',
userSelect: 'none', display: 'flex', alignItems: 'center',
justifyContent: 'space-between', padding: '0 5px 0 15px',
position: 'relative', zIndex: '1'
});
const newHeaderTitle = document.createElement('span');
newHeaderTitle.textContent = 'New TrixBox';
Object.assign(newHeaderTitle.style, {
color: 'white', fontWeight: 'bold', fontSize: '14px',
textShadow: '1px 1px 0 rgb(160, 160, 230), 2px 2px 0 rgba(0, 0, 0, 0.15)'
});
newDragHeader.appendChild(newHeaderTitle);
const newCloseButton = document.createElement('button');
newCloseButton.innerHTML = '×';
Object.assign(newCloseButton.style, {
background: 'none', border: 'none', color: 'rgb(80, 80, 120)',
fontSize: '24px', lineHeight: '1', cursor: 'pointer', padding: '0 8px'
});
newCloseButton.addEventListener('click', () => {
newChatContainer.remove();
toggleIcon.style.display = 'flex';
});
const newSettingsButton = document.createElement('button');
newSettingsButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(80, 80, 120)" width="16px" height="16px"><path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.69-1.62-0.92L14.4,2.23C14.38,2,14.17,1.84,13.92,1.84H9.92 c-0.25,0-0.47,0.16-0.54,0.39L9.04,4.95C8.45,5.18,7.92,5.49,7.42,5.87L5.03,4.91c-0.22-0.08-0.47,0-0.59,0.22L2.52,8.45 c-0.11,0.2-0.06,0.47,0.12,0.61l2.03,1.58C4.59,11.36,4.56,11.68,4.56,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.69,1.62,0.92l0.34,2.72 c0.07,0.23,0.29,0.39,0.54,0.39h3.99c0.25,0,0.47-0.16,0.54-0.39l0.34-2.72c0.59-0.23,1.12-0.54,1.62-0.92l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.11-0.2,0.06-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/></svg>`;
Object.assign(newSettingsButton.style, {
background: 'none', border: 'none', cursor: 'pointer',
padding: '0 8px', lineHeight: '1', display: 'flex', alignItems: 'center'
});
newSettingsButton.title = 'Settings';
newSettingsButton.addEventListener('click', (e) => {
e.stopPropagation();
showNewTrixBoxSettingsModal(newChatContainer);
});
newDragHeader.appendChild(newSettingsButton);
newDragHeader.appendChild(newCloseButton);
newChatContainer.appendChild(newDragHeader);
const newIframeWrapper = document.createElement('div');
Object.assign(newIframeWrapper.style, {
flexGrow: '1', overflow: 'hidden', position: 'relative'
});
const newIframe = document.createElement('iframe');
newIframe.src = 'https://www5.cbox.ws/box/?boxid=959661&boxtag=LgpZi2';
newIframe.width = '100%';
newIframe.height = '100%';
newIframe.allowTransparency = 'yes';
newIframe.allow = 'autoplay';
newIframe.frameBorder = '0';
newIframe.marginHeight = '0';
newIframe.marginWidth = '0';
newIframe.scrolling = 'auto';
Object.assign(newIframe.style, {
border: 'none', position: 'absolute', top: '0', left: '0'
});
newIframeWrapper.appendChild(newIframe);
newChatContainer.appendChild(newIframeWrapper);
// Hide and disable Cbox text in iframe footer
try {
const hideStyle = document.createElement('style');
hideStyle.textContent = `
a[href*="cbox.ws"],
a[href*="www.cbox.ws"],
.btn.Right.Interactive.Hand,
iframe ~ a[href*="cbox.ws"],
div[align="center"] a[href*="cbox.ws"] {
display: none !important;
pointer-events: none !important;
visibility: hidden !important;
}
`;
document.head.appendChild(hideStyle);
} catch (e) { }
document.body.appendChild(newChatContainer);
// Click outside to close New TrixBox (if enabled)
const closeOnClickOutside = () => {
const shouldCloseOnClick = localStorage.getItem('trixbox-new-close-on-click') !== 'false';
if (shouldCloseOnClick) {
document.addEventListener('mousedown', (e) => {
if (!newChatContainer.contains(e.target) && newChatContainer.parentElement) {
newChatContainer.remove();
toggleIcon.style.display = 'flex';
}
}, { once: true });
}
};
closeOnClickOutside();
// Drag functionality for new chat
let isDragging = false, offsetX, offsetY;
newDragHeader.addEventListener('mousedown', (e) => {
if (e.target !== newDragHeader && e.target !== newHeaderTitle) return;
isDragging = true;
offsetX = e.clientX - newChatContainer.getBoundingClientRect().left;
offsetY = e.clientY - newChatContainer.getBoundingClientRect().top;
newIframe.style.pointerEvents = 'none';
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
newChatContainer.style.left = `${e.clientX - offsetX}px`;
newChatContainer.style.top = `${e.clientY - offsetY}px`;
newChatContainer.style.bottom = 'auto';
newChatContainer.style.right = 'auto';
});
document.addEventListener('mouseup', () => {
if (!isDragging) return;
isDragging = false;
newIframe.style.pointerEvents = 'auto';
});
};
const showVoiceChat = () => {
const voiceChatContainer = document.createElement('div');
voiceChatContainer.id = 'trixbox-voice-container';
Object.assign(voiceChatContainer.style, {
position: 'fixed', bottom: 'auto', top: '70px', right: '15px', width: '400px', height: '600px',
zIndex: '99999', display: 'flex', flexDirection: 'column',
boxShadow: '0 4px 12px rgba(0,0,0,0.3)', borderRadius: '8px', overflow: 'hidden'
});
const voiceDragHeader = document.createElement('div');
Object.assign(voiceDragHeader.style, {
height: '30px', backgroundColor: 'rgb(210, 210, 255)', cursor: 'move',
userSelect: 'none', display: 'flex', alignItems: 'center',
justifyContent: 'space-between', padding: '0 5px 0 15px',
position: 'relative', zIndex: '1'
});
const voiceHeaderTitle = document.createElement('span');
voiceHeaderTitle.textContent = 'TrixBox Voice Chat';
Object.assign(voiceHeaderTitle.style, {
color: 'white', fontWeight: 'bold', fontSize: '14px',
textShadow: '1px 1px 0 rgb(160, 160, 230), 2px 2px 0 rgba(0, 0, 0, 0.15)'
});
voiceDragHeader.appendChild(voiceHeaderTitle);
const voiceCloseButton = document.createElement('button');
voiceCloseButton.innerHTML = '×';
Object.assign(voiceCloseButton.style, {
background: 'none', border: 'none', color: 'rgb(80, 80, 120)',
fontSize: '24px', lineHeight: '1', cursor: 'pointer', padding: '0 8px'
});
voiceCloseButton.addEventListener('click', () => {
voiceChatContainer.remove();
toggleIcon.style.display = 'flex';
});
voiceDragHeader.appendChild(voiceCloseButton);
voiceChatContainer.appendChild(voiceDragHeader);
const voiceIframeWrapper = document.createElement('div');
Object.assign(voiceIframeWrapper.style, {
flexGrow: '1', overflow: 'hidden', position: 'relative'
});
const voiceIframe = document.createElement('iframe');
voiceIframe.src = 'https://zp1v56uxy8rdx5ypatb0ockcb9tr6a-oci3--5173--cf284e50.local-credentialless.webcontainer-api.io/embed/test-6armm1';
voiceIframe.width = '400';
voiceIframe.height = '600';
voiceIframe.frameBorder = '0';
voiceIframe.allowFullscreen = true;
voiceIframe.allow = 'microphone; camera; autoplay';
Object.assign(voiceIframe.style, {
border: 'none', position: 'absolute', top: '0', left: '0',
width: '100%', height: '100%'
});
voiceIframeWrapper.appendChild(voiceIframe);
voiceChatContainer.appendChild(voiceIframeWrapper);
document.body.appendChild(voiceChatContainer);
// Auto-click the connect button when voice chat iframe loads (using postMessage to bypass CORS)
voiceIframe.addEventListener('load', () => {
setTimeout(() => {
voiceIframe.contentWindow.postMessage({ action: 'clickConnect' }, '*');
}, 500);
});
// Drag functionality for voice chat
let isDragging = false, offsetX, offsetY;
voiceDragHeader.addEventListener('mousedown', (e) => {
if (e.target !== voiceDragHeader && e.target !== voiceHeaderTitle) return;
isDragging = true;
offsetX = e.clientX - voiceChatContainer.getBoundingClientRect().left;
offsetY = e.clientY - voiceChatContainer.getBoundingClientRect().top;
voiceIframe.style.pointerEvents = 'none';
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
voiceChatContainer.style.left = `${e.clientX - offsetX}px`;
voiceChatContainer.style.top = `${e.clientY - offsetY}px`;
voiceChatContainer.style.bottom = 'auto';
voiceChatContainer.style.right = 'auto';
});
document.addEventListener('mouseup', () => {
if (!isDragging) return;
isDragging = false;
voiceIframe.style.pointerEvents = 'auto';
});
};
closeButton.addEventListener('click', (e) => {
e.stopPropagation();
chatContainer.style.display = 'none';
toggleIcon.style.display = 'flex';
});
settingsButton.addEventListener('click', (e) => {
e.stopPropagation();
showSettingsModal();
});
const showGuideModal = () => {
const modalStyle = `
.trixbox-guide-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 100001; display: flex; align-items: center; justify-content: center; }
.trixbox-guide-content { background: ${theme.modalBg}; color: ${theme.text}; padding: 25px; border-radius: 10px; font-family: sans-serif; box-shadow: 0 5px 15px rgba(0,0,0,0.5); width: 500px; max-height: 80vh; overflow-y: auto; }
.trixbox-guide-content h2 { margin-top: 0; text-align: center; color: ${theme.buttonPrimary}; }
.trixbox-guide-section { margin: 20px 0; }
.trixbox-guide-section h4 { color: ${theme.buttonPrimary}; margin-top: 15px; margin-bottom: 10px; }
.trixbox-guide-table { width: 100%; border-collapse: collapse; margin: 10px 0; }
.trixbox-guide-table th, .trixbox-guide-table td { border: 1px solid #4f545c; padding: 8px; text-align: left; font-size: 13px; }
.trixbox-guide-table th { background: #4f545c; color: white; font-weight: bold; }
.trixbox-guide-table code { background: #36393f; color: #88ff88; padding: 2px 4px; border-radius: 2px; }
.trixbox-guide-btn { width: 100%; color: white; border: none; padding: 10px; border-radius: 5px; cursor: pointer; font-size: 14px; background: ${theme.buttonPrimary}; margin-top: 15px; }
.trixbox-guide-btn:hover { opacity: 0.9; }
`;
const styleSheet = document.createElement("style");
styleSheet.innerText = modalStyle;
document.head.appendChild(styleSheet);
const overlay = document.createElement('div');
overlay.className = 'trixbox-guide-overlay';
overlay.innerHTML = `
<div class="trixbox-guide-content">
<h2>Message Markdown & Commands</h2>
<div class="trixbox-guide-section">
<h4>✨ Message Markdown</h4>
<table class="trixbox-guide-table">
<tr><th>Syntax</th><th>Result</th></tr>
<tr><td><code>*text*</code></td><td><i>Italic</i></td></tr>
<tr><td><code>**text**</code></td><td><b>Bold</b></td></tr>
<tr><td><code>~~text~~</code></td><td><s>Strikethrough</s></td></tr>
<tr><td><code>\`text\`</code></td><td><code>Code</code></td></tr>
<tr><td><code>>text</code></td><td>Blockquote</td></tr>
<tr><td><code>[Link](url)</code></td><td>Hyperlink</td></tr>
<tr><td><code></code></td><td>Image</td></tr>
</table>
</div>
<div class="trixbox-guide-section">
<h4>⚙️ Default Commands</h4>
<table class="trixbox-guide-table">
<tr><th>Command</th><th>Description</th></tr>
<tr><td><code>!connect</code></td><td>Reconnect to chat</td></tr>
<tr><td><code>!online</code></td><td>Show online users</td></tr>
<tr><td><code>!tutorial</code></td><td>Show chat tutorial</td></tr>
<tr><td><code>!help</code></td><td>Show help</td></tr>
</table>
<small style="color: #99aab5; font-size: 11px; display: block; margin-top: 8px;">Owner/Moderator commands: !clear, !lock, !unlock, !reset, !attach</small>
</div>
<div class="trixbox-guide-section">
<h4>🎮 Custom Commands</h4>
<p style="font-size: 13px; margin: 10px 0;"><code>!coinflip</code> - Flip a coin (Heads/Tails)</p>
</div>
<button id="trixbox-guide-close" class="trixbox-guide-btn">Got it!</button>
</div>
`;
document.body.appendChild(overlay);
document.getElementById('trixbox-guide-close').addEventListener('click', () => {
overlay.remove();
});
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
overlay.remove();
}
});
};
const showSettingsModal = () => {
const modalStyle = `
.trixbox-settings-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 100001; display: flex; align-items: center; justify-content: center; }
.trixbox-settings-content { background: ${theme.modalBg}; color: ${theme.text}; padding: 25px; border-radius: 10px; text-align: left; font-family: sans-serif; box-shadow: 0 5px 15px rgba(0,0,0,0.5); width: 350px; max-height: 80vh; overflow-y: auto; }
.trixbox-settings-content h2 { margin-top: 0; text-align: center; }
.trixbox-settings-group { margin: 20px 0; }
.trixbox-settings-group label { display: block; margin-bottom: 8px; font-weight: bold; }
.trixbox-settings-group input[type="text"], .trixbox-settings-group input[type="file"] { width: 100%; padding: 8px; background: #36393f; color: ${theme.text}; border: 1px solid #4f545c; border-radius: 4px; box-sizing: border-box; }
.trixbox-settings-group input[type="text"]::placeholder { color: #99aab5; }
.trixbox-profile-preview { width: 60px; height: 60px; border-radius: 50%; background: #4f545c; margin: 10px auto; overflow: hidden; display: flex; align-items: center; justify-content: center; }
.trixbox-profile-preview img { width: 100%; height: 100%; object-fit: cover; }
.trixbox-settings-btn-group { display: flex; gap: 10px; margin-top: 20px; }
.trixbox-settings-btn { flex: 1; color: white; border: none; padding: 10px; border-radius: 5px; cursor: pointer; font-size: 14px; }
.trixbox-settings-btn.save { background: ${theme.buttonPrimary}; }
.trixbox-settings-btn.cancel { background: ${theme.buttonSecondary}; }
`;
const styleSheet = document.createElement("style");
styleSheet.innerText = modalStyle;
document.head.appendChild(styleSheet);
const overlay = document.createElement('div');
overlay.className = 'trixbox-settings-overlay';
overlay.innerHTML = `
<div class="trixbox-settings-content">
<h2>Chat Settings</h2>
<div class="trixbox-settings-group">
<label>Username</label>
<input type="text" id="trixbox-username-input" placeholder="Enter your username" maxlength="32">
</div>
<div class="trixbox-settings-group">
<label>Profile Picture</label>
<div class="trixbox-profile-preview" id="trixbox-profile-preview"></div>
<input type="file" id="trixbox-profile-upload" accept="image/*">
</div>
<div class="trixbox-settings-group">
<label>Disable Focus Hotkey</label>
<input type="text" id="trixbox-hotkey-input" placeholder="Enter key (e.g., Escape, e, q)" maxlength="20">
<small style="color: #99aab5; font-size: 12px; display: block; margin-top: 5px;">Press any key or type a key name</small>
</div>
<div class="trixbox-settings-group">
<label>Notification Sound</label>
<input type="text" id="trixbox-notif-sound-input" placeholder="Enter URL or 'none' to disable" maxlength="500">
<small style="color: #99aab5; font-size: 12px; display: block; margin-top: 5px;">Example: https://example.com/sound.mp3 or type 'none'</small>
</div>
<div class="trixbox-settings-btn-group">
<button id="trixbox-settings-guide" class="trixbox-settings-btn save" style="background: #43b581;">Guide</button>
<button id="trixbox-settings-save" class="trixbox-settings-btn save">Save</button>
<button id="trixbox-settings-cancel" class="trixbox-settings-btn cancel">Cancel</button>
</div>
</div>
`;
document.body.appendChild(overlay);
const usernameInput = document.getElementById('trixbox-username-input');
const profileUpload = document.getElementById('trixbox-profile-upload');
const profilePreview = document.getElementById('trixbox-profile-preview');
const hotkeyInput = document.getElementById('trixbox-hotkey-input');
const notifSoundInput = document.getElementById('trixbox-notif-sound-input');
const guideBtn = document.getElementById('trixbox-settings-guide');
const saveBtn = document.getElementById('trixbox-settings-save');
const cancelBtn = document.getElementById('trixbox-settings-cancel');
let selectedProfileImage = localStorage.getItem('trixbox-profile-image') || null;
guideBtn.addEventListener('click', (e) => {
e.stopPropagation();
overlay.remove();
showGuideModal();
});
usernameInput.value = localStorage.getItem('trixbox-username') || '';
hotkeyInput.value = localStorage.getItem('trixbox-disable-focus-key') || 'Escape';
notifSoundInput.value = localStorage.getItem('trixbox-notification-sound') || '';
// Capture hotkey input
hotkeyInput.addEventListener('keydown', (e) => {
e.preventDefault();
hotkeyInput.value = e.key;
});
if (selectedProfileImage) {
profilePreview.innerHTML = `<img src="${selectedProfileImage}" alt="Profile">`;
}
profileUpload.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
selectedProfileImage = event.target.result;
profilePreview.innerHTML = `<img src="${selectedProfileImage}" alt="Profile">`;
};
reader.readAsDataURL(file);
}
});
saveBtn.addEventListener('click', () => {
const username = usernameInput.value.trim();
const hotkey = hotkeyInput.value.trim() || 'Escape';
const notifSound = notifSoundInput.value.trim() || '';
if (username) {
localStorage.setItem('trixbox-username', username);
localStorage.setItem('trixbox-disable-focus-key', hotkey);
localStorage.setItem('trixbox-notification-sound', notifSound);
if (selectedProfileImage) {
localStorage.setItem('trixbox-profile-image', selectedProfileImage);
}
// Update CSS variable for notification sound
updateNotificationSoundVariable(notifSound);
if (typeof chattable !== 'undefined') {
if (typeof chattable.setName === 'function') {
chattable.setName(username);
}
// Send profile picture to other users via payload
if (selectedProfileImage && typeof chattable.sendPayload === 'function') {
chattable.sendPayload({
type: 'profile-picture',
username: username,
image: selectedProfileImage
});
}
}
overlay.remove();
} else {
alert('Please enter a username');
}
});
cancelBtn.addEventListener('click', () => {
overlay.remove();
});
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
overlay.remove();
}
});
};
let isDragging = false, offsetX, offsetY;
dragHeader.addEventListener('mousedown', (e) => {
if (e.target !== dragHeader && e.target !== headerTitle) return;
isDragging = true;
offsetX = e.clientX - chatContainer.getBoundingClientRect().left;
offsetY = e.clientY - chatContainer.getBoundingClientRect().top;
chatIframe.style.pointerEvents = 'none';
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
chatContainer.style.left = `${e.clientX - offsetX}px`;
chatContainer.style.top = `${e.clientY - offsetY}px`;
chatContainer.style.bottom = 'auto';
chatContainer.style.right = 'auto';
});
document.addEventListener('mouseup', () => {
if (!isDragging) return;
isDragging = false;
chatIframe.style.pointerEvents = 'auto';
});
// --- NOTIFICATION SOUND UTILITY ---
const updateNotificationSoundVariable = (soundValue) => {
if (soundValue && soundValue.toLowerCase() !== 'none') {
// Set as CSS variable with URL
document.documentElement.style.setProperty('--notification-sfx', `url('${soundValue}')`);
} else {
// Disable notifications by setting to 'none'
document.documentElement.style.setProperty('--notification-sfx', 'none');
}
// Also inject CSS into iframe to override Chattable's default notification sound
injectNotificationSoundIntoChatIframe(soundValue);
};
const injectNotificationSoundIntoChatIframe = (soundValue) => {
try {
const css = `
:root {
${soundValue && soundValue.toLowerCase() !== 'none'
? `--notification-sfx: url('${soundValue}');`
: `--notification-sfx: none;`}
}
`;
chatIframe.contentWindow.postMessage({ type: 'css', content: css }, '*');
} catch (e) {
// Will work once iframe is loaded, retry on initialization
}
};
const playNotificationSound = () => {
const soundValue = localStorage.getItem('trixbox-notification-sound') || '';
if (soundValue && soundValue.toLowerCase() !== 'none') {
try {
const audio = new Audio(soundValue);
audio.play().catch(err => console.warn('TrixBox: Could not play notification sound', err));
} catch (e) {
console.warn('TrixBox: Error playing notification sound', e);
}
}
};
// Initialize notification sound on script load
const initializeNotificationSound = () => {
const savedSound = localStorage.getItem('trixbox-notification-sound') || '';
updateNotificationSoundVariable(savedSound);
};
// --- 5. DEFINE CUSTOM COMMANDS ---
chattable.commands = {
"coinflip": function(fullCommand) {
const result = Math.random() < 0.5 ? "Heads" : "Tails";
chattable.sendMessage(`🪙 Coin flip result: **${result}**`, "TrixBox", "", false);
}
};
// --- 6. INITIALIZE THE CHAT ---
const initializeChattable = () => {
if (typeof chattable !== 'undefined' && chattable.initialize) {
try {
chattable.initialize({
theme: "tendo"
});
// Inject custom notification sound CSS into iframe after initialization
setTimeout(() => {
const savedSound = localStorage.getItem('trixbox-notification-sound') || '';
const css = `
:root {
${savedSound && savedSound.toLowerCase() !== 'none'
? `--notification-sfx: url('${savedSound}');`
: `--notification-sfx: none;`}
}
`;
try {
chatIframe.contentWindow.postMessage(css, '*');
} catch (e) { }
}, 500);
// Handle profile picture payloads after initialization
chattable.on('payload', function(data) {
if (data.type === 'profile-picture') {
localStorage.setItem(`trixbox-profile-${data.username}`, data.image);
}
});
} catch (e) {
console.warn('TrixBox: Chattable initialization error, retrying...', e);
setTimeout(initializeChattable, 500);
}
} else {
// Retry if chattable is not yet loaded
setTimeout(initializeChattable, 500);
}
};
chatLibraryScript.onload = function() {
// Wait a tick for chattable to be fully ready
setTimeout(initializeChattable, 100);
};
// Fallback initialization if script loads via other method
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(initializeChattable, 500);
});
} else {
setTimeout(initializeChattable, 500);
}
// --- 7. INITIALIZE NOTIFICATION SOUND ---
initializeNotificationSound();
// --- 8. RUN THE UPDATE CHECKER ---
checkForUpdates();
// --- 8. HOTKEY HANDLING - Press configured key to release chat focus and use game hotkeys ---
document.addEventListener('keydown', (e) => {
const disableFocusKey = localStorage.getItem('trixbox-disable-focus-key') || 'Escape';
if (e.key === disableFocusKey || (disableFocusKey === 'Escape' && (e.key === 'Escape' || e.keyCode === 27))) {
// Remove focus from any active chat input
document.activeElement.blur();
// Hide main chat
if (chatContainer.style.display === 'flex') {
chatContainer.style.display = 'none';
toggleIcon.style.display = 'flex';
}
// Hide any open chat windows
const voiceContainer = document.getElementById('trixbox-voice-container');
const newContainer = document.getElementById('trixbox-new-container');
if (voiceContainer) voiceContainer.remove();
if (newContainer) newContainer.remove();
toggleIcon.style.display = 'flex';
}
});
})();