Add channel and permission management, sounds, themes and the public server list

- Channels: create, edit, delete, link and move (drag and drop for channels and
  people), right-click on free space in the channel list
- Permission editor with inherited rules, allow/deny per permission, groups and
  members; menus grey out actions the server does not allow
- Notification sounds for connection, channel, chat, mute and push-to-talk events;
  custom files and TeamSpeak 3 style sound packs (settings.ini), stored in IndexedDB
- Color schemes: Dark, Light, Midnight, Frost and Windows 95
- Public server list with live UDP ping, fetched through Chromium's network stack
- Kick, ban and connection-lost card with Reconnect
- Right-click menus for text fields, links, images and message authors
- Formatting toolbar in the message box, shared with the description editor
- Resizable channel list, menu headers, registration from the self menu only
- Fix: client-side pacing now mirrors Murmur's whole-second leaky bucket exactly,
  so quick actions are no longer dropped silently

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 00:54:03 +02:00
co-authored by Claude Opus 5.5
parent aa679dddcc
commit 9a3aeb29ad
53 changed files with 2684 additions and 170 deletions
+17
View File
@@ -80,6 +80,11 @@ class VoiceEngine {
private client: MumbleClient | null = null;
private sessionGains = new Map<number, number>();
private unsubscribe: (() => void)[] = [];
// Push-to-talk press/release, for the notification sounds
onPtt: ((down: boolean) => void) | null = null;
// Speaking (or pressing push to talk) while muted, for the reminder sound
onTalkingWhileMuted: (() => void) | null = null;
private lastMutedReminder = 0;
private frameCounter = 0; // Mumble sequence, in 10 ms frames
private sampleTime = 0; // encoder timestamps, microseconds
@@ -301,6 +306,12 @@ class VoiceEngine {
if (db > s.vadThreshold) this.lastVoiceAt = now;
wanted = now - this.lastVoiceAt < s.vadHoldMs;
}
// Speaking into a muted microphone: remind, at most every 8 seconds
const self = this.client?.self;
if (wanted && s.mode === 'vad' && self && (self.selfMute || self.mute) && !self.selfDeaf && now - this.lastMutedReminder > 8000) {
this.lastMutedReminder = now;
this.onTalkingWhileMuted?.();
}
wanted &&= this.canTalk() && !!this.encoder && this.encoder.state === 'configured';
if (wanted && !this.open) { this.open = true; this.transmitting = true; }
@@ -353,7 +364,13 @@ class VoiceEngine {
// Typing in a text field does not trigger push to talk
const t = e.target as HTMLElement | null;
if (down && t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
if (this.pttDown === down) return;
this.pttDown = down;
if (this.settings.mode === 'ptt' && this.client) {
const self = this.client.self;
if (down && self && (self.selfMute || self.mute)) this.onTalkingWhileMuted?.();
else this.onPtt?.(down);
}
}
// ─── Receiving ──────────────────────────────────────────────────────────────