server protocol & ports UX improvement
This commit is contained in:
+9
-2
@@ -81,11 +81,18 @@
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
button {
|
||||
@apply btn-sm;
|
||||
}
|
||||
|
||||
input {
|
||||
@apply input-sm;
|
||||
}
|
||||
|
||||
select {
|
||||
@apply select-sm;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "w95f";
|
||||
src: url("/w95f.woff2") format("woff2");
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
import { messages } from "$lib/i18n";
|
||||
import {
|
||||
containsScheme,
|
||||
pastedServerAddress,
|
||||
type ServerConnectionType,
|
||||
} from "$lib/server-endpoint";
|
||||
|
||||
let {
|
||||
idPrefix,
|
||||
address = $bindable(),
|
||||
port = $bindable(),
|
||||
connectionType = $bindable(),
|
||||
disabled = false,
|
||||
required = false,
|
||||
labelClass = "fieldset-label",
|
||||
}: {
|
||||
idPrefix: string;
|
||||
address: string;
|
||||
port: string;
|
||||
connectionType: ServerConnectionType;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
labelClass?: string;
|
||||
} = $props();
|
||||
|
||||
let hasTypedScheme = $derived(containsScheme(address));
|
||||
let portPlaceholder = $derived(
|
||||
connectionType === "https"
|
||||
? "443"
|
||||
: connectionType === "http"
|
||||
? "80"
|
||||
: "27520",
|
||||
);
|
||||
|
||||
function pasteAddress(event: ClipboardEvent) {
|
||||
const pasted = pastedServerAddress(
|
||||
event.clipboardData?.getData("text") ?? "",
|
||||
);
|
||||
if (!pasted) return;
|
||||
event.preventDefault();
|
||||
address = pasted.address;
|
||||
connectionType = pasted.connectionType;
|
||||
port = pasted.port;
|
||||
}
|
||||
</script>
|
||||
|
||||
<label class={`${labelClass} mt-1`} for={`${idPrefix}-address`}
|
||||
>{$messages.common_address()}</label
|
||||
>
|
||||
<div class="join w-full">
|
||||
<select
|
||||
id={`${idPrefix}-connection-type`}
|
||||
class="select join-item w-auto"
|
||||
bind:value={connectionType}
|
||||
{disabled}
|
||||
aria-label={$messages.common_connection_type()}
|
||||
>
|
||||
<option value="https">https://</option>
|
||||
<option value="http">http://</option>
|
||||
<option value="direct">{$messages.common_connection_direct()}</option>
|
||||
</select>
|
||||
<input
|
||||
id={`${idPrefix}-address`}
|
||||
class:input-error={hasTypedScheme}
|
||||
class="input join-item min-w-0 flex-1"
|
||||
bind:value={address}
|
||||
placeholder="example.net"
|
||||
autocomplete="off"
|
||||
{required}
|
||||
{disabled}
|
||||
onpaste={pasteAddress}
|
||||
aria-invalid={hasTypedScheme}
|
||||
aria-describedby={hasTypedScheme ? `${idPrefix}-address-error` : undefined}
|
||||
/>
|
||||
</div>
|
||||
{#if hasTypedScheme}
|
||||
<p id={`${idPrefix}-address-error`} class="text-error text-xs">
|
||||
{$messages.error_server_address_scheme()}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<label class={`${labelClass} mt-1`} for={`${idPrefix}-port`}
|
||||
>{$messages.common_optional_port()}</label
|
||||
>
|
||||
<input
|
||||
id={`${idPrefix}-port`}
|
||||
class="input w-28"
|
||||
bind:value={port}
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
inputmode="numeric"
|
||||
placeholder={portPlaceholder}
|
||||
{disabled}
|
||||
/>
|
||||
@@ -0,0 +1,64 @@
|
||||
export type ServerConnectionType = "https" | "http" | "direct";
|
||||
|
||||
const SCHEME = /^(https?|wss?):\/\//i;
|
||||
|
||||
export function connectionTypeFromScheme(scheme: string): ServerConnectionType {
|
||||
return scheme.toLowerCase().startsWith("https") ||
|
||||
scheme.toLowerCase().startsWith("wss")
|
||||
? "https"
|
||||
: "http";
|
||||
}
|
||||
|
||||
export function splitServerAddress(address: string): {
|
||||
address: string;
|
||||
connectionType: ServerConnectionType;
|
||||
} {
|
||||
const match = address.match(SCHEME);
|
||||
if (!match) return { address, connectionType: "direct" };
|
||||
return {
|
||||
address: address.slice(match[0].length),
|
||||
connectionType: connectionTypeFromScheme(match[1]),
|
||||
};
|
||||
}
|
||||
|
||||
export function storedServerAddress(
|
||||
address: string,
|
||||
connectionType: ServerConnectionType,
|
||||
): string {
|
||||
const host = address.trim();
|
||||
if (connectionType === "direct") return host;
|
||||
return `${connectionType}://${host}`;
|
||||
}
|
||||
|
||||
export function containsScheme(address: string): boolean {
|
||||
return SCHEME.test(address.trim());
|
||||
}
|
||||
|
||||
export function pastedServerAddress(value: string): {
|
||||
address: string;
|
||||
connectionType: ServerConnectionType;
|
||||
port: string;
|
||||
} | null {
|
||||
const trimmed = value.trim();
|
||||
if (!SCHEME.test(trimmed)) return null;
|
||||
|
||||
try {
|
||||
const url = new URL(trimmed);
|
||||
if (
|
||||
url.username ||
|
||||
url.password ||
|
||||
(url.pathname !== "/" && url.pathname !== "") ||
|
||||
url.search ||
|
||||
url.hash
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
address: url.hostname,
|
||||
connectionType: connectionTypeFromScheme(url.protocol),
|
||||
port: url.port,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,18 @@
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { commands, type RemoteInput } from "$lib/bindings";
|
||||
import PanelMessage from "$lib/components/control-panel/panel-message.svelte";
|
||||
import ServerEndpointFields from "$lib/components/server-endpoint-fields.svelte";
|
||||
import { errorMessage, messages } from "$lib/i18n";
|
||||
import {
|
||||
containsScheme,
|
||||
storedServerAddress,
|
||||
type ServerConnectionType,
|
||||
} from "$lib/server-endpoint";
|
||||
|
||||
let name = $state("");
|
||||
let address = $state("");
|
||||
let port = $state("");
|
||||
let connectionType = $state<ServerConnectionType>("https");
|
||||
let busy = $state(false);
|
||||
let error = $state("");
|
||||
|
||||
@@ -24,6 +31,10 @@
|
||||
error = $messages.error_server_address_required();
|
||||
return null;
|
||||
}
|
||||
if (containsScheme(address)) {
|
||||
error = $messages.error_server_address_scheme();
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
parsedPort !== null &&
|
||||
(!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535)
|
||||
@@ -32,7 +43,7 @@
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
address: address.trim(),
|
||||
address: storedServerAddress(address, connectionType),
|
||||
name: name.trim() || null,
|
||||
port: parsedPort,
|
||||
};
|
||||
@@ -79,28 +90,13 @@
|
||||
maxlength="64"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<label class="fieldset-label mt-1" for="remote-address">{$messages.common_address()}</label>
|
||||
<input
|
||||
id="remote-address"
|
||||
class="input w-full"
|
||||
bind:value={address}
|
||||
placeholder="example.net"
|
||||
autocomplete="off"
|
||||
<ServerEndpointFields
|
||||
idPrefix="remote"
|
||||
bind:address
|
||||
bind:port
|
||||
bind:connectionType
|
||||
required
|
||||
/>
|
||||
<label class="fieldset-label mt-1" for="remote-port"
|
||||
>{$messages.common_optional_port()}</label
|
||||
>
|
||||
<input
|
||||
id="remote-port"
|
||||
class="input w-28"
|
||||
bind:value={port}
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
inputmode="numeric"
|
||||
placeholder="27520"
|
||||
/>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,11 +4,19 @@
|
||||
import { onMount } from "svelte";
|
||||
import { commands, type RemoteInput } from "$lib/bindings";
|
||||
import PanelMessage from "$lib/components/control-panel/panel-message.svelte";
|
||||
import ServerEndpointFields from "$lib/components/server-endpoint-fields.svelte";
|
||||
import { errorMessage, messages } from "$lib/i18n";
|
||||
import {
|
||||
containsScheme,
|
||||
splitServerAddress,
|
||||
storedServerAddress,
|
||||
type ServerConnectionType,
|
||||
} from "$lib/server-endpoint";
|
||||
|
||||
let name = $state("");
|
||||
let address = $state("");
|
||||
let port = $state("");
|
||||
let connectionType = $state<ServerConnectionType>("https");
|
||||
let loading = $state(true);
|
||||
let busy = $state(false);
|
||||
let available = $state(false);
|
||||
@@ -38,7 +46,9 @@
|
||||
return;
|
||||
}
|
||||
name = remote.name ?? "";
|
||||
address = remote.address;
|
||||
const endpoint = splitServerAddress(remote.address);
|
||||
address = endpoint.address;
|
||||
connectionType = endpoint.connectionType;
|
||||
port = remote.port?.toString() ?? "";
|
||||
available = true;
|
||||
})
|
||||
@@ -64,6 +74,10 @@
|
||||
error = $messages.error_server_address_required();
|
||||
return null;
|
||||
}
|
||||
if (containsScheme(address)) {
|
||||
error = $messages.error_server_address_scheme();
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
parsedPort !== null &&
|
||||
(!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535)
|
||||
@@ -72,7 +86,7 @@
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
address: address.trim(),
|
||||
address: storedServerAddress(address, connectionType),
|
||||
name: name.trim() || null,
|
||||
port: parsedPort,
|
||||
};
|
||||
@@ -135,28 +149,14 @@
|
||||
maxlength="64"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<label class="fieldset-label mt-1" for="remote-address">{$messages.common_address()}</label>
|
||||
<input
|
||||
id="remote-address"
|
||||
class="input w-full"
|
||||
bind:value={address}
|
||||
placeholder="example.net"
|
||||
autocomplete="off"
|
||||
<ServerEndpointFields
|
||||
idPrefix="remote"
|
||||
bind:address
|
||||
bind:port
|
||||
bind:connectionType
|
||||
disabled={loading || busy || !available}
|
||||
required
|
||||
/>
|
||||
<label class="fieldset-label mt-1" for="remote-port"
|
||||
>{$messages.common_optional_port()}</label
|
||||
>
|
||||
<input
|
||||
id="remote-port"
|
||||
class="input w-28"
|
||||
bind:value={port}
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
inputmode="numeric"
|
||||
placeholder="27520"
|
||||
/>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
import SkinStep from "./components/skin-step.svelte";
|
||||
import WelcomeStep from "./components/welcome-step.svelte";
|
||||
import { errorMessage, messages, type MessageCatalog } from "$lib/i18n";
|
||||
import {
|
||||
containsScheme,
|
||||
storedServerAddress,
|
||||
type ServerConnectionType,
|
||||
} from "$lib/server-endpoint";
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
|
||||
type StepId =
|
||||
@@ -99,6 +104,7 @@
|
||||
let serverName = $state("");
|
||||
let serverAddress = $state("");
|
||||
let serverPort = $state("");
|
||||
let serverConnectionType = $state<ServerConnectionType>("https");
|
||||
let savedServerId = $state<string | null>(null);
|
||||
let friendId = $state("");
|
||||
let savedFriendId = $state<string | null>(null);
|
||||
@@ -191,6 +197,9 @@
|
||||
|
||||
function remoteInput(): RemoteInput | null {
|
||||
if (!serverAddress.trim()) return null;
|
||||
if (containsScheme(serverAddress)) {
|
||||
throw new Error($messages.error_server_address_scheme());
|
||||
}
|
||||
const port = serverPort ? Number(serverPort) : null;
|
||||
if (
|
||||
port !== null &&
|
||||
@@ -199,7 +208,7 @@
|
||||
throw new Error($messages.error_port_invalid());
|
||||
}
|
||||
return {
|
||||
address: serverAddress.trim(),
|
||||
address: storedServerAddress(serverAddress, serverConnectionType),
|
||||
name: serverName.trim() || null,
|
||||
port,
|
||||
};
|
||||
@@ -370,6 +379,7 @@
|
||||
bind:serverName
|
||||
bind:serverAddress
|
||||
bind:serverPort
|
||||
bind:serverConnectionType
|
||||
{busy}
|
||||
/>
|
||||
{:else if step.id === "friend"}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
<script lang="ts">
|
||||
import PanelMessage from "$lib/components/control-panel/panel-message.svelte";
|
||||
import ServerEndpointFields from "$lib/components/server-endpoint-fields.svelte";
|
||||
import { messages } from "$lib/i18n";
|
||||
import type { ServerConnectionType } from "$lib/server-endpoint";
|
||||
|
||||
let {
|
||||
serverName = $bindable(),
|
||||
serverAddress = $bindable(),
|
||||
serverPort = $bindable(),
|
||||
serverConnectionType = $bindable(),
|
||||
busy,
|
||||
}: {
|
||||
serverName: string;
|
||||
serverAddress: string;
|
||||
serverPort: string;
|
||||
serverConnectionType: ServerConnectionType;
|
||||
busy: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
@@ -29,25 +33,12 @@
|
||||
autocomplete="off"
|
||||
disabled={busy}
|
||||
/>
|
||||
<label class="label mt-1" for="onboarding-server-address">{$messages.common_address()}</label>
|
||||
<input
|
||||
id="onboarding-server-address"
|
||||
class="input w-full"
|
||||
bind:value={serverAddress}
|
||||
placeholder="example.net"
|
||||
autocomplete="off"
|
||||
disabled={busy}
|
||||
/>
|
||||
<label class="label mt-1" for="onboarding-server-port">{$messages.common_optional_port()}</label>
|
||||
<input
|
||||
id="onboarding-server-port"
|
||||
class="input w-28"
|
||||
bind:value={serverPort}
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
inputmode="numeric"
|
||||
placeholder="27520"
|
||||
<ServerEndpointFields
|
||||
idPrefix="onboarding-server"
|
||||
bind:address={serverAddress}
|
||||
bind:port={serverPort}
|
||||
bind:connectionType={serverConnectionType}
|
||||
disabled={busy}
|
||||
labelClass="label"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
Reference in New Issue
Block a user