utsuru 0.2.1

A WebRTC utility for forwarding track packets from a single source to multiple mirrors.
Documentation
<script lang="ts">
	let { states } = $props();

    let token = $state();
    let guild_id = $state();
    let channel_id = $state();

    let loading = $state(false);
    let loadingClass = $derived(loading ? "bg-indigo-300 cursor-not-allowed" : "bg-indigo-500 cursor-pointer");

    let responseText = $state([]);

    async function createMirror() {
        if (loading) return;
        loading = true;
        responseText = [];

        const response = await fetch("/api/mirrors?action=create", {
            method: "POST",
            headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify({
                token,
                guild_id: JSON.rawJSON(guild_id),
                channel_id: JSON.rawJSON(channel_id)
            })
        });

        if (!response.body) return;

        const reader = response.body.getReader();
        const decoder = new TextDecoder();

        while (true) {
            const { value, done } = await reader.read();
            if (done) break;

            const chunk = decoder.decode(value, { stream: true });
            responseText.push(chunk);
        }

        const flush = decoder.decode();
        if (flush.length) responseText.push(flush);

        loading = false;
        if (responseText.at(-1) !== "success") return;
        await states.refreshItems();
        states.closeModal();
    }
</script>

<div class="px-4 py-2.5 flex flex-col text-sm text-gray-100">
    {#if states}
        <div class="flex">
            <span class="flex-auto uppercase tracking-widest">Create Discord Mirror</span>
            <button class="my-0.5 cursor-pointer" onclick={() => states.closeModal()}>
                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
                    <!-- Icon from Lucide by Lucide Contributors - https://github.com/lucide-icons/lucide/blob/main/LICENSE -->
                    <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 6L6 18M6 6l12 12"/>
                </svg>
            </button>
        </div>
        <div class="h-px mt-2 mb-3 bg-zinc-700"></div>
        <div class="flex flex-col space-y-1">
            <input bind:value={token} type="text" class="min-w-64 h-8 px-3 bg-zinc-800 rounded-md border border-zinc-700" placeholder="Token">
            <input bind:value={guild_id} type="text" class="min-w-64 h-8 px-3 bg-zinc-800 rounded-md border border-zinc-700" placeholder="Guild ID">
            <input bind:value={channel_id} type="text" class="min-w-64 h-8 px-3 bg-zinc-800 rounded-md border border-zinc-700" placeholder="Voice Channel ID">
            <div class="mt-3 mb-1.5 flex flex-col items-center">
                <button class="py-1.5 px-2 mb-1.5 flex items-center space-x-1 rounded-lg {loadingClass}" onclick={createMirror}>
                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
                        <!-- Icon from Lucide by Lucide Contributors - https://github.com/lucide-icons/lucide/blob/main/LICENSE -->
                        <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14m-7-7v14"/>
                    </svg>
                    <span>Add</span>
                </button>
                {#if responseText.length}
                    <div class="flex items-center space-x-1.5">
                        {#if loading}
                            <svg class="animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" width="16" height="16" viewBox="0 0 24 24">
                                <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
                                <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                            </svg>
                        {/if}
                        <span>{responseText.at(-1)}</span>
                    </div>
                {/if}
            </div>
        </div>
    {/if}
</div>