Files
mumh5/src/ui/IdentityList.svelte
T
kibiandClaude Opus 5.5 6d29ee1485 Initial commit: mumh5, a modern Mumble client
Electron desktop app with a Svelte 5 interface for any Mumble server.

- Mumble protocol core: TLS, handshake, channels, users, text, plugin data,
  client-side pacing of Murmur's rate limits
- Voice: WebCodecs Opus over the TCP tunnel, voice activity, push to talk,
  always-on, devices, per-user volume and local mute
- Several servers at once, voice on one; server rail with icons and ordering
- Chat: channels, direct messages, side chat, file sharing through f0ckm,
  inline images without it, click-to-play YouTube
- Profiles with rich descriptions, registration, rename, nicknames,
  connection information and moderation menus
- Identity wizard, multiple identities, PKCS#12 import/export, desktop
  Mumble certificate import, certificate pinning and viewer
- Tray icon with voice state
- Unit, server and end-to-end tests against a real Murmur

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:28:43 +02:00

84 lines
3.7 KiB
Svelte

<script lang="ts">
import { native, type IdentitySummary } from '../lib/native.ts';
import { identities } from '../lib/identities.svelte.ts';
import { store } from '../lib/settings.svelte.ts';
import { ui } from '../lib/ui.svelte.ts';
let message = $state('');
const usedBy = (id: string) => store.servers.filter(s => s.identityId === id).map(s => s.label);
async function act(task: () => Promise<unknown>) {
message = '';
try {
await task();
await identities.refresh();
} catch (e) {
message = (e as Error).message.replace(/^Error invoking remote method '[^']+': (Error: )?/, '');
}
}
function exportId(i: IdentitySummary) {
ui.prompt = {
title: `Back up ${i.name}`, label: 'Password for the backup file', confirm: 'Choose location...', input: 'password',
text: 'The .p12 file contains your private key. Protect it with a password; the desktop Mumble client can import it.',
onsubmit: pw => act(async () => {
const path = await native!.identities.exportP12(i.id, pw);
if (path) message = `Saved to ${path}`;
})
};
}
function rename(i: IdentitySummary) {
ui.prompt = { title: 'Rename identity', label: 'Name', confirm: 'Rename', initial: i.name, onsubmit: n => act(() => native!.identities.rename(i.id, n)) };
}
function remove(i: IdentitySummary) {
const servers = usedBy(i.id);
ui.prompt = {
title: `Delete ${i.name}?`, label: '', confirm: 'Delete', danger: true, input: 'none',
text: `Registrations on servers are tied to this certificate and cannot be recovered without a backup.${servers.length ? ` Used by: ${servers.join(', ')}; they will use the default identity instead.` : ''}`,
onsubmit: () => act(() => native!.identities.remove(i.id))
};
}
</script>
<ul class="ids">
{#each identities.list as i (i.id)}
<li class:default={i.isDefault}>
<div class="head">
<strong>{i.name}</strong>
{#if i.isDefault}<span class="tag">default</span>{/if}
</div>
<code>{i.fingerprint}</code>
{#if usedBy(i.id).length}<p class="help">Used for {usedBy(i.id).join(', ')}</p>{/if}
<div class="actions">
{#if !i.isDefault}<button class="link" onclick={() => act(() => native!.identities.setDefault(i.id))}>Make default</button>{/if}
<button class="link" onclick={() => (ui.cert = { title: `Certificate: ${i.name}`, chain: [i.cert] })}>View certificate</button>
<button class="link" onclick={() => exportId(i)}>Back up</button>
<button class="link" onclick={() => rename(i)}>Rename</button>
<button class="link danger" onclick={() => remove(i)}>Delete</button>
</div>
</li>
{:else}
<li class="help">No identity yet; one is created when you first connect.</li>
{/each}
</ul>
<button class="btn" onclick={() => (ui.wizard = 'add')}>Add identity</button>
{#if message}<p class="help msg">{message}</p>{/if}
<style>
.ids { list-style: none; margin: 8px 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
li { padding: 10px 12px; background: var(--bg-0); border: 1px solid var(--line); }
li.default { border-color: var(--accent); }
.head { display: flex; align-items: center; gap: 8px; }
.tag { font-size: 11px; font-weight: 600; padding: 0 6px; background: var(--accent); color: var(--accent-text); }
code { display: block; margin-top: 4px; font: 11px var(--mono); color: var(--text-dim); overflow-wrap: anywhere; }
.actions { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 8px; }
.link { color: var(--link); font-size: 13px; }
.link:hover { text-decoration: underline; }
.link.danger { color: var(--danger); }
.help { margin: 4px 0 0; font-size: 13px; color: var(--text-dim); }
.msg { overflow-wrap: anywhere; }
</style>