diff options
| author | David Skoland <davidskoland@gmail.com> | 2026-03-24 12:23:45 +0100 |
|---|---|---|
| committer | David Skoland <davidskoland@gmail.com> | 2026-03-24 12:23:45 +0100 |
| commit | 1a1ed4e1d0568a1610d5f5da3d541a59afe2b863 (patch) | |
| tree | 6df185dcb2994767619d2dc32e45e27e3496aff3 /webAO | |
| parent | 4715e7ccde04a77ff04f1ac839c151eaebc4ad44 (diff) | |
Add reconnect UI, disconnect button, and visual cleanup
- Redesign disconnect overlay as a full-screen modal with dark backdrop
- Add working Reconnect button that properly re-establishes WebSocket connection
- Add Disconnect button in Settings for testing
- Separate disconnect and ban/kick codepaths (no reconnect on ban)
- Log disconnect notice in IC log using hrtext style
- Refactor area list rendering from client state (renderAreaList)
- Extract appendICNotice for reusable IC log notices
- Clean up charselect: hide during loading, simplify toolbar layout
- Freshen loading screen and charselect styling
- Remove loading progress text updates (just show "Loading...")
- Guard against undefined client.chars and client.serv
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'webAO')
| -rw-r--r-- | webAO/client.ts | 28 | ||||
| -rw-r--r-- | webAO/client/appendICNotice.ts | 10 | ||||
| -rw-r--r-- | webAO/client/createArea.ts | 25 | ||||
| -rw-r--r-- | webAO/client/fixLastArea.ts | 4 | ||||
| -rw-r--r-- | webAO/client/handleBans.ts | 2 | ||||
| -rw-r--r-- | webAO/client/sender/sendCharacter.ts | 2 | ||||
| -rw-r--r-- | webAO/dom/areaClick.ts | 7 | ||||
| -rw-r--r-- | webAO/dom/disconnectButton.ts | 12 | ||||
| -rw-r--r-- | webAO/dom/reconnectButton.ts | 22 | ||||
| -rw-r--r-- | webAO/dom/renderAreaList.ts | 24 | ||||
| -rw-r--r-- | webAO/dom/window.ts | 1 | ||||
| -rw-r--r-- | webAO/packets/handlers/handleARUP.ts | 15 | ||||
| -rw-r--r-- | webAO/packets/handlers/handleCI.ts | 2 | ||||
| -rw-r--r-- | webAO/packets/handlers/handleEI.ts | 2 | ||||
| -rw-r--r-- | webAO/packets/handlers/handleEM.ts | 1 | ||||
| -rw-r--r-- | webAO/packets/handlers/handleSM.ts | 3 | ||||
| -rw-r--r-- | webAO/styles/client.css | 94 |
17 files changed, 176 insertions, 78 deletions
diff --git a/webAO/client.ts b/webAO/client.ts index 05a40c9..e00afe6 100644 --- a/webAO/client.ts +++ b/webAO/client.ts @@ -13,6 +13,8 @@ import { Viewport } from "./viewport/interfaces/Viewport"; import { EventEmitter } from "events"; import { onReplayGo } from "./dom/onReplayGo"; import { packetHandler } from "./packets/packetHandler"; +import { appendICNotice } from "./client/appendICNotice"; +import { renderAreaList } from "./dom/renderAreaList"; import { loadResources } from "./client/loadResources"; import { AO_HOST } from "./client/aoHost"; import { @@ -111,6 +113,8 @@ export enum clientState { Connected, // Should be set once the client has joined the server (after handshake) Joined, + // Set when a reconnect attempt is in progress + Reconnecting, } export let lastICMessageTime = new Date(0); @@ -274,6 +278,10 @@ class Client extends EventEmitter { */ onOpen(_e: Event) { client.state = clientState.Connected; + document.getElementById("client_error_overlay").style.display = "none"; + document.getElementById("client_waiting").style.display = "block"; + document.getElementById("client_loading").style.display = "block"; + document.getElementById("client_charselect").style.display = "none"; client.joinServer(); } @@ -282,19 +290,21 @@ class Client extends EventEmitter { * @param {CloseEvent} e */ onClose(e: CloseEvent) { - client.state = clientState.NotConnected; console.error(`The connection was closed: ${e.reason} (${e.code})`); + if (this.state === clientState.Reconnecting) return; + client.state = clientState.NotConnected; if (this.banned === false) { if (this.areas.length > 0) { document.getElementById("client_errortext").textContent = "You were disconnected from the server."; + appendICNotice("Disconnected from the server."); } else { document.getElementById("client_errortext").textContent = "Could not connect to the server."; } + (<HTMLElement>document.getElementById("client_reconnect")).style.display = ""; } - document.getElementById("client_waiting").style.display = "block"; - document.getElementById("client_error").style.display = "flex"; + document.getElementById("client_error_overlay").style.display = "flex"; document.getElementById("client_loading").style.display = "none"; document.getElementById("error_id").textContent = String(e.code); this.cleanup(); @@ -364,10 +374,14 @@ class Client extends EventEmitter { * @param {ErrorEvent} e */ onError(e: ErrorEvent) { - client.state = clientState.NotConnected; console.error(`A network error occurred`); console.error(e); - document.getElementById("client_error").style.display = "flex"; + if (this.state === clientState.Reconnecting) return; + client.state = clientState.NotConnected; + document.getElementById("client_errortext").textContent = + "Could not connect to the server."; + (<HTMLElement>document.getElementById("client_reconnect")).style.display = ""; + document.getElementById("client_error_overlay").style.display = "flex"; this.cleanup(); } @@ -376,7 +390,7 @@ class Client extends EventEmitter { */ cleanup() { clearInterval(this.checkUpdater); - this.serv.close(); + if (this.serv) this.serv.close(); } /** @@ -412,7 +426,7 @@ class Client extends EventEmitter { resetAreaList() { this.areas = []; - document.getElementById("areas").innerHTML = ""; + renderAreaList(); fetchBackgroundList(); fetchEvidenceList(); fetchCharacterList(); diff --git a/webAO/client/appendICNotice.ts b/webAO/client/appendICNotice.ts new file mode 100644 index 0000000..29065db --- /dev/null +++ b/webAO/client/appendICNotice.ts @@ -0,0 +1,10 @@ +/** + * Appends a notice (hrtext divider) to the IC log. + * @param {string} msg the notice text + */ +export function appendICNotice(msg: string) { + const el = document.createElement("div"); + el.className = "hrtext"; + el.textContent = msg; + document.getElementById("client_log")!.appendChild(el); +} diff --git a/webAO/client/createArea.ts b/webAO/client/createArea.ts index 9a40bef..dfc57e8 100644 --- a/webAO/client/createArea.ts +++ b/webAO/client/createArea.ts @@ -1,32 +1,15 @@ import { client } from "../client"; -import { area_click } from "../dom/areaClick"; +import { renderAreaList } from "../dom/renderAreaList"; import { safeTags } from "../encoding"; export const createArea = (id: number, aname: string) => { const name = safeTags(aname); - const thisarea = { + client.areas.push({ name, players: 0, status: "IDLE", cm: "", locked: "FREE", - }; - - client.areas.push(thisarea); - - // Create area button - const newarea = document.createElement("SPAN"); - newarea.className = "area-button area-default"; - newarea.id = `area${id}`; - newarea.innerText = thisarea.name; - newarea.title = - `Players: ${thisarea.players}\n` + - `Status: ${thisarea.status}\n` + - `CM: ${thisarea.cm}\n` + - `Area lock: ${thisarea.locked}`; - newarea.onclick = function () { - area_click(newarea); - }; - - document.getElementById("areas")!.appendChild(newarea); + }); + renderAreaList(); }; diff --git a/webAO/client/fixLastArea.ts b/webAO/client/fixLastArea.ts index a9979da..839b14c 100644 --- a/webAO/client/fixLastArea.ts +++ b/webAO/client/fixLastArea.ts @@ -1,4 +1,5 @@ import { client } from "../client"; +import { renderAreaList } from "../dom/renderAreaList"; import { addTrack } from "./addTrack"; /** @@ -7,8 +8,7 @@ import { addTrack } from "./addTrack"; export const fix_last_area = () => { if (client.areas.length > 0) { const malplaced = client.areas.pop().name; - const areas = document.getElementById("areas")!; - areas.removeChild(areas.lastChild); + renderAreaList(); addTrack(malplaced); } }; diff --git a/webAO/client/handleBans.ts b/webAO/client/handleBans.ts index 9eec9be..cf7f881 100644 --- a/webAO/client/handleBans.ts +++ b/webAO/client/handleBans.ts @@ -6,7 +6,7 @@ import { safeTags } from "../encoding"; * @param {string} reason why */ export const handleBans = (type: string, reason: string) => { - document.getElementById("client_error")!.style.display = "flex"; + document.getElementById("client_error_overlay")!.style.display = "flex"; document.getElementById("client_errortext")!.innerHTML = `${type}:<br>${safeTags(reason).replace(/\n/g, "<br />")}`; (<HTMLElement>document.getElementById("client_reconnect")).style.display = diff --git a/webAO/client/sender/sendCharacter.ts b/webAO/client/sender/sendCharacter.ts index 2db4dcd..eed6a99 100644 --- a/webAO/client/sender/sendCharacter.ts +++ b/webAO/client/sender/sendCharacter.ts @@ -5,7 +5,7 @@ import { client } from "../../client"; * @param {number} character the character ID */ export const sendCharacter = (character: number) => { - if (character === -1 || client.chars[character].name) { + if (character === -1 || (client.chars[character] && client.chars[character].name)) { client.sender.sendServer(`CC#${client.playerID}#${character}#web#%`); } }; diff --git a/webAO/dom/areaClick.ts b/webAO/dom/areaClick.ts index 27682c7..120ef39 100644 --- a/webAO/dom/areaClick.ts +++ b/webAO/dom/areaClick.ts @@ -1,4 +1,5 @@ import { client } from "../client"; +import { appendICNotice } from "../client/appendICNotice"; import { renderPlayerList } from "./renderPlayerList"; /** * Triggered when an item on the area list is clicked. @@ -7,11 +8,7 @@ import { renderPlayerList } from "./renderPlayerList"; export function area_click(el: HTMLElement) { const area = client.areas[el.id.substring(4)].name; client.sender.sendMusicChange(area); - - const areaHr = document.createElement("div"); - areaHr.className = "hrtext"; - areaHr.textContent = `switched to ${el.textContent}`; - document.getElementById("client_log")!.appendChild(areaHr); + appendICNotice(`switched to ${el.textContent}`); client.area = Number(el.id.substring(4)); renderPlayerList(); } diff --git a/webAO/dom/disconnectButton.ts b/webAO/dom/disconnectButton.ts new file mode 100644 index 0000000..35daf16 --- /dev/null +++ b/webAO/dom/disconnectButton.ts @@ -0,0 +1,12 @@ +import { client } from "../client"; + +/** + * Triggered when the disconnect button in settings is pushed. + * Forces a disconnection for testing purposes. + */ +export function DisconnectButton() { + if (client.serv && client.serv.readyState === WebSocket.OPEN) { + client.serv.close(); + } +} +window.DisconnectButton = DisconnectButton; diff --git a/webAO/dom/reconnectButton.ts b/webAO/dom/reconnectButton.ts index 079e7fc..ae492fb 100644 --- a/webAO/dom/reconnectButton.ts +++ b/webAO/dom/reconnectButton.ts @@ -1,16 +1,26 @@ -import Client, { client, setClient } from "../client"; +import Client, { client, clientState, setClient } from "../client"; import queryParser from "../utils/queryParser"; -const { ip: serverIP } = queryParser(); +const { ip: serverIP, connect } = queryParser(); /** * Triggered when the reconnect button is pushed. */ export function ReconnectButton() { - client.cleanup(); - setClient(new Client(serverIP)); + document.getElementById("client_errortext")!.textContent = "Reconnecting..."; - if (client) { - document.getElementById("client_error")!.style.display = "none"; + // Build the connection string the same way the initial connection does + let connectionString = connect; + if (!connectionString && serverIP) { + connectionString = `ws://${serverIP}`; } + + const hdid = client.hdid; + client.state = clientState.Reconnecting; + client.cleanup(); + + const newClient = new Client(connectionString); + setClient(newClient); + newClient.hdid = hdid; + newClient.connect(); } window.ReconnectButton = ReconnectButton; diff --git a/webAO/dom/renderAreaList.ts b/webAO/dom/renderAreaList.ts new file mode 100644 index 0000000..e622765 --- /dev/null +++ b/webAO/dom/renderAreaList.ts @@ -0,0 +1,24 @@ +import { client } from "../client"; +import { area_click } from "./areaClick"; + +export function renderAreaList() { + const container = document.getElementById("areas")!; + container.innerHTML = ""; + + for (let i = 0; i < client.areas.length; i++) { + const area = client.areas[i]; + const el = document.createElement("SPAN"); + el.className = `area-button area-${area.status.toLowerCase()}`; + el.id = `area${i}`; + el.innerText = `${area.name} (${area.players}) [${area.status}]`; + el.title = + `Players: ${area.players}\n` + + `Status: ${area.status}\n` + + `CM: ${area.cm}\n` + + `Area lock: ${area.locked}`; + el.onclick = function () { + area_click(el); + }; + container.appendChild(el); + } +} diff --git a/webAO/dom/window.ts b/webAO/dom/window.ts index ee1b121..48d0714 100644 --- a/webAO/dom/window.ts +++ b/webAO/dom/window.ts @@ -30,6 +30,7 @@ declare global { pickChar: (ccharacter: any) => void; chartable_filter: (_event: any) => void; ReconnectButton: (_event: any) => void; + DisconnectButton: () => void; opusCheck: (channel: HTMLAudioElement) => OnErrorEventHandlerNonNull; imgError: (image: any) => void; charError: (image: any) => void; diff --git a/webAO/packets/handlers/handleARUP.ts b/webAO/packets/handlers/handleARUP.ts index 7f72b15..5a7aebb 100644 --- a/webAO/packets/handlers/handleARUP.ts +++ b/webAO/packets/handlers/handleARUP.ts @@ -1,4 +1,5 @@ import { client } from "../../client"; +import { renderAreaList } from "../../dom/renderAreaList"; import { safeTags } from "../../encoding"; /** @@ -10,7 +11,6 @@ export const handleARUP = (args: string[]) => { for (let i = 0; i < args.length - 1; i++) { if (client.areas[i]) { // the server sends us ARUP before we even get the area list - const thisarea = document.getElementById(`area${i}`)!; switch (Number(args[0])) { case 0: // playercount client.areas[i].players = Number(args[i + 1]); @@ -25,18 +25,7 @@ export const handleARUP = (args: string[]) => { client.areas[i].locked = safeTags(args[i + 1]); break; } - - thisarea.className = `area-button area-${client.areas[ - i - ].status.toLowerCase()}`; - - thisarea.innerText = `${client.areas[i].name} (${client.areas[i].players}) [${client.areas[i].status}]`; - - thisarea.title = - `Players: ${client.areas[i].players}\n` + - `Status: ${client.areas[i].status}\n` + - `CM: ${client.areas[i].cm}\n` + - `Area lock: ${client.areas[i].locked}`; } } + renderAreaList(); }; diff --git a/webAO/packets/handlers/handleCI.ts b/webAO/packets/handlers/handleCI.ts index c0cbd84..58a6dad 100644 --- a/webAO/packets/handlers/handleCI.ts +++ b/webAO/packets/handlers/handleCI.ts @@ -8,8 +8,6 @@ import { handleCharacterInfo } from "../../client/handleCharacterInfo"; */ export const handleCI = (args: string[]) => { // Loop through the 10 characters that were sent - document.getElementById("client_loadingtext")!.innerHTML = - `Loading Character ${args[1]}/${client.char_list_length}`; for (let i = 2; i <= args.length - 2; i++) { if (i % 2 === 0) { const chargs = args[i].split("&"); diff --git a/webAO/packets/handlers/handleEI.ts b/webAO/packets/handlers/handleEI.ts index 3d15766..6602214 100644 --- a/webAO/packets/handlers/handleEI.ts +++ b/webAO/packets/handlers/handleEI.ts @@ -11,8 +11,6 @@ import { prepChat } from "../../encoding"; * @param {Array} args packet arguments */ export const handleEI = (args: string[]) => { - document.getElementById("client_loadingtext")!.innerHTML = - `Loading Evidence ${args[1]}/${client.evidence_list_length}`; const evidenceID = Number(args[1]); const arg = args[2].split("&"); client.evidences[evidenceID] = { diff --git a/webAO/packets/handlers/handleEM.ts b/webAO/packets/handlers/handleEM.ts index b3947dd..94d224c 100644 --- a/webAO/packets/handlers/handleEM.ts +++ b/webAO/packets/handlers/handleEM.ts @@ -10,7 +10,6 @@ import { isAudio } from "../../client/isAudio"; * @param {Array} args packet arguments */ export const handleEM = (args: string[]) => { - document.getElementById("client_loadingtext")!.innerHTML = "Loading Music"; if (args[1] === "0") { client.resetMusicList(); client.resetAreaList(); diff --git a/webAO/packets/handlers/handleSM.ts b/webAO/packets/handlers/handleSM.ts index 1c3fd5e..17453c1 100644 --- a/webAO/packets/handlers/handleSM.ts +++ b/webAO/packets/handlers/handleSM.ts @@ -8,14 +8,11 @@ import { createArea } from "../../client/createArea"; * @param {Array} args packet arguments */ export const handleSM = (args: string[]) => { - document.getElementById("client_loadingtext")!.innerHTML = "Loading Music "; client.resetMusicList(); client.resetAreaList(); client.musics_time = false; - document.getElementById("client_loadingtext")!.innerHTML = `Loading Music`; - for (let i = 1; i < args.length - 1; i++) { // Check when found the song for the first time const trackname = args[i]; diff --git a/webAO/styles/client.css b/webAO/styles/client.css index e3ee820..773e9aa 100644 --- a/webAO/styles/client.css +++ b/webAO/styles/client.css @@ -58,25 +58,75 @@ } } +#client_error_overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + display: flex; + justify-content: center; + align-items: center; + z-index: 200; +} + #client_error { - position: absolute; display: flex; flex-direction: column; - padding: 10px; - top: 50%; - left: 50%; - margin-right: -50%; - transform: translate(-50%, -50%); + padding: 24px 36px; justify-content: center; align-items: center; - background: #a00; + background: rgba(0, 0, 0, 0.9); + border: 2px solid #c00; + border-radius: 8px; color: #fff; font-size: large; - z-index: 100; + min-width: 280px; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5); +} + +#client_error_icon { + font-size: 48px; + color: #f44; + margin-bottom: 4px; } #client_errortext { - animation: error_blink 3s ease-in-out infinite; + margin: 4px 0 8px; + text-align: center; +} + +#client_error_code { + color: #999; + font-size: 12px; + margin: 0 0 16px; +} + +#client_reconnect { + background: #c00; + color: #fff; + border: none; + border-radius: 4px; + padding: 10px 32px; + font-size: 16px; + cursor: pointer; + margin-bottom: 12px; + transition: background 0.2s; +} + +#client_reconnect:hover { + background: #e22; +} + +#client_error_help { + color: #aaa; + font-size: 12px; + margin: 0; +} + +#client_error_help a { + color: #6af; } #client_secondfactor { @@ -102,10 +152,12 @@ justify-content: center; text-align: center; align-items: center; + box-sizing: border-box; font-size: large; - overflow-y: scroll; + overflow-y: auto; z-index: 100; - background: #555; + background: #333; + border: 1px solid #000; } #client_loading { @@ -114,8 +166,7 @@ justify-content: center; text-align: center; align-items: center; - background: black; - color: lightgreen; + color: #fff; font-size: large; } @@ -142,7 +193,22 @@ display: block; text-align: center; margin: 0 auto; - background: #444; + padding: 10px; + background: #333; + color: #eee; +} + +#client_charselect_toolbar { + display: flex; + justify-content: center; + align-items: center; + gap: 6px; + margin-top: 5px; + margin-bottom: 10px; +} + +#client_charactersearch { + width: 150px; } #client_icwrapper { |
