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
+8 -1
View File
@@ -5,13 +5,16 @@ export interface MenuItem {
checked?: boolean;
disabled?: boolean;
separator?: boolean;
// Non-interactive title at the top (e.g. the server's name)
header?: boolean;
// Inline slider instead of a clickable item; the menu stays open while dragging
slider?: { value: number; min: number; max: number; step: number; format: (v: number) => string; oninput: (v: number) => void };
}
// One context menu for the whole app, opened at the pointer position
class MenuStore {
open = $state<{ x: number; y: number; items: MenuItem[] } | null>(null);
// keepFocus: leave focus (and the text selection) where it is, for text editing menus
open = $state<{ x: number; y: number; items: MenuItem[]; keepFocus?: boolean } | null>(null);
show(e: MouseEvent, items: MenuItem[]): void {
e.preventDefault();
@@ -19,6 +22,10 @@ class MenuStore {
this.open = { x: e.clientX, y: e.clientY, items };
}
showAt(x: number, y: number, items: MenuItem[], keepFocus = false): void {
this.open = { x, y, items, keepFocus };
}
close(): void {
this.open = null;
}