diff --git a/messages/en.json b/messages/en.json index 52252e8..a91204a 100644 --- a/messages/en.json +++ b/messages/en.json @@ -22,6 +22,8 @@ "common_name": "Name", "common_status": "Status", "common_address": "Address", + "common_connection_type": "Connection type", + "common_connection_direct": "Direct", "common_optional_port": "Port (optional)", "common_optional_friendly_name": "Friendly name (optional)", "common_server_details": "Server details", @@ -36,6 +38,7 @@ "error_unexpected": "Something went wrong. Please try again.", "error_display_name_empty": "Display name cannot be empty.", "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_friend_key_required": "Identification Key is required.", "error_server_not_selected": "No server was selected.", diff --git a/messages/zh-CN.json b/messages/zh-CN.json index dc7e9ca..fc63836 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -22,6 +22,8 @@ "common_name": "名称", "common_status": "状态", "common_address": "地址", + "common_connection_type": "连接类型", + "common_connection_direct": "直连", "common_optional_port": "端口(可选)", "common_optional_friendly_name": "备注名称(可选)", "common_server_details": "服务器信息", @@ -36,6 +38,7 @@ "error_unexpected": "发生错误,请重试。", "error_display_name_empty": "显示名称不能为空。", "error_server_address_required": "服务器地址不能为空。", + "error_server_address_scheme": "请从菜单中选择协议,不要将协议写入地址。", "error_port_invalid": "端口必须是 1 到 65535 之间的整数。", "error_friend_key_required": "请输入身份密钥。", "error_server_not_selected": "尚未选择服务器。", diff --git a/src-tauri/src/network/connection.rs b/src-tauri/src/network/connection.rs index af1a52b..55720b8 100644 --- a/src-tauri/src/network/connection.rs +++ b/src-tauri/src/network/connection.rs @@ -397,12 +397,15 @@ fn url(remote: &Remote) -> String { .or_else(|| address.strip_prefix("ws://")) .or_else(|| address.strip_prefix("wss://")) .unwrap_or(address); - let scheme = if remote.address.starts_with("https://") || remote.address.starts_with("wss://") { - "wss" - } else { - "ws" - }; - let port = remote.port.unwrap_or(DEFAULT_SERVER_PORT); + let (scheme, default_port) = + if remote.address.starts_with("https://") || remote.address.starts_with("wss://") { + ("wss", 443) + } else if remote.address.starts_with("http://") || remote.address.starts_with("ws://") { + ("ws", 80) + } else { + ("ws", DEFAULT_SERVER_PORT) + }; + let port = remote.port.unwrap_or(default_port); format!("{scheme}://{address}:{port}/v1/ws") } @@ -421,13 +424,29 @@ mod tests { } #[test] - fn url_uses_default_server_port_when_unspecified() { + fn url_uses_direct_server_port_when_unspecified() { assert_eq!( url(&remote("example.net", None)), "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] fn url_preserves_explicit_port() { assert_eq!( diff --git a/src/app.css b/src/app.css index 981de40..d73cedf 100644 --- a/src/app.css +++ b/src/app.css @@ -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"); diff --git a/src/lib/components/server-endpoint-fields.svelte b/src/lib/components/server-endpoint-fields.svelte new file mode 100644 index 0000000..630ed71 --- /dev/null +++ b/src/lib/components/server-endpoint-fields.svelte @@ -0,0 +1,96 @@ + + + +
+ {$messages.error_server_address_scheme()} +
+{/if} + + + diff --git a/src/lib/server-endpoint.ts b/src/lib/server-endpoint.ts new file mode 100644 index 0000000..56cc3cc --- /dev/null +++ b/src/lib/server-endpoint.ts @@ -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; + } +} diff --git a/src/routes/control-panel/add/network/+page.svelte b/src/routes/control-panel/add/network/+page.svelte index 1f98669..5ea597b 100644 --- a/src/routes/control-panel/add/network/+page.svelte +++ b/src/routes/control-panel/add/network/+page.svelte @@ -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