Files
Commonwealth-Online-Public/ui/views/CommonwealthOnline/browser/browser.js
T
andrew 549c117818 Restyle server browser UI; hide overlay on join
Restyles the server browser to match main-menu/settings (Futura PT Demibold, amber/green palette, yellow selection bars, shared panel chrome), removes SVG border strips, and tweaks modal width/layout. Adds JS + C++ coordination so the custom main-menu overlay hides when a server join begins (C++ invokes onSessionLoading and calls HideMainMenu; CompleteJoinFailure restores the menu/browser). Controller glyph tint now derives from --co-amber. Updates changelog and dev-log; includes compiled Vault109 script update.
2026-07-07 17:43:52 +12:00

1014 lines
32 KiB
JavaScript

(function () {
"use strict";
function buildMarkup(layout) {
var layoutClass = layout === "embedded" ? " co-browser--embedded" : " co-browser--standalone";
return (
'<div class="co-root' +
layoutClass +
'" id="coRoot">' +
'<div class="co-panel" id="coPanel">' +
'<h1 class="co-panel__heading">MULTIPLAYER</h1>' +
'<div class="co-subpanel">' +
'<nav class="co-nav-list"></nav>' +
'<div class="co-subpanel-content">' +
'<div class="co-tab-content">' +
'<div class="co-tab-pane" data-pane="direct-connect">' +
'<header class="co-subpanel-header">' +
'<h2 class="co-subpanel-title">Direct Connect</h2>' +
"</header>" +
'<div class="co-direct-connect"></div>' +
"</div>" +
'<div class="co-tab-pane active" data-pane="server-browser">' +
'<header class="co-subpanel-header">' +
'<h2 class="co-subpanel-title" id="coSubpanelTitle">Servers</h2>' +
'<div class="co-subpanel-meta">' +
'<span id="coConnectionStatus">Online</span>' +
'<span class="co-count" id="coServerCount"></span>' +
"</div>" +
"</header>" +
'<div class="co-filters"></div>' +
'<div class="co-server-list-wrap">' +
'<div class="co-server-list"></div>' +
'<div class="co-scroll-indicator" aria-hidden="true">&raquo;&raquo;</div>' +
"</div>" +
"</div>" +
'<div class="co-tab-pane" data-pane="local">' +
'<header class="co-subpanel-header">' +
'<h2 class="co-subpanel-title" id="coLocalTitle">Local Servers</h2>' +
'<div class="co-subpanel-meta">' +
'<span class="co-count" id="coLocalCount"></span>' +
"</div>" +
"</header>" +
'<div class="co-server-list-wrap">' +
'<div class="co-server-list co-local-list"></div>' +
'<div class="co-scroll-indicator" aria-hidden="true">&raquo;&raquo;</div>' +
"</div>" +
"</div>" +
'<div class="co-tab-pane" data-pane="favorites">' +
'<header class="co-subpanel-header">' +
'<h2 class="co-subpanel-title">Favorites</h2>' +
"</header>" +
'<div class="co-preview-area"></div>' +
'<div class="co-server-list-wrap">' +
'<div class="co-server-list co-favorites-list"></div>' +
'<div class="co-scroll-indicator" aria-hidden="true">&raquo;&raquo;</div>' +
"</div>" +
"</div>" +
'<div class="co-tab-pane" data-pane="recent">' +
'<header class="co-subpanel-header">' +
'<h2 class="co-subpanel-title">Recent</h2>' +
"</header>" +
'<div class="co-preview-area"></div>' +
'<div class="co-server-list-wrap">' +
'<div class="co-server-list co-recent-list"></div>' +
'<div class="co-scroll-indicator" aria-hidden="true">&raquo;&raquo;</div>' +
"</div>" +
"</div>" +
"</div>" +
"</div>" +
"</div>" +
'<footer class="co-action-bar">' +
'<div class="co-prompts"></div>' +
"</footer>" +
'<div class="co-join-overlay hidden" id="coJoinOverlay">' +
'<div class="co-spinner"></div>' +
'<p id="coJoinMessage">Connecting...</p>' +
"</div>" +
"</div>" +
"</div>"
);
}
/**
* Reusable Commonwealth Online server browser UI.
*
* var browser = CO_Browser.create({
* mount: document.getElementById("browserPanel"),
* layout: "embedded",
* onBack: function () { ... },
* exposeGlobals: true,
* });
* browser.show();
*/
function create(options) {
if (!options || !options.mount) {
throw new Error("CO_Browser.create requires a mount element");
}
var mountEl = options.mount;
var layout = options.layout || "embedded";
var onBack = typeof options.onBack === "function" ? options.onBack : function () {};
var exposeGlobals = options.exposeGlobals !== false;
var captureKeys = options.captureKeys === true;
var visible = false;
if (!mountEl.querySelector("#coRoot")) {
mountEl.innerHTML = buildMarkup(layout);
}
var root = mountEl.querySelector("#coRoot");
var panel = mountEl.querySelector("#coPanel");
var joinOverlay = mountEl.querySelector("#coJoinOverlay");
var joinMessage = mountEl.querySelector("#coJoinMessage");
if (!root || !panel || !joinOverlay || !joinMessage) {
throw new Error("CO_Browser.create failed to mount browser markup");
}
var FOCUS_ZONES = ["tabs", "filters", "server-list"];
var REPEAT_INITIAL_MS = 400;
var REPEAT_RATE_MS = 80;
var repeatTimer = null;
var repeatAction = null;
var RECENT_STORAGE_KEY = "co_recent_servers";
var MAX_RECENT = 8;
var state = {
activeTab: "server-browser",
focusZone: "server-list",
selectedTabIndex: 1,
selectedServerIndex: 0,
selectedFilterIndex: 0,
filters: {
search: "",
region: "Any Region",
mode: "Any Mode",
worldspace: "Any Worldspace",
ping: "Any",
showFull: false,
hasPassword: false,
modded: false
},
allServers: [],
servers: [],
favorites: [],
recent: [],
localServers: [],
connectionStatus: "Online",
isRefreshing: false,
isScanningLocal: false,
directConnect: { address: "127.0.0.1", port: "7777" },
directConnectFieldIndex: 0,
joinOverlay: null
};
function sendAction(action, payload) {
var msg = JSON.stringify(Object.assign({ action: action }, payload || {}));
if (typeof window.coAction === "function") {
window.coAction(msg);
return;
}
console.warn("[CO] coAction bridge unavailable:", action);
handleActionLocal(action, payload || {});
}
function handleActionLocal(action, payload) {
switch (action) {
case "refreshServerList":
refreshLocalMock();
break;
case "scanLocalServers":
scanLocalMock();
break;
case "setFilter":
state.filters[payload.filterName] = payload.value;
applyLocalFilters();
render();
break;
case "resetFilters":
resetLocalFilters();
break;
case "toggleFavorite":
toggleFavoriteLocal(payload.serverId);
break;
case "joinServer":
joinServerLocal(payload.serverId);
break;
case "openDirectConnect":
joinDirectLocal(payload.address, payload.port);
break;
case "switchTab":
switchTab(payload.tabName, { skipAction: true });
if (payload.tabName === "local") {
scanLocalMock();
}
break;
case "closeBrowser":
if (typeof window.closeBrowser === "function") {
window.closeBrowser();
} else {
hide();
onBack();
}
break;
default:
break;
}
}
function refreshLocalMock() {
state.isRefreshing = true;
render();
setTimeout(function () {
state.allServers = (window.CO_MOCK_SERVERS || []).slice();
applyLocalFilters();
state.isRefreshing = false;
render();
}, 600);
}
function scanLocalMock() {
state.isScanningLocal = true;
render();
setTimeout(function () {
state.localServers = (window.CO_MOCK_SERVERS || []).filter(function (s) {
return s.region === "Local" ||
(s.host && window.CO_ServerList && CO_ServerList.isLocalHost(s.host));
}).map(function (s) {
return {
id: s.id || ("local:" + (s.host || "127.0.0.1") + ":" + portNumFrom(s.port)),
name: s.name || "",
host: s.host || "127.0.0.1",
port: portNumFrom(s.port),
region: "Local",
players: s.players || 0,
maxPlayers: s.maxPlayers || 16,
ping: s.ping || 1,
worldspace: s.worldspace || "",
mode: s.mode || ""
};
});
state.isScanningLocal = false;
if (state.activeTab === "local" && state.selectedServerIndex >= state.localServers.length) {
state.selectedServerIndex = Math.max(0, state.localServers.length - 1);
}
render();
}, 800);
}
function resetLocalFilters() {
state.filters = {
search: "",
region: "Any Region",
mode: "Any Mode",
worldspace: "Any Worldspace",
ping: "Any",
showFull: false,
hasPassword: false,
modded: false
};
sendAction("resetFilters", {});
applyLocalFilters();
render();
}
function applyLocalFilters() {
var list = state.allServers.slice();
var f = state.filters;
if (f.search) {
var q = f.search.toLowerCase();
list = list.filter(function (s) {
return s.name.toLowerCase().indexOf(q) >= 0;
});
}
if (f.region !== "Any Region") {
list = list.filter(function (s) { return s.region === f.region; });
}
if (f.mode !== "Any Mode") {
list = list.filter(function (s) { return s.mode === f.mode; });
}
if (f.worldspace !== "Any Worldspace") {
list = list.filter(function (s) { return s.worldspace === f.worldspace; });
}
if (f.ping === "< 50 ms") list = list.filter(function (s) { return s.ping < 50; });
else if (f.ping === "< 100 ms") list = list.filter(function (s) { return s.ping < 100; });
else if (f.ping === "< 150 ms") list = list.filter(function (s) { return s.ping < 150; });
if (!f.showFull) list = list.filter(function (s) { return s.status !== "Full"; });
if (f.hasPassword) list = list.filter(function (s) { return s.passworded; });
if (f.modded) list = list.filter(function (s) { return s.modded; });
state.servers = list;
state.favorites = state.allServers.filter(function (s) { return s.favorite; });
if (state.selectedServerIndex >= state.servers.length) {
state.selectedServerIndex = Math.max(0, state.servers.length - 1);
}
updateServerCount();
}
function updateServerCount() {
var countEl = root.querySelector("#coServerCount");
var titleEl = root.querySelector("#coSubpanelTitle");
var count = state.servers.length;
if (countEl) {
countEl.textContent = count + " Found";
}
if (titleEl && state.activeTab === "server-browser") {
titleEl.textContent = count === 1 ? "1 Server" : count + " Servers";
}
}
function updateLocalCount() {
var countEl = root.querySelector("#coLocalCount");
var titleEl = root.querySelector("#coLocalTitle");
if (!countEl && !titleEl) {
return;
}
var count = state.localServers.length;
if (countEl) {
if (state.isScanningLocal) {
countEl.textContent = "Scanning";
} else {
countEl.textContent = count + " Found";
}
}
if (titleEl && state.activeTab === "local") {
if (state.isScanningLocal) {
titleEl.textContent = "Scanning...";
} else {
titleEl.textContent = count === 1 ? "1 Local Server" : count + " Local Servers";
}
}
}
function toggleFavoriteLocal(serverId) {
var server = findServerById(serverId);
if (!server) {
return;
}
server.favorite = !server.favorite;
sendAction("toggleFavorite", { serverId: serverId });
applyLocalFilters();
render();
}
function findServerById(id) {
for (var i = 0; i < state.allServers.length; i++) {
if (state.allServers[i].id === id) {
return state.allServers[i];
}
}
return null;
}
function findLocalServerById(id) {
for (var i = 0; i < state.localServers.length; i++) {
if (state.localServers[i].id === id) {
return state.localServers[i];
}
}
return null;
}
function getSelectedServer() {
return getActiveList()[state.selectedServerIndex] || null;
}
function getActiveList() {
if (state.activeTab === "favorites") return state.favorites;
if (state.activeTab === "recent") return state.recent;
if (state.activeTab === "local") return state.localServers;
return state.servers;
}
function recentEntryKey(server) {
if (!server) return "";
var host = server.host || server.address || "";
if (host) {
return host + ":" + portNumFrom(server.port);
}
return server.id || "";
}
function snapshotRecentEntry(server) {
return {
id: server.id || recentEntryKey(server),
name: server.name || "",
host: server.host || server.address || "",
port: portNumFrom(server.port),
region: server.region || "",
players: server.players || 0,
maxPlayers: server.maxPlayers || 0,
ping: server.ping || 0,
worldspace: server.worldspace || "",
mode: server.mode || "",
favorite: !!server.favorite
};
}
function loadRecent() {
try {
var raw = localStorage.getItem(RECENT_STORAGE_KEY);
if (!raw) return;
var parsed = JSON.parse(raw);
if (Array.isArray(parsed)) {
state.recent = parsed.slice(0, MAX_RECENT);
}
} catch (err) {
console.warn("[CO] Failed to load recent servers:", err);
}
}
function saveRecent() {
try {
localStorage.setItem(RECENT_STORAGE_KEY, JSON.stringify(state.recent));
} catch (err) {
console.warn("[CO] Failed to save recent servers:", err);
}
}
function joinServerEntry(server) {
if (!server) return;
var label = window.CO_ServerList
? CO_ServerList.displayName(server)
: (server.name || server.host || "server");
showJoinOverlay("Connecting to " + label + "...");
var browserEntry = server.id ? (findServerById(server.id) || findLocalServerById(server.id)) : null;
if (browserEntry) {
sendAction("joinServer", { serverId: server.id });
return;
}
var host = server.host || server.address;
if (host) {
sendAction("openDirectConnect", {
address: host,
port: String(portNumFrom(server.port))
});
return;
}
if (server.id) {
sendAction("joinServer", { serverId: server.id });
return;
}
showJoinOverlay("Server connection details unavailable.", true);
}
function findServerInActiveList(serverId) {
var list = getActiveList();
for (var i = 0; i < list.length; i++) {
if (list[i].id === serverId) {
return list[i];
}
}
return null;
}
function joinServerLocal(serverId) {
var server = findServerById(serverId) || findServerInActiveList(serverId);
if (!server) return;
joinServerEntry(server);
}
function validateDirectConnect(address, port) {
if (!address || !String(address).trim()) {
return "Server address is required.";
}
var portNum = parseInt(port, 10);
if (!port || isNaN(portNum) || portNum < 1 || portNum > 65535) {
return "Port must be a number between 1 and 65535.";
}
return null;
}
function joinDirectLocal(address, port) {
var validationError = validateDirectConnect(address, port);
if (validationError) {
showJoinOverlay(validationError, true);
return;
}
showJoinOverlay("Connecting to " + address + ":" + port + "...");
sendAction("openDirectConnect", { address: address, port: String(portNumFrom(port)) });
}
function portNumFrom(port) {
var portNum = parseInt(port, 10);
return isNaN(portNum) ? 7777 : portNum;
}
function showJoinOverlay(message, isError) {
state.joinOverlay = { message: message, error: !!isError };
joinOverlay.classList.remove("hidden");
if (isError) joinOverlay.classList.add("co-error");
else joinOverlay.classList.remove("co-error");
joinMessage.textContent = message;
var spinner = joinOverlay.querySelector(".co-spinner");
if (spinner) spinner.style.display = isError ? "none" : "block";
render();
}
function hideJoinOverlay() {
state.joinOverlay = null;
joinOverlay.classList.add("hidden");
render();
}
function switchTab(tabName, switchOptions) {
switchOptions = switchOptions || {};
state.activeTab = tabName;
state.selectedTabIndex = CO_Tabs.TAB_ORDER.indexOf(tabName);
if (state.selectedTabIndex < 0) state.selectedTabIndex = 0;
if (tabName === "server-browser") state.focusZone = "server-list";
else if (tabName === "direct-connect") {
state.focusZone = "tabs";
state.directConnectFieldIndex = 0;
} else if (tabName === "favorites" || tabName === "recent" || tabName === "local") {
var list = state.servers;
if (tabName === "favorites") list = state.favorites;
else if (tabName === "recent") list = state.recent;
else if (tabName === "local") list = state.localServers;
if (state.selectedServerIndex >= list.length) {
state.selectedServerIndex = Math.max(0, list.length - 1);
}
if (tabName === "local") {
state.focusZone = "server-list";
}
}
if (!switchOptions.skipAction) {
sendAction("switchTab", { tabName: tabName });
}
render();
}
function render() {
if (!root) return;
panel.classList.toggle("co-loading", state.isRefreshing || state.isScanningLocal);
CO_Tabs.render(root, state);
CO_ControllerPrompts.render(root, state);
var statusEl = root.querySelector("#coConnectionStatus");
if (statusEl) {
statusEl.textContent = String(state.connectionStatus || "Online").toUpperCase();
}
root.querySelectorAll(".co-tab-pane").forEach(function (pane) {
pane.classList.toggle("active", pane.getAttribute("data-pane") === state.activeTab);
});
if (state.activeTab === "direct-connect") {
CO_DirectConnect.render(root, state);
} else {
var listServers = state.servers;
if (state.activeTab === "favorites") listServers = state.favorites;
if (state.activeTab === "recent") listServers = state.recent;
if (state.activeTab === "local") listServers = state.localServers;
var listState = Object.assign({}, state, { servers: listServers });
if (state.activeTab === "favorites" || state.activeTab === "recent" || state.activeTab === "local") {
listState.focusZone = state.focusZone === "filters" ? "server-list" : state.focusZone;
}
if (state.activeTab === "server-browser") {
CO_Filters.render(root, listState);
}
if (state.activeTab !== "direct-connect") {
CO_ServerDetails.render(root, listState);
}
var activePane = root.querySelector('.co-tab-pane[data-pane="' + state.activeTab + '"]');
if (activePane) {
var listEl = activePane.querySelector(".co-server-list");
if (listEl) {
var tempRoot = {
querySelector: function (sel) {
if (sel === ".co-server-list-wrap") return activePane.querySelector(".co-server-list-wrap");
if (sel === ".co-server-list") return listEl;
return root.querySelector(sel);
}
};
CO_ServerList.render(tempRoot, listState);
}
}
}
updateServerCount();
updateLocalCount();
}
function bindUi() {
if (window.CO_Hover) {
CO_Hover.bind(root);
}
CO_Tabs.bind(root, function (tabName) {
switchTab(tabName);
});
CO_Filters.bind(root, function (name, value) {
state.filters[name] = value;
sendAction("setFilter", { filterName: name, value: value });
applyLocalFilters();
render();
}, resetLocalFilters);
root.addEventListener("click", function (e) {
var row = e.target.closest(".co-tab-pane.active .co-server-row");
if (!row) return;
state.selectedServerIndex = parseInt(row.getAttribute("data-index"), 10);
render();
});
root.addEventListener("dblclick", function (e) {
var row = e.target.closest(".co-tab-pane.active .co-server-row");
if (!row) return;
state.selectedServerIndex = parseInt(row.getAttribute("data-index"), 10);
joinSelected();
});
CO_DirectConnect.bind(root, function (address, port) {
state.directConnect = { address: address, port: port };
joinDirectLocal(address, port);
});
if (captureKeys) {
document.addEventListener("keydown", onKeyDown);
}
}
function onKeyDown(e) {
if (!visible) {
return;
}
if (state.joinOverlay && state.joinOverlay.error) {
if (e.key === "Escape" || e.key === "Enter" || e.key === "b" || e.key === "B") {
hideJoinOverlay();
return;
}
}
if (e.repeat) return;
handleControllerButton(mapKeyToButton(e.key), "down");
}
function mapKeyToButton(key) {
var map = {
ArrowUp: "dpad_up", ArrowDown: "dpad_down", ArrowLeft: "dpad_left", ArrowRight: "dpad_right",
Enter: "a", Escape: "b", " ": "a", r: "x", f: "y", q: "lb", e: "rb"
};
return map[key] || null;
}
function handleControllerButton(button, phase) {
if (!visible || !button || phase !== "down") return;
if (state.joinOverlay) {
if (button === "b" || (state.joinOverlay.error && button === "a")) {
hideJoinOverlay();
}
return;
}
if (button === "lb" || button === "rb") {
var idx = state.selectedTabIndex;
idx = button === "lb" ? idx - 1 : idx + 1;
if (idx < 0) idx = CO_Tabs.TAB_ORDER.length - 1;
if (idx >= CO_Tabs.TAB_ORDER.length) idx = 0;
switchTab(CO_Tabs.TAB_ORDER[idx]);
return;
}
if (button === "b") {
sendAction("closeBrowser", {});
return;
}
if (state.activeTab === "direct-connect") {
if (button === "dpad_up") {
state.directConnectFieldIndex = Math.max(0, state.directConnectFieldIndex - 1);
render();
return;
}
if (button === "dpad_down") {
state.directConnectFieldIndex = Math.min(1, state.directConnectFieldIndex + 1);
render();
return;
}
if (button === "a") {
var vals = CO_DirectConnect.getValues(root);
joinDirectLocal(vals.address, vals.port);
}
return;
}
if (button === "a") {
joinSelected();
return;
}
if (button === "x") {
if (state.activeTab === "local") {
sendAction("scanLocalServers", {});
} else {
sendAction("refreshServerList", {});
}
return;
}
if (button === "y") {
var sel = getSelectedServer();
if (sel) toggleFavoriteLocal(sel.id);
return;
}
navigateFocus(button);
}
function cycleFilterValue(filterKey, filterOptions) {
var current = state.filters[filterKey];
var index = filterOptions.indexOf(current);
if (index < 0) index = 0;
var next = filterOptions[(index + 1) % filterOptions.length];
state.filters[filterKey] = next;
sendAction("setFilter", { filterName: filterKey, value: next });
applyLocalFilters();
}
function activateFocusedFilter() {
var maxFilter = CO_Filters.FILTER_ROWS.length;
if (state.selectedFilterIndex === maxFilter) {
resetLocalFilters();
return;
}
var row = CO_Filters.FILTER_ROWS[state.selectedFilterIndex];
if (!row) return;
if (row.type === "select") {
cycleFilterValue(row.key, row.options);
} else if (row.type === "checkbox") {
state.filters[row.key] = !state.filters[row.key];
sendAction("setFilter", { filterName: row.key, value: state.filters[row.key] });
applyLocalFilters();
}
}
function joinSelected() {
var sel = getSelectedServer();
if (!sel) return;
joinServerEntry(sel);
}
function nextFocusZone(fromIdx, direction) {
var zones = FOCUS_ZONES.slice();
if (state.activeTab !== "server-browser") {
zones = zones.filter(function (z) { return z !== "filters"; });
}
var current = zones.indexOf(state.focusZone);
if (current < 0) current = fromIdx;
var next = current + direction;
if (next < 0 || next >= zones.length) return null;
return zones[next];
}
function navigateFocus(button) {
var list = getActiveList();
if (button === "dpad_left") {
var prevZone = nextFocusZone(FOCUS_ZONES.indexOf(state.focusZone), -1);
if (prevZone) {
state.focusZone = prevZone;
render();
}
return;
}
if (button === "dpad_right") {
if (state.activeTab === "direct-connect") return;
var nextZone = nextFocusZone(FOCUS_ZONES.indexOf(state.focusZone), 1);
if (nextZone) {
state.focusZone = nextZone;
render();
}
return;
}
if (state.focusZone === "tabs") {
if (button === "dpad_up" || button === "dpad_down") {
var dir = button === "dpad_up" ? -1 : 1;
state.selectedTabIndex = (state.selectedTabIndex + dir + CO_Tabs.TAB_ORDER.length) % CO_Tabs.TAB_ORDER.length;
switchTab(CO_Tabs.TAB_ORDER[state.selectedTabIndex]);
}
return;
}
if (state.focusZone === "filters") {
var maxFilter = CO_Filters.FILTER_ROWS.length;
if (button === "dpad_up") state.selectedFilterIndex = Math.max(0, state.selectedFilterIndex - 1);
if (button === "dpad_down") state.selectedFilterIndex = Math.min(maxFilter, state.selectedFilterIndex + 1);
if (button === "a") activateFocusedFilter();
render();
return;
}
if (state.focusZone === "server-list") {
if (button === "dpad_up") state.selectedServerIndex = Math.max(0, state.selectedServerIndex - 1);
if (button === "dpad_down") state.selectedServerIndex = Math.min(list.length - 1, state.selectedServerIndex + 1);
render();
CO_ServerList.scrollToSelected(root, state.selectedServerIndex);
}
}
function startRepeat(action) {
stopRepeat();
repeatAction = action;
repeatTimer = setTimeout(function tick() {
if (repeatAction) repeatAction();
repeatTimer = setTimeout(tick, REPEAT_RATE_MS);
}, REPEAT_INITIAL_MS);
}
function stopRepeat() {
if (repeatTimer) clearTimeout(repeatTimer);
repeatTimer = null;
repeatAction = null;
}
function dispatchCoEvent(event) {
if (!event || !event.type) return;
switch (event.type) {
case "serverListUpdated":
state.allServers = event.servers || [];
applyLocalFilters();
break;
case "serverListRefreshStarted":
state.isRefreshing = true;
break;
case "serverListRefreshFinished":
state.isRefreshing = false;
if (!event.success && event.error) {
console.warn("[CO] Refresh failed:", event.error);
}
break;
case "localScanStarted":
state.isScanningLocal = true;
break;
case "localServersUpdated":
state.localServers = event.servers || [];
if (state.activeTab === "local" && state.selectedServerIndex >= state.localServers.length) {
state.selectedServerIndex = Math.max(0, state.localServers.length - 1);
}
break;
case "localScanFinished":
state.isScanningLocal = false;
if (!event.success) {
console.warn("[CO] Local scan found no servers.", event.error || "");
}
break;
case "joinStarted":
showJoinOverlay("Connecting...");
break;
case "joinLoading":
showJoinOverlay(event.message || "Loading...");
break;
case "joinFailed":
showJoinOverlay(event.reason || "Connection failed", true);
break;
case "joinSucceeded":
hideJoinOverlay();
addRecent(event.server || event.serverId);
break;
case "connectionStatusChanged":
state.connectionStatus = event.status || "Online";
break;
case "uiTheme":
if (typeof window.applyCoTheme === "function") {
window.applyCoTheme(event);
}
break;
case "menuOpened":
break;
case "menuClosed":
hideJoinOverlay();
break;
case "controllerInput":
if (event.phase === "down") {
handleControllerButton(event.button, "down");
if (event.button === "dpad_up" || event.button === "dpad_down") {
startRepeat(function () { handleControllerButton(event.button, "down"); });
}
} else if (event.phase === "up") {
stopRepeat();
}
break;
case "uiSound":
break;
default:
break;
}
render();
}
function addRecent(serverOrId, serverSnapshot) {
var server = serverSnapshot;
if (!server) {
if (serverOrId && typeof serverOrId === "object") {
server = serverOrId;
} else if (serverOrId) {
server = findServerById(serverOrId);
}
}
if (!server) return;
var entry = snapshotRecentEntry(server);
var key = recentEntryKey(entry);
state.recent = state.recent.filter(function (s) {
return recentEntryKey(s) !== key && s.id !== entry.id;
});
state.recent.unshift(entry);
if (state.recent.length > MAX_RECENT) {
state.recent.length = MAX_RECENT;
}
saveRecent();
}
function setConnectionStatus(connected) {
state.connectionStatus = connected ? "Connected" : "Online";
render();
}
function setPointerMode(mode) {
if (!root) return;
var gamepad = mode === "gamepad";
root.classList.toggle("co-gamepad-mode", gamepad);
if (window.CO_Hover && CO_Hover.clear) {
CO_Hover.clear();
}
}
function onShown() {
if (window.CO_Layout && CO_Layout.update) {
CO_Layout.update();
}
dispatchCoEvent({ type: "menuOpened" });
render();
}
function show() {
visible = true;
render();
}
function hide() {
visible = false;
hideJoinOverlay();
stopRepeat();
}
function handleKeydown(event) {
if (!visible) {
return false;
}
onKeyDown(event);
return true;
}
function handleResize() {
if (window.CO_Layout && CO_Layout.update) {
CO_Layout.update();
}
render();
}
if (exposeGlobals) {
window.dispatchCoEvent = dispatchCoEvent;
window.setConnectionStatus = setConnectionStatus;
window.onBrowserShown = onShown;
window.coAction = window.coAction || function () {
console.warn("[CO] coAction bridge not ready");
};
window.CO_App = window.CO_App || {};
window.CO_App.joinSelected = joinSelected;
window.CO_App.getState = function () { return state; };
window.CO_App.setPointerMode = setPointerMode;
}
loadRecent();
state.allServers = (window.CO_MOCK_SERVERS || []).slice();
applyLocalFilters();
bindUi();
window.addEventListener("resize", handleResize);
render();
return {
show: show,
hide: hide,
onShown: onShown,
isVisible: function () { return visible; },
handleKeydown: handleKeydown,
handleControllerInput: handleControllerButton,
render: render,
dispatchCoEvent: dispatchCoEvent,
setPointerMode: setPointerMode
};
}
window.CO_Browser = {
create: create
};
})();