server protocol & ports UX improvement
This commit is contained in:
@@ -22,6 +22,8 @@
|
|||||||
"common_name": "Name",
|
"common_name": "Name",
|
||||||
"common_status": "Status",
|
"common_status": "Status",
|
||||||
"common_address": "Address",
|
"common_address": "Address",
|
||||||
|
"common_connection_type": "Connection type",
|
||||||
|
"common_connection_direct": "Direct",
|
||||||
"common_optional_port": "Port (optional)",
|
"common_optional_port": "Port (optional)",
|
||||||
"common_optional_friendly_name": "Friendly name (optional)",
|
"common_optional_friendly_name": "Friendly name (optional)",
|
||||||
"common_server_details": "Server details",
|
"common_server_details": "Server details",
|
||||||
@@ -36,6 +38,7 @@
|
|||||||
"error_unexpected": "Something went wrong. Please try again.",
|
"error_unexpected": "Something went wrong. Please try again.",
|
||||||
"error_display_name_empty": "Display name cannot be empty.",
|
"error_display_name_empty": "Display name cannot be empty.",
|
||||||
"error_server_address_required": "Server address is required.",
|
"error_server_address_required": "Server address is required.",
|
||||||
|
"error_server_address_scheme": "Choose the protocol from the menu instead of including it in the address.",
|
||||||
"error_port_invalid": "Port must be a whole number from 1 to 65535.",
|
"error_port_invalid": "Port must be a whole number from 1 to 65535.",
|
||||||
"error_friend_key_required": "Identification Key is required.",
|
"error_friend_key_required": "Identification Key is required.",
|
||||||
"error_server_not_selected": "No server was selected.",
|
"error_server_not_selected": "No server was selected.",
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
"common_name": "名称",
|
"common_name": "名称",
|
||||||
"common_status": "状态",
|
"common_status": "状态",
|
||||||
"common_address": "地址",
|
"common_address": "地址",
|
||||||
|
"common_connection_type": "连接类型",
|
||||||
|
"common_connection_direct": "直连",
|
||||||
"common_optional_port": "端口(可选)",
|
"common_optional_port": "端口(可选)",
|
||||||
"common_optional_friendly_name": "备注名称(可选)",
|
"common_optional_friendly_name": "备注名称(可选)",
|
||||||
"common_server_details": "服务器信息",
|
"common_server_details": "服务器信息",
|
||||||
@@ -36,6 +38,7 @@
|
|||||||
"error_unexpected": "发生错误,请重试。",
|
"error_unexpected": "发生错误,请重试。",
|
||||||
"error_display_name_empty": "显示名称不能为空。",
|
"error_display_name_empty": "显示名称不能为空。",
|
||||||
"error_server_address_required": "服务器地址不能为空。",
|
"error_server_address_required": "服务器地址不能为空。",
|
||||||
|
"error_server_address_scheme": "请从菜单中选择协议,不要将协议写入地址。",
|
||||||
"error_port_invalid": "端口必须是 1 到 65535 之间的整数。",
|
"error_port_invalid": "端口必须是 1 到 65535 之间的整数。",
|
||||||
"error_friend_key_required": "请输入身份密钥。",
|
"error_friend_key_required": "请输入身份密钥。",
|
||||||
"error_server_not_selected": "尚未选择服务器。",
|
"error_server_not_selected": "尚未选择服务器。",
|
||||||
|
|||||||
@@ -397,12 +397,15 @@ fn url(remote: &Remote) -> String {
|
|||||||
.or_else(|| address.strip_prefix("ws://"))
|
.or_else(|| address.strip_prefix("ws://"))
|
||||||
.or_else(|| address.strip_prefix("wss://"))
|
.or_else(|| address.strip_prefix("wss://"))
|
||||||
.unwrap_or(address);
|
.unwrap_or(address);
|
||||||
let scheme = if remote.address.starts_with("https://") || remote.address.starts_with("wss://") {
|
let (scheme, default_port) =
|
||||||
"wss"
|
if remote.address.starts_with("https://") || remote.address.starts_with("wss://") {
|
||||||
} else {
|
("wss", 443)
|
||||||
"ws"
|
} else if remote.address.starts_with("http://") || remote.address.starts_with("ws://") {
|
||||||
};
|
("ws", 80)
|
||||||
let port = remote.port.unwrap_or(DEFAULT_SERVER_PORT);
|
} else {
|
||||||
|
("ws", DEFAULT_SERVER_PORT)
|
||||||
|
};
|
||||||
|
let port = remote.port.unwrap_or(default_port);
|
||||||
format!("{scheme}://{address}:{port}/v1/ws")
|
format!("{scheme}://{address}:{port}/v1/ws")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,13 +424,29 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn url_uses_default_server_port_when_unspecified() {
|
fn url_uses_direct_server_port_when_unspecified() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
url(&remote("example.net", None)),
|
url(&remote("example.net", None)),
|
||||||
"ws://example.net:27520/v1/ws"
|
"ws://example.net:27520/v1/ws"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn url_uses_https_default_port_when_unspecified() {
|
||||||
|
assert_eq!(
|
||||||
|
url(&remote("https://example.net", None)),
|
||||||
|
"wss://example.net:443/v1/ws"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn url_uses_http_default_port_when_unspecified() {
|
||||||
|
assert_eq!(
|
||||||
|
url(&remote("http://example.net", None)),
|
||||||
|
"ws://example.net:80/v1/ws"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn url_preserves_explicit_port() {
|
fn url_preserves_explicit_port() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
+9
-2
@@ -81,11 +81,18 @@
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button {
|
||||||
input {
|
|
||||||
@apply btn-sm;
|
@apply btn-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
@apply input-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
@apply select-sm;
|
||||||
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "w95f";
|
font-family: "w95f";
|
||||||
src: url("/w95f.woff2") format("woff2");
|
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 { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import { commands, type RemoteInput } from "$lib/bindings";
|
import { commands, type RemoteInput } from "$lib/bindings";
|
||||||
import PanelMessage from "$lib/components/control-panel/panel-message.svelte";
|
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 { errorMessage, messages } from "$lib/i18n";
|
||||||
|
import {
|
||||||
|
containsScheme,
|
||||||
|
storedServerAddress,
|
||||||
|
type ServerConnectionType,
|
||||||
|
} from "$lib/server-endpoint";
|
||||||
|
|
||||||
let name = $state("");
|
let name = $state("");
|
||||||
let address = $state("");
|
let address = $state("");
|
||||||
let port = $state("");
|
let port = $state("");
|
||||||
|
let connectionType = $state<ServerConnectionType>("https");
|
||||||
let busy = $state(false);
|
let busy = $state(false);
|
||||||
let error = $state("");
|
let error = $state("");
|
||||||
|
|
||||||
@@ -24,6 +31,10 @@
|
|||||||
error = $messages.error_server_address_required();
|
error = $messages.error_server_address_required();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (containsScheme(address)) {
|
||||||
|
error = $messages.error_server_address_scheme();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
parsedPort !== null &&
|
parsedPort !== null &&
|
||||||
(!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535)
|
(!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535)
|
||||||
@@ -32,7 +43,7 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
address: address.trim(),
|
address: storedServerAddress(address, connectionType),
|
||||||
name: name.trim() || null,
|
name: name.trim() || null,
|
||||||
port: parsedPort,
|
port: parsedPort,
|
||||||
};
|
};
|
||||||
@@ -79,28 +90,13 @@
|
|||||||
maxlength="64"
|
maxlength="64"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
<label class="fieldset-label mt-1" for="remote-address">{$messages.common_address()}</label>
|
<ServerEndpointFields
|
||||||
<input
|
idPrefix="remote"
|
||||||
id="remote-address"
|
bind:address
|
||||||
class="input w-full"
|
bind:port
|
||||||
bind:value={address}
|
bind:connectionType
|
||||||
placeholder="example.net"
|
|
||||||
autocomplete="off"
|
|
||||||
required
|
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>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,19 @@
|
|||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { commands, type RemoteInput } from "$lib/bindings";
|
import { commands, type RemoteInput } from "$lib/bindings";
|
||||||
import PanelMessage from "$lib/components/control-panel/panel-message.svelte";
|
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 { errorMessage, messages } from "$lib/i18n";
|
||||||
|
import {
|
||||||
|
containsScheme,
|
||||||
|
splitServerAddress,
|
||||||
|
storedServerAddress,
|
||||||
|
type ServerConnectionType,
|
||||||
|
} from "$lib/server-endpoint";
|
||||||
|
|
||||||
let name = $state("");
|
let name = $state("");
|
||||||
let address = $state("");
|
let address = $state("");
|
||||||
let port = $state("");
|
let port = $state("");
|
||||||
|
let connectionType = $state<ServerConnectionType>("https");
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let busy = $state(false);
|
let busy = $state(false);
|
||||||
let available = $state(false);
|
let available = $state(false);
|
||||||
@@ -38,7 +46,9 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
name = remote.name ?? "";
|
name = remote.name ?? "";
|
||||||
address = remote.address;
|
const endpoint = splitServerAddress(remote.address);
|
||||||
|
address = endpoint.address;
|
||||||
|
connectionType = endpoint.connectionType;
|
||||||
port = remote.port?.toString() ?? "";
|
port = remote.port?.toString() ?? "";
|
||||||
available = true;
|
available = true;
|
||||||
})
|
})
|
||||||
@@ -64,6 +74,10 @@
|
|||||||
error = $messages.error_server_address_required();
|
error = $messages.error_server_address_required();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (containsScheme(address)) {
|
||||||
|
error = $messages.error_server_address_scheme();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
parsedPort !== null &&
|
parsedPort !== null &&
|
||||||
(!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535)
|
(!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535)
|
||||||
@@ -72,7 +86,7 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
address: address.trim(),
|
address: storedServerAddress(address, connectionType),
|
||||||
name: name.trim() || null,
|
name: name.trim() || null,
|
||||||
port: parsedPort,
|
port: parsedPort,
|
||||||
};
|
};
|
||||||
@@ -135,28 +149,14 @@
|
|||||||
maxlength="64"
|
maxlength="64"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
<label class="fieldset-label mt-1" for="remote-address">{$messages.common_address()}</label>
|
<ServerEndpointFields
|
||||||
<input
|
idPrefix="remote"
|
||||||
id="remote-address"
|
bind:address
|
||||||
class="input w-full"
|
bind:port
|
||||||
bind:value={address}
|
bind:connectionType
|
||||||
placeholder="example.net"
|
disabled={loading || busy || !available}
|
||||||
autocomplete="off"
|
|
||||||
required
|
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>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,11 @@
|
|||||||
import SkinStep from "./components/skin-step.svelte";
|
import SkinStep from "./components/skin-step.svelte";
|
||||||
import WelcomeStep from "./components/welcome-step.svelte";
|
import WelcomeStep from "./components/welcome-step.svelte";
|
||||||
import { errorMessage, messages, type MessageCatalog } from "$lib/i18n";
|
import { errorMessage, messages, type MessageCatalog } from "$lib/i18n";
|
||||||
|
import {
|
||||||
|
containsScheme,
|
||||||
|
storedServerAddress,
|
||||||
|
type ServerConnectionType,
|
||||||
|
} from "$lib/server-endpoint";
|
||||||
import { getVersion } from "@tauri-apps/api/app";
|
import { getVersion } from "@tauri-apps/api/app";
|
||||||
|
|
||||||
type StepId =
|
type StepId =
|
||||||
@@ -99,6 +104,7 @@
|
|||||||
let serverName = $state("");
|
let serverName = $state("");
|
||||||
let serverAddress = $state("");
|
let serverAddress = $state("");
|
||||||
let serverPort = $state("");
|
let serverPort = $state("");
|
||||||
|
let serverConnectionType = $state<ServerConnectionType>("https");
|
||||||
let savedServerId = $state<string | null>(null);
|
let savedServerId = $state<string | null>(null);
|
||||||
let friendId = $state("");
|
let friendId = $state("");
|
||||||
let savedFriendId = $state<string | null>(null);
|
let savedFriendId = $state<string | null>(null);
|
||||||
@@ -191,6 +197,9 @@
|
|||||||
|
|
||||||
function remoteInput(): RemoteInput | null {
|
function remoteInput(): RemoteInput | null {
|
||||||
if (!serverAddress.trim()) return null;
|
if (!serverAddress.trim()) return null;
|
||||||
|
if (containsScheme(serverAddress)) {
|
||||||
|
throw new Error($messages.error_server_address_scheme());
|
||||||
|
}
|
||||||
const port = serverPort ? Number(serverPort) : null;
|
const port = serverPort ? Number(serverPort) : null;
|
||||||
if (
|
if (
|
||||||
port !== null &&
|
port !== null &&
|
||||||
@@ -199,7 +208,7 @@
|
|||||||
throw new Error($messages.error_port_invalid());
|
throw new Error($messages.error_port_invalid());
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
address: serverAddress.trim(),
|
address: storedServerAddress(serverAddress, serverConnectionType),
|
||||||
name: serverName.trim() || null,
|
name: serverName.trim() || null,
|
||||||
port,
|
port,
|
||||||
};
|
};
|
||||||
@@ -370,6 +379,7 @@
|
|||||||
bind:serverName
|
bind:serverName
|
||||||
bind:serverAddress
|
bind:serverAddress
|
||||||
bind:serverPort
|
bind:serverPort
|
||||||
|
bind:serverConnectionType
|
||||||
{busy}
|
{busy}
|
||||||
/>
|
/>
|
||||||
{:else if step.id === "friend"}
|
{:else if step.id === "friend"}
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import PanelMessage from "$lib/components/control-panel/panel-message.svelte";
|
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 { messages } from "$lib/i18n";
|
||||||
|
import type { ServerConnectionType } from "$lib/server-endpoint";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
serverName = $bindable(),
|
serverName = $bindable(),
|
||||||
serverAddress = $bindable(),
|
serverAddress = $bindable(),
|
||||||
serverPort = $bindable(),
|
serverPort = $bindable(),
|
||||||
|
serverConnectionType = $bindable(),
|
||||||
busy,
|
busy,
|
||||||
}: {
|
}: {
|
||||||
serverName: string;
|
serverName: string;
|
||||||
serverAddress: string;
|
serverAddress: string;
|
||||||
serverPort: string;
|
serverPort: string;
|
||||||
|
serverConnectionType: ServerConnectionType;
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
@@ -29,25 +33,12 @@
|
|||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
/>
|
/>
|
||||||
<label class="label mt-1" for="onboarding-server-address">{$messages.common_address()}</label>
|
<ServerEndpointFields
|
||||||
<input
|
idPrefix="onboarding-server"
|
||||||
id="onboarding-server-address"
|
bind:address={serverAddress}
|
||||||
class="input w-full"
|
bind:port={serverPort}
|
||||||
bind:value={serverAddress}
|
bind:connectionType={serverConnectionType}
|
||||||
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"
|
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
|
labelClass="label"
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, test } from "node:test";
|
||||||
|
import {
|
||||||
|
containsScheme,
|
||||||
|
pastedServerAddress,
|
||||||
|
splitServerAddress,
|
||||||
|
storedServerAddress,
|
||||||
|
} from "../src/lib/server-endpoint";
|
||||||
|
|
||||||
|
describe("server endpoint fields", () => {
|
||||||
|
test("normalizes a pasted secure URL and explicit port", () => {
|
||||||
|
assert.deepEqual(pastedServerAddress("https://api.example.com:8443/"), {
|
||||||
|
address: "api.example.com",
|
||||||
|
connectionType: "https",
|
||||||
|
port: "8443",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("maps WebSocket schemes to their matching HTTP modes", () => {
|
||||||
|
assert.deepEqual(pastedServerAddress("wss://api.example.com"), {
|
||||||
|
address: "api.example.com",
|
||||||
|
connectionType: "https",
|
||||||
|
port: "",
|
||||||
|
});
|
||||||
|
assert.deepEqual(pastedServerAddress("ws://api.example.com"), {
|
||||||
|
address: "api.example.com",
|
||||||
|
connectionType: "http",
|
||||||
|
port: "",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not normalize URLs containing unsupported components", () => {
|
||||||
|
assert.equal(pastedServerAddress("https://api.example.com/v1/ws"), null);
|
||||||
|
assert.equal(pastedServerAddress("https://user@api.example.com"), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("detects a manually typed scheme without changing the address", () => {
|
||||||
|
assert.equal(containsScheme("https://api.example.com"), true);
|
||||||
|
assert.equal(containsScheme("api.example.com"), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("round-trips stored proxy and direct addresses", () => {
|
||||||
|
assert.equal(
|
||||||
|
storedServerAddress("api.example.com", "https"),
|
||||||
|
"https://api.example.com",
|
||||||
|
);
|
||||||
|
assert.deepEqual(splitServerAddress("https://api.example.com"), {
|
||||||
|
address: "api.example.com",
|
||||||
|
connectionType: "https",
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
storedServerAddress("lan.example.com", "direct"),
|
||||||
|
"lan.example.com",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user