feat: implement web-driven voice connection with guild/channel selection and API integration

This commit is contained in:
MythEclipse
2026-05-13 18:23:20 +07:00
parent a5a794c590
commit 4dadcf3871
9 changed files with 937 additions and 72 deletions
+152
View File
@@ -121,6 +121,40 @@
border-left-color: var(--success);
background: rgba(67, 181, 129, 0.1);
}
.connection-panel {
gap: 0.75rem;
text-align: left;
}
.field-group {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.field-group label {
font-size: 0.8rem;
color: var(--text-muted);
}
.field-group select {
padding: 0.65rem;
border-radius: 8px;
border: 1px solid rgba(255,255,255,0.12);
background: #2f3136;
color: var(--text);
}
.button-row {
display: flex;
gap: 0.5rem;
}
.voice-status {
font-size: 0.85rem;
color: var(--text-muted);
}
</style>
</head>
<body>
@@ -128,6 +162,22 @@
<h1 style="margin-bottom: 0.5rem;">Discord Gateway v4</h1>
<p style="color: var(--text-muted); font-size: 0.8rem; margin-bottom: 2rem;">Optimized 24kHz Mono Bridge</p>
<div class="status-card connection-panel">
<div class="field-group">
<label for="guildSelect">Guild</label>
<select id="guildSelect"></select>
</div>
<div class="field-group">
<label for="channelSelect">Voice Channel</label>
<select id="channelSelect"></select>
</div>
<div class="button-row">
<button id="joinVoiceBtn" class="btn btn-success">Join Channel</button>
<button id="disconnectVoiceBtn" class="btn btn-danger">Disconnect</button>
</div>
<div id="voiceStatusText" class="voice-status">Not connected</div>
</div>
<div class="status-card">
<div style="display: flex; align-items: center; gap: 10px;">
<div id="statusIndicator" class="status-indicator"></div>
@@ -156,6 +206,11 @@
const listenStatus = document.getElementById('listenStatus');
const userList = document.getElementById('userList');
const visualizer = document.getElementById('visualizer');
const guildSelect = document.getElementById('guildSelect');
const channelSelect = document.getElementById('channelSelect');
const joinVoiceBtn = document.getElementById('joinVoiceBtn');
const disconnectVoiceBtn = document.getElementById('disconnectVoiceBtn');
const voiceStatusText = document.getElementById('voiceStatusText');
for (let i = 0; i < 32; i++) {
const bar = document.createElement('div');
@@ -179,6 +234,62 @@
const NOISE_GATE_HOLD_FRAMES = 3;
let noiseGateHold = 0;
async function apiRequest(url, options = {}) {
const response = await fetch(url, {
headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
...options,
});
if (!response.ok) {
const error = await response.json().catch(() => ({ message: response.statusText }));
throw new Error(error.message || response.statusText);
}
return response.json();
}
function setVoiceStatus(message, isError = false) {
voiceStatusText.innerText = message;
voiceStatusText.style.color = isError ? '#f04747' : 'var(--text-muted)';
}
function renderSelect(select, items, placeholder) {
select.replaceChildren();
const placeholderOption = document.createElement('option');
placeholderOption.value = '';
placeholderOption.textContent = placeholder;
select.appendChild(placeholderOption);
for (const item of items) {
const option = document.createElement('option');
option.value = item.id;
option.textContent = item.name;
select.appendChild(option);
}
}
async function loadGuilds() {
const guilds = await apiRequest('/api/guilds');
renderSelect(guildSelect, guilds, 'Select guild');
}
async function loadVoiceChannels(guildId) {
if (!guildId) {
renderSelect(channelSelect, [], 'Select voice channel');
return;
}
const channels = await apiRequest(`/api/guilds/${guildId}/voice-channels`);
renderSelect(channelSelect, channels, 'Select voice channel');
}
async function refreshVoiceStatus() {
const status = await apiRequest('/api/status');
if (status.connected) {
setVoiceStatus(`Connected to ${status.activeChannelName || status.activeChannelId}`);
} else if (status.ready) {
setVoiceStatus('Discord ready. Select a voice channel.');
} else {
setVoiceStatus('Discord client is not ready yet.');
}
}
function initWebSocket() {
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) return;
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
@@ -253,6 +364,45 @@
userTimelines.set(userIdHash, userNextStartTime);
}
guildSelect.onchange = async () => {
try {
await loadVoiceChannels(guildSelect.value);
} catch (error) {
setVoiceStatus(error.message, true);
}
};
joinVoiceBtn.onclick = async () => {
try {
if (!guildSelect.value || !channelSelect.value) {
setVoiceStatus('Select guild and voice channel first.', true);
return;
}
joinVoiceBtn.disabled = true;
const status = await apiRequest('/api/connect', {
method: 'POST',
body: JSON.stringify({ guildId: guildSelect.value, channelId: channelSelect.value }),
});
setVoiceStatus(`Connected to ${status.activeChannelName || status.activeChannelId}`);
} catch (error) {
setVoiceStatus(error.message, true);
} finally {
joinVoiceBtn.disabled = false;
}
};
disconnectVoiceBtn.onclick = async () => {
try {
disconnectVoiceBtn.disabled = true;
await apiRequest('/api/disconnect', { method: 'POST' });
setVoiceStatus('Disconnected.');
} catch (error) {
setVoiceStatus(error.message, true);
} finally {
disconnectVoiceBtn.disabled = false;
}
};
toggleBtn.onclick = async () => {
if (isStreaming) {
stopStreaming();
@@ -350,6 +500,8 @@
}
};
loadGuilds().catch((error) => setVoiceStatus(error.message, true));
refreshVoiceStatus().catch((error) => setVoiceStatus(error.message, true));
initWebSocket();
</script>
</body>