UI: Improve UX of Remote API setting page (#1870)

Co-authored-by: David Walker <d0sboots@gmail.com>
This commit is contained in:
catloversg
2025-01-13 01:58:24 +07:00
committed by GitHub
parent fdb325bf66
commit 0e8dca85e1
3 changed files with 102 additions and 60 deletions
+41 -27
View File
@@ -7,34 +7,38 @@ import { isRemoteFileApiConnectionLive, newRemoteFileApiConnection } from "../..
export const RemoteAPIPage = (): React.ReactElement => { export const RemoteAPIPage = (): React.ReactElement => {
const [remoteFileApiHostname, setRemoteFileApiHostname] = useState(Settings.RemoteFileApiAddress); const [remoteFileApiHostname, setRemoteFileApiHostname] = useState(Settings.RemoteFileApiAddress);
const [remoteFileApiPort, setRemoteFileApiPort] = useState(Settings.RemoteFileApiPort); const [hostnameError, setHostnameError] = useState(
isValidConnectionHostname(Settings.RemoteFileApiAddress).message ?? "",
);
const [remoteFileApiPort, setRemoteFileApiPort] = useState(Settings.RemoteFileApiPort.toString());
const [portError, setPortError] = useState(isValidConnectionPort(Settings.RemoteFileApiPort).message ?? "");
const isValidHostname = hostnameError === "";
const isValidPort = portError === "";
function handleRemoteFileApiHostnameChange(event: React.ChangeEvent<HTMLInputElement>): void { function handleRemoteFileApiHostnameChange(event: React.ChangeEvent<HTMLInputElement>): void {
let newValue = event.target.value.trim(); const newValue = event.target.value.trim();
// Empty string will be automatically changed to "localhost". setRemoteFileApiHostname(newValue);
if (newValue === "") { const result = isValidConnectionHostname(newValue);
newValue = "localhost"; if (!result.success) {
} setHostnameError(result.message);
if (!isValidConnectionHostname(newValue)) {
return; return;
} }
setRemoteFileApiHostname(newValue);
Settings.RemoteFileApiAddress = newValue; Settings.RemoteFileApiAddress = newValue;
setHostnameError("");
} }
function handleRemoteFileApiPortChange(event: React.ChangeEvent<HTMLInputElement>): void { function handleRemoteFileApiPortChange(event: React.ChangeEvent<HTMLInputElement>): void {
let newValue = event.target.value.trim(); const newValue = event.target.value.trim();
// Empty string will be automatically changed to "0". setRemoteFileApiPort(newValue);
if (newValue === "") {
newValue = "0";
}
const port = Number.parseInt(newValue); const port = Number.parseInt(newValue);
// Disallow invalid ports but still allow the player to set port to 0. Setting it to 0 means that RFA is disabled. const result = isValidConnectionPort(port);
if (port !== 0 && !isValidConnectionPort(port)) { if (!result.success) {
setPortError(result.message);
return; return;
} }
setRemoteFileApiPort(port);
Settings.RemoteFileApiPort = port; Settings.RemoteFileApiPort = port;
setPortError("");
} }
return ( return (
@@ -56,11 +60,17 @@ export const RemoteAPIPage = (): React.ReactElement => {
title={ title={
<Typography> <Typography>
This hostname is used to connect to a Remote API, please ensure that it matches with your Remote API This hostname is used to connect to a Remote API, please ensure that it matches with your Remote API
hostname. Default: localhost. hostname.
<br />
If you use IPv6, you need to wrap it in square brackets. For example: [::1]
<br />
Default: localhost.
</Typography> </Typography>
} }
> >
<div>
<TextField <TextField
error={!isValidHostname}
InputProps={{ InputProps={{
startAdornment: <Typography>Hostname:&nbsp;</Typography>, startAdornment: <Typography>Hostname:&nbsp;</Typography>,
}} }}
@@ -69,32 +79,36 @@ export const RemoteAPIPage = (): React.ReactElement => {
placeholder="localhost" placeholder="localhost"
size={"medium"} size={"medium"}
/> />
{hostnameError && <Typography color={Settings.theme.error}>{hostnameError}</Typography>}
</div>
</Tooltip> </Tooltip>
<br />
<Tooltip <Tooltip
title={ title={
<Typography> <Typography>
This port number is used to connect to a Remote API, please ensure that it matches with your Remote API This port number is used to connect to the Remote API. Please ensure that it matches with your Remote API
server port. Set to 0 to disable the feature. server port.
<br />
The value must be in the range of [0, 65535]. Set it to 0 to disable the feature.
</Typography> </Typography>
} }
> >
<div>
<TextField <TextField
error={!isValidPort}
InputProps={{ InputProps={{
startAdornment: ( startAdornment: <Typography color={isValidPort ? "success" : "error"}>Port:&nbsp;</Typography>,
<Typography color={isValidConnectionPort(remoteFileApiPort) ? "success" : "error"}>
Port:&nbsp;
</Typography>
),
}} }}
value={remoteFileApiPort} value={remoteFileApiPort}
onChange={handleRemoteFileApiPortChange} onChange={handleRemoteFileApiPortChange}
placeholder="12525" placeholder="12525"
size={"medium"} size={"medium"}
/> />
{portError && <Typography color={Settings.theme.error}>{portError}</Typography>}
</div>
</Tooltip> </Tooltip>
<br /> <Button disabled={!isValidHostname || !isValidPort} onClick={newRemoteFileApiConnection}>
<Button onClick={newRemoteFileApiConnection}>Connect</Button> Connect
</Button>
</GameOptionsPage> </GameOptionsPage>
); );
}; };
+11 -4
View File
@@ -3,6 +3,10 @@ import { RFARequestHandler } from "./MessageHandlers";
import { SnackbarEvents } from "../ui/React/Snackbar"; import { SnackbarEvents } from "../ui/React/Snackbar";
import { ToastVariant } from "@enums"; import { ToastVariant } from "@enums";
function showErrorMessage(address: string, detail: string) {
SnackbarEvents.emit(`Error with websocket ${address}, details: ${detail}`, ToastVariant.ERROR, 5000);
}
export class Remote { export class Remote {
connection?: WebSocket; connection?: WebSocket;
static protocol = "ws"; static protocol = "ws";
@@ -20,11 +24,14 @@ export class Remote {
public startConnection(): void { public startConnection(): void {
const address = Remote.protocol + "://" + this.ipaddr + ":" + this.port; const address = Remote.protocol + "://" + this.ipaddr + ":" + this.port;
try {
this.connection = new WebSocket(address); this.connection = new WebSocket(address);
} catch (error) {
this.connection.addEventListener("error", (e: Event) => console.error(error);
SnackbarEvents.emit(`Error with websocket ${address}, details: ${JSON.stringify(e)}`, ToastVariant.ERROR, 5000), showErrorMessage(address, String(error));
); return;
}
this.connection.addEventListener("error", (e: Event) => showErrorMessage(address, JSON.stringify(e)));
this.connection.addEventListener("message", handleMessageEvent); this.connection.addEventListener("message", handleMessageEvent);
this.connection.addEventListener("open", () => this.connection.addEventListener("open", () =>
SnackbarEvents.emit( SnackbarEvents.emit(
+32 -11
View File
@@ -4,6 +4,7 @@ import { defaultStyles } from "../Themes/Styles";
import { CursorStyle, CursorBlinking, WordWrapOptions } from "../ScriptEditor/ui/Options"; import { CursorStyle, CursorBlinking, WordWrapOptions } from "../ScriptEditor/ui/Options";
import { defaultMonacoTheme } from "../ScriptEditor/ui/themes"; import { defaultMonacoTheme } from "../ScriptEditor/ui/themes";
import { objectAssert } from "../utils/helpers/typeAssertion"; import { objectAssert } from "../utils/helpers/typeAssertion";
import { Result } from "../types";
import { import {
assertAndSanitizeEditorTheme, assertAndSanitizeEditorTheme,
assertAndSanitizeMainTheme, assertAndSanitizeMainTheme,
@@ -21,7 +22,14 @@ import {
* - Use non-http schemes in the hostname: "ftp://a.com" * - Use non-http schemes in the hostname: "ftp://a.com"
* - etc. * - etc.
*/ */
export function isValidConnectionHostname(hostname: string): boolean { export function isValidConnectionHostname(hostname: string): Result {
// Return a user-friendly error message.
if (hostname === "") {
return {
success: false,
message: "Hostname cannot be empty",
};
}
/** /**
* We expect a hostname, but the player may mistakenly put other unexpected things. We will try to catch common mistakes: * We expect a hostname, but the player may mistakenly put other unexpected things. We will try to catch common mistakes:
* - Specify a scheme: http or https. * - Specify a scheme: http or https.
@@ -31,23 +39,36 @@ export function isValidConnectionHostname(hostname: string): boolean {
try { try {
// Check scheme. // Check scheme.
if (hostname.startsWith("http://") || hostname.startsWith("https://")) { if (hostname.startsWith("http://") || hostname.startsWith("https://")) {
return false; return {
success: false,
message: "Do not specify scheme (e.g., http, https)",
};
} }
// Parse to a URL with a default scheme. // Parse to a URL with a default scheme.
const url = new URL(`http://${hostname}`); const url = new URL(`http://${hostname}`);
// Check port, pathname, and search params. // Check port, pathname, and search params.
if (url.port !== "" || url.pathname !== "/" || url.search !== "") { if (url.port !== "" || url.pathname !== "/" || url.search !== "") {
return false; return {
success: false,
message: "Do not specify port, pathname, or search parameters",
};
} }
} catch (e) { } catch (error) {
console.error(`Invalid hostname: ${hostname}`, e); console.error(error);
return false; return {
success: false,
message: `Invalid hostname: ${hostname}`,
};
} }
return true; return { success: true };
} }
export function isValidConnectionPort(port: number): boolean { export function isValidConnectionPort(port: number): Result {
return Number.isFinite(port) && port > 0 && port <= 65535; // 0 is a special value for port. It's an invalid port, but the player can use it to disable RFA.
if (!Number.isFinite(port) || port < 0 || port > 65535) {
return { success: false, message: "Invalid port" };
}
return { success: true };
} }
/** The current options the player has customized to their play style. */ /** The current options the player has customized to their play style. */
@@ -198,10 +219,10 @@ export const Settings = {
* The hostname and port of RFA have not been validated properly, so the save data may contain invalid data. In that * The hostname and port of RFA have not been validated properly, so the save data may contain invalid data. In that
* case, we set them to the default value. * case, we set them to the default value.
*/ */
if (!isValidConnectionHostname(Settings.RemoteFileApiAddress)) { if (!isValidConnectionHostname(Settings.RemoteFileApiAddress).success) {
Settings.RemoteFileApiAddress = "localhost"; Settings.RemoteFileApiAddress = "localhost";
} }
if (!isValidConnectionPort(Settings.RemoteFileApiPort)) { if (!isValidConnectionPort(Settings.RemoteFileApiPort).success) {
Settings.RemoteFileApiPort = 0; Settings.RemoteFileApiPort = 0;
} }
}, },