control panel ui
This commit is contained in:
@@ -1,9 +1,131 @@
|
||||
<script lang="ts">
|
||||
//
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import ActivityPanel from "$lib/components/control-panel/activity-panel.svelte";
|
||||
import FriendsPanel from "$lib/components/control-panel/friends-panel.svelte";
|
||||
import GeneralPanel from "$lib/components/control-panel/general-panel.svelte";
|
||||
import NetworkPanel from "$lib/components/control-panel/network-panel.svelte";
|
||||
import type {
|
||||
PanelActions,
|
||||
PanelState,
|
||||
} from "$lib/components/control-panel/types";
|
||||
|
||||
const tabs = [
|
||||
{ id: "general", label: "General" },
|
||||
{ id: "friends", label: "Friends" },
|
||||
{ id: "network", label: "Network" },
|
||||
{ id: "activity", label: "Activity" },
|
||||
] as const;
|
||||
type TabId = (typeof tabs)[number]["id"];
|
||||
|
||||
let activeTab = $state<TabId>("general");
|
||||
let actions: Partial<Record<TabId, PanelActions>> = {};
|
||||
let panelStates = $state<Record<TabId, PanelState>>({
|
||||
general: { dirty: false, busy: false },
|
||||
friends: { dirty: false, busy: false },
|
||||
network: { dirty: false, busy: false },
|
||||
activity: { dirty: false, busy: false },
|
||||
});
|
||||
let anyDirty = $derived(tabs.some((tab) => panelStates[tab.id].dirty));
|
||||
let anyBusy = $derived(tabs.some((tab) => panelStates[tab.id].busy));
|
||||
|
||||
function register(
|
||||
name: string,
|
||||
nextActions: PanelActions,
|
||||
state: PanelState,
|
||||
) {
|
||||
const id = name as TabId;
|
||||
actions[id] = nextActions;
|
||||
if (
|
||||
panelStates[id].dirty !== state.dirty ||
|
||||
panelStates[id].busy !== state.busy
|
||||
) {
|
||||
panelStates[id] = state;
|
||||
}
|
||||
}
|
||||
|
||||
async function applyAll() {
|
||||
for (const tab of tabs) {
|
||||
if (!panelStates[tab.id].dirty) continue;
|
||||
if (!(await actions[tab.id]?.apply())) {
|
||||
activeTab = tab.id;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function hideWindow() {
|
||||
try {
|
||||
await getCurrentWindow().hide();
|
||||
} catch (error) {
|
||||
console.error("failed to hide control panel", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function confirm() {
|
||||
if (await applyAll()) await hideWindow();
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
tabs.forEach((tab) => actions[tab.id]?.reset());
|
||||
void hideWindow();
|
||||
}
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<div>
|
||||
<p>Control panel</p>
|
||||
<svelte:head><title>Wyd Properties</title></svelte:head>
|
||||
|
||||
<main
|
||||
class="flex h-full min-h-0 flex-col overflow-hidden bg-base-100 p-2 text-base-content"
|
||||
>
|
||||
<div class="tabs tabs-lift min-h-0 flex-1">
|
||||
{#each tabs as tab}
|
||||
<input
|
||||
id={`${tab.id}-tab`}
|
||||
type="radio"
|
||||
name="control_panel_tabs"
|
||||
class="tab"
|
||||
aria-label={`${tab.label}${panelStates[tab.id].dirty ? " *" : ""}`}
|
||||
value={tab.id}
|
||||
bind:group={activeTab}
|
||||
/>
|
||||
<div
|
||||
class="tab-content h-[calc(100%-1.75rem)] w-full overflow-hidden border border-base-300 bg-base-100 p-3"
|
||||
role="tabpanel"
|
||||
aria-labelledby={`${tab.id}-tab`}
|
||||
>
|
||||
<div class="h-full">
|
||||
{#if tab.id === "general"}
|
||||
<GeneralPanel {register} />
|
||||
{:else if tab.id === "friends"}
|
||||
<FriendsPanel {register} />
|
||||
{:else if tab.id === "network"}
|
||||
<NetworkPanel {register} />
|
||||
{:else}
|
||||
<ActivityPanel {register} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="flex shrink-0 justify-end gap-1.5 pt-2">
|
||||
<button
|
||||
class="btn min-w-16"
|
||||
type="button"
|
||||
disabled={anyBusy}
|
||||
onclick={confirm}>OK</button
|
||||
>
|
||||
<button
|
||||
class="btn min-w-16"
|
||||
type="button"
|
||||
disabled={anyBusy}
|
||||
onclick={cancel}>Cancel</button
|
||||
>
|
||||
<button
|
||||
class="btn min-w-16"
|
||||
type="button"
|
||||
disabled={!anyDirty || anyBusy}
|
||||
onclick={applyAll}>{anyBusy ? "Applying…" : "Apply"}</button
|
||||
>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { commands } from "$lib/bindings";
|
||||
import PanelMessage from "$lib/components/control-panel/panel-message.svelte";
|
||||
import Info from "$lib/icons/info.svelte";
|
||||
|
||||
let displayName = $state("");
|
||||
let friendId = $state("");
|
||||
let busy = $state(false);
|
||||
let error = $state("");
|
||||
|
||||
async function closeWindow() {
|
||||
await getCurrentWindow().close();
|
||||
}
|
||||
|
||||
async function submit(event: SubmitEvent) {
|
||||
event.preventDefault();
|
||||
const name = displayName.trim();
|
||||
const id = friendId.trim();
|
||||
if (!name || !id) {
|
||||
error = "Both display name and public key are required.";
|
||||
return;
|
||||
}
|
||||
|
||||
busy = true;
|
||||
error = "";
|
||||
try {
|
||||
await commands.createFriend({ id, displayName: name });
|
||||
await closeWindow();
|
||||
} catch (cause) {
|
||||
error = String(cause);
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Add Friend</title></svelte:head>
|
||||
<svelte:window
|
||||
onkeydown={(event) => event.key === "Escape" && !busy && void closeWindow()}
|
||||
/>
|
||||
|
||||
<form
|
||||
class="flex h-full flex-col bg-base-100 p-3 text-base-content"
|
||||
onsubmit={submit}
|
||||
>
|
||||
<div class="min-h-0 flex-1 space-y-2">
|
||||
<div>
|
||||
<h1 class="text-sm font-bold">Add a friend</h1>
|
||||
<p class="text-xs text-base-content/65">
|
||||
Enter the identity your friend shared with you.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if error}<PanelMessage kind="error">{error}</PanelMessage>{/if}
|
||||
|
||||
<fieldset class="fieldset border border-base-300 bg-base-100 p-3">
|
||||
<legend class="fieldset-legend px-1">Friend identity</legend>
|
||||
<label class="fieldset-label" for="friend-name">Display name</label>
|
||||
<input
|
||||
id="friend-name"
|
||||
class="input w-full"
|
||||
bind:value={displayName}
|
||||
maxlength="64"
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
<label class="fieldset-label mt-1" for="friend-key"
|
||||
>Identification Key <div
|
||||
class="tooltip tooltip-primary"
|
||||
data-tip="Verify with your friend before adding."
|
||||
>
|
||||
<div class="*:size-3">
|
||||
<Info />
|
||||
</div>
|
||||
</div></label
|
||||
>
|
||||
<textarea
|
||||
id="friend-key"
|
||||
class="textarea h-20 w-full resize-none font-mono text-[10px]"
|
||||
bind:value={friendId}
|
||||
required></textarea>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 justify-end gap-1.5 border-t border-base-300 pt-2">
|
||||
<button class="btn min-w-16" type="submit" disabled={busy}
|
||||
>{busy ? "Adding…" : "OK"}</button
|
||||
>
|
||||
<button
|
||||
class="btn min-w-16"
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onclick={closeWindow}>Cancel</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,121 @@
|
||||
<script lang="ts">
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { commands, type RemoteInput } from "$lib/bindings";
|
||||
import PanelMessage from "$lib/components/control-panel/panel-message.svelte";
|
||||
|
||||
let name = $state("");
|
||||
let address = $state("");
|
||||
let port = $state("");
|
||||
let busy = $state(false);
|
||||
let error = $state("");
|
||||
|
||||
async function closeWindow() {
|
||||
await getCurrentWindow().close();
|
||||
}
|
||||
|
||||
function remoteInput(): RemoteInput | null {
|
||||
const parsedPort = port ? Number(port) : null;
|
||||
if (!address.trim()) {
|
||||
error = "Server address is required.";
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
parsedPort !== null &&
|
||||
(!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535)
|
||||
) {
|
||||
error = "Port must be a whole number from 1 to 65535.";
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
address: address.trim(),
|
||||
name: name.trim() || null,
|
||||
port: parsedPort,
|
||||
};
|
||||
}
|
||||
|
||||
async function submit(event: SubmitEvent) {
|
||||
event.preventDefault();
|
||||
error = "";
|
||||
const remote = remoteInput();
|
||||
if (!remote) return;
|
||||
|
||||
busy = true;
|
||||
try {
|
||||
await commands.createRemote(remote);
|
||||
await closeWindow();
|
||||
} catch (cause) {
|
||||
error = String(cause);
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Add Server</title></svelte:head>
|
||||
<svelte:window
|
||||
onkeydown={(event) => event.key === "Escape" && !busy && void closeWindow()}
|
||||
/>
|
||||
|
||||
<form
|
||||
class="flex h-full flex-col bg-base-100 p-3 text-base-content"
|
||||
onsubmit={submit}
|
||||
>
|
||||
<div class="min-h-0 flex-1 space-y-2">
|
||||
<div>
|
||||
<h1 class="text-sm font-bold">Add a server</h1>
|
||||
<p class="text-xs text-base-content/65">
|
||||
Configure a remote server used to connect with friends.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if error}<PanelMessage kind="error">{error}</PanelMessage>{/if}
|
||||
|
||||
<fieldset class="fieldset border border-base-300 bg-base-100 p-3">
|
||||
<legend class="fieldset-legend px-1">Server details</legend>
|
||||
<label class="fieldset-label" for="remote-name"
|
||||
>Friendly name (optional)</label
|
||||
>
|
||||
<input
|
||||
id="remote-name"
|
||||
class="input w-full"
|
||||
bind:value={name}
|
||||
maxlength="64"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<label class="fieldset-label mt-1" for="remote-address">Address</label>
|
||||
<input
|
||||
id="remote-address"
|
||||
class="input w-full"
|
||||
bind:value={address}
|
||||
placeholder="example.net"
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
<label class="fieldset-label mt-1" for="remote-port"
|
||||
>Port (optional)</label
|
||||
>
|
||||
<input
|
||||
id="remote-port"
|
||||
class="input w-28"
|
||||
bind:value={port}
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
inputmode="numeric"
|
||||
placeholder="Default"
|
||||
/>
|
||||
<p class="fieldset-label">Leave port blank to use the server default.</p>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 justify-end gap-1.5 border-t border-base-300 pt-2">
|
||||
<button class="btn min-w-16" type="submit" disabled={busy}
|
||||
>{busy ? "Adding…" : "OK"}</button
|
||||
>
|
||||
<button
|
||||
class="btn min-w-16"
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onclick={closeWindow}>Cancel</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,172 @@
|
||||
<script lang="ts">
|
||||
import { page } from "$app/stores";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { onMount } from "svelte";
|
||||
import { commands, type RemoteInput } from "$lib/bindings";
|
||||
import PanelMessage from "$lib/components/control-panel/panel-message.svelte";
|
||||
|
||||
let name = $state("");
|
||||
let address = $state("");
|
||||
let port = $state("");
|
||||
let loading = $state(true);
|
||||
let busy = $state(false);
|
||||
let available = $state(false);
|
||||
let error = $state("");
|
||||
|
||||
const remoteId = $derived($page.params.id ?? "");
|
||||
|
||||
onMount(() => {
|
||||
let active = true;
|
||||
|
||||
if (!remoteId) {
|
||||
error = "No server was selected.";
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
void commands
|
||||
.getRemote(remoteId)
|
||||
.then((remote) => {
|
||||
if (!active) return;
|
||||
if (!remote) {
|
||||
error = "This server no longer exists.";
|
||||
return;
|
||||
}
|
||||
name = remote.name ?? "";
|
||||
address = remote.address;
|
||||
port = remote.port?.toString() ?? "";
|
||||
available = true;
|
||||
})
|
||||
.catch((cause) => {
|
||||
if (active) error = String(cause);
|
||||
})
|
||||
.finally(() => {
|
||||
if (active) loading = false;
|
||||
});
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
});
|
||||
|
||||
async function closeWindow() {
|
||||
await getCurrentWindow().close();
|
||||
}
|
||||
|
||||
function remoteInput(): RemoteInput | null {
|
||||
const parsedPort = port ? Number(port) : null;
|
||||
if (!address.trim()) {
|
||||
error = "Server address is required.";
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
parsedPort !== null &&
|
||||
(!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535)
|
||||
) {
|
||||
error = "Port must be a whole number from 1 to 65535.";
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
address: address.trim(),
|
||||
name: name.trim() || null,
|
||||
port: parsedPort,
|
||||
};
|
||||
}
|
||||
|
||||
async function submit(event: SubmitEvent) {
|
||||
event.preventDefault();
|
||||
error = "";
|
||||
const remote = remoteInput();
|
||||
if (!remote || !available) return;
|
||||
|
||||
busy = true;
|
||||
try {
|
||||
const updated = await commands.updateRemote(remoteId, remote);
|
||||
if (!updated) {
|
||||
error = "This server no longer exists.";
|
||||
available = false;
|
||||
busy = false;
|
||||
return;
|
||||
}
|
||||
await closeWindow();
|
||||
} catch (cause) {
|
||||
error = String(cause);
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Edit Server</title></svelte:head>
|
||||
<svelte:window
|
||||
onkeydown={(event) => event.key === "Escape" && !busy && void closeWindow()}
|
||||
/>
|
||||
|
||||
<form
|
||||
class="flex h-full flex-col bg-base-100 p-3 text-base-content"
|
||||
onsubmit={submit}
|
||||
>
|
||||
<div class="min-h-0 flex-1 space-y-2">
|
||||
<div>
|
||||
<h1 class="text-sm font-bold">Edit server</h1>
|
||||
<p class="text-xs text-base-content/65">
|
||||
Update how Wyd connects to this remote server.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if error}<PanelMessage kind="error">{error}</PanelMessage>{/if}
|
||||
|
||||
<fieldset
|
||||
class="fieldset border border-base-300 bg-base-100 p-3"
|
||||
disabled={loading || busy || !available}
|
||||
>
|
||||
<legend class="fieldset-legend px-1">Server details</legend>
|
||||
<label class="fieldset-label" for="remote-name"
|
||||
>Friendly name (optional)</label
|
||||
>
|
||||
<input
|
||||
id="remote-name"
|
||||
class="input w-full"
|
||||
bind:value={name}
|
||||
maxlength="64"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<label class="fieldset-label mt-1" for="remote-address">Address</label>
|
||||
<input
|
||||
id="remote-address"
|
||||
class="input w-full"
|
||||
bind:value={address}
|
||||
placeholder="example.net"
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
<label class="fieldset-label mt-1" for="remote-port"
|
||||
>Port (optional)</label
|
||||
>
|
||||
<input
|
||||
id="remote-port"
|
||||
class="input w-28"
|
||||
bind:value={port}
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
inputmode="numeric"
|
||||
placeholder="Default"
|
||||
/>
|
||||
<p class="fieldset-label">Leave port blank to use the server default.</p>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 justify-end gap-1.5 border-t border-base-300 pt-2">
|
||||
<button
|
||||
class="btn min-w-16"
|
||||
type="submit"
|
||||
disabled={loading || busy || !available}>{busy ? "Saving…" : "OK"}</button
|
||||
>
|
||||
<button
|
||||
class="btn min-w-16"
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onclick={closeWindow}>Cancel</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
Reference in New Issue
Block a user