Embed server browser as main menu modal
Refactors the browser UI into a reusable `CO_Browser.create()` module and adds embedded modal styling so MULTIPLAYER opens inside the main menu instead of swapping to a separate view. Updates main-menu JS/CSS to host, animate, and route input for the browser panel while keeping nav and prompts in sync. On the C++ side, PrismaUI now drives browser show/hide and event dispatch through the main menu view, and `ServerBrowserBridge::SetView()` was added so bridge events target the active host view.
This commit is contained in:
@@ -1,845 +1,19 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var root = document.getElementById("coRoot");
|
||||
var panel = document.getElementById("coPanel");
|
||||
var joinOverlay = document.getElementById("coJoinOverlay");
|
||||
var joinMessage = document.getElementById("coJoinMessage");
|
||||
|
||||
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 || {});
|
||||
var mount = document.body;
|
||||
if (!window.CO_Browser) {
|
||||
console.error("[CO] CO_Browser module is required before app.js");
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
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 = document.getElementById("coServerCount");
|
||||
var titleEl = document.getElementById("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 = document.getElementById("coLocalCount");
|
||||
var titleEl = document.getElementById("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, options) {
|
||||
options = options || {};
|
||||
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 (!options.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 = document.getElementById("coConnectionStatus");
|
||||
if (statusEl) {
|
||||
statusEl.textContent = String(state.connectionStatus || "Online").toUpperCase();
|
||||
}
|
||||
|
||||
document.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);
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
}
|
||||
|
||||
function onKeyDown(e) {
|
||||
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 (!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 === "b") {
|
||||
sendAction("closeBrowser", {});
|
||||
return;
|
||||
}
|
||||
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, options) {
|
||||
var current = state.filters[filterKey];
|
||||
var index = options.indexOf(current);
|
||||
if (index < 0) index = 0;
|
||||
var next = options[(index + 1) % options.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);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
window.dispatchCoEvent = function (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":
|
||||
// TODO: Wire to Fallout 4 menu sound via C++ when audio hooks are available.
|
||||
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();
|
||||
}
|
||||
|
||||
window.setConnectionStatus = function (connected, logPrefix) {
|
||||
state.connectionStatus = connected ? "Connected" : "Online";
|
||||
render();
|
||||
};
|
||||
|
||||
window.onBrowserShown = function () {
|
||||
if (window.CO_Layout && CO_Layout.update) {
|
||||
CO_Layout.update();
|
||||
}
|
||||
dispatchCoEvent({ type: "menuOpened" });
|
||||
render();
|
||||
};
|
||||
|
||||
window.coAction = window.coAction || function () {
|
||||
console.warn("[CO] coAction bridge not ready");
|
||||
};
|
||||
|
||||
window.CO_App = {
|
||||
joinSelected: joinSelected,
|
||||
getState: function () { return state; },
|
||||
setPointerMode: function (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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadRecent();
|
||||
state.allServers = (window.CO_MOCK_SERVERS || []).slice();
|
||||
applyLocalFilters();
|
||||
bindUi();
|
||||
window.addEventListener("resize", function () {
|
||||
if (window.CO_Layout && CO_Layout.update) {
|
||||
CO_Layout.update();
|
||||
}
|
||||
render();
|
||||
var browser = window.CO_Browser.create({
|
||||
mount: mount,
|
||||
layout: "standalone",
|
||||
exposeGlobals: true,
|
||||
captureKeys: true
|
||||
});
|
||||
render();
|
||||
console.log("[CO] Server browser loaded");
|
||||
|
||||
browser.show();
|
||||
console.log("[CO] Standalone server browser loaded");
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user