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 -3
View File
@@ -12,10 +12,12 @@
import IdentityWizard from './ui/IdentityWizard.svelte';
import QuickAudio from './ui/QuickAudio.svelte';
import UserInfoDialog from './ui/UserInfoDialog.svelte';
import ChannelDialog from './ui/ChannelDialog.svelte';
import PublicServers from './ui/PublicServers.svelte';
import { identities } from './lib/identities.svelte.ts';
import { session } from './lib/session.svelte.ts';
import { ui } from './lib/ui.svelte.ts';
import type { SavedServer } from './lib/settings.svelte.ts';
import { store, type SavedServer } from './lib/settings.svelte.ts';
let width = $state(window.innerWidth);
// wide: rail + sidebar + chat + panel; medium: panel as an overlay; narrow: everything but chat in drawers
@@ -26,6 +28,13 @@
const showPanel = $derived(ui.panelOpen && session.status === 'connected');
// Color scheme on the root element; dark is the stylesheet default
$effect(() => {
const t = store.settings.theme;
if (t && t !== 'dark') document.documentElement.dataset.theme = t;
else delete document.documentElement.dataset.theme;
});
// The panel starts open on wide layouts only
let lastLayout = '';
$effect(() => {
@@ -53,9 +62,9 @@
<div class="app {layout}">
{#if layout !== 'narrow' || navOpen}
<div class="nav" class:drawer={layout === 'narrow'}>
<ServerRail onadd={() => (editing = { server: null })} onedit={s => (editing = { server: s })}
<ServerRail onadd={() => (editing = { server: null })} onedit={s => (editing = { server: s })} onbrowse={() => (ui.publicServers = true)}
onsettings={() => (ui.settingsOpen = true)} />
<Sidebar onnavigate={() => { if (layout === 'narrow') navOpen = false; }} />
<Sidebar resizable={layout !== 'narrow'} onnavigate={() => { if (layout === 'narrow') navOpen = false; }} />
</div>
{#if layout === 'narrow'}<button class="scrim" aria-label="Close" onclick={() => (navOpen = false)}></button>{/if}
{/if}
@@ -73,6 +82,8 @@
</div>
{#if editing}<ConnectDialog server={editing.server} onclose={() => (editing = null)} />{/if}
{#if ui.addServer}<ConnectDialog server={null} prefill={ui.addServer} onclose={() => (ui.addServer = null)} />{/if}
{#if ui.publicServers}<PublicServers onclose={() => (ui.publicServers = false)} />{/if}
{#if ui.settingsOpen}<SettingsDialog tab={ui.settingsTab} onclose={() => { ui.settingsOpen = false; ui.settingsTab = 'voice'; }} />{/if}
{#if ui.quickAudio}<QuickAudio />{/if}
{#if ui.userInfo != null && session.status === 'connected'}
@@ -82,6 +93,9 @@
{#if ui.cert}<CertViewer {...ui.cert} onclose={() => (ui.cert = null)} />{/if}
{#if identities.loaded && !identities.setupDone}<IdentityWizard mode="setup" onclose={() => {}} />
{:else if ui.wizard}<IdentityWizard mode={ui.wizard} onclose={() => (ui.wizard = null)} />{/if}
{#if ui.channelDialog && session.status === 'connected'}
{#key ui.channelDialog}<ChannelDialog {...ui.channelDialog} onclose={() => (ui.channelDialog = null)} />{/key}
{/if}
{#if ui.prompt}<PromptDialog {...ui.prompt} onclose={() => (ui.prompt = null)} />{/if}
<ContextMenu />