Rework the CommonwealthOnline browser UI: restructure HTML into a nav + subpanel layout, add SVG/PNG/font assets and new sprites, and overhaul styles with a new font-face and refreshed color/spacing variables. Refactor components: tabs use a left nav list; filters extraction (renderRow) and reset focus handling; direct-connect renders into a dedicated container; server-list switched to compact row layout and adds a scroll indicator; server-details now uses a preview area and simplified preview/stats rendering. Update app logic: adjust focus zone handling (nextFocusZone), limit zones per tab, update connection status/count display, and avoid rendering details for direct-connect. Overall changes improve responsiveness, visual hierarchy, and focus/navigation behavior.
542 lines
16 KiB
JavaScript
542 lines
16 KiB
JavaScript
(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 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: [],
|
|
connectionStatus: "Online",
|
|
isRefreshing: false,
|
|
directConnect: { address: "127.0.0.1", port: "7777" },
|
|
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 "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);
|
|
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 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 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 getSelectedServer() {
|
|
return getActiveList()[state.selectedServerIndex] || null;
|
|
}
|
|
|
|
function getActiveList() {
|
|
if (state.activeTab === "favorites") return state.favorites;
|
|
if (state.activeTab === "recent") return state.recent;
|
|
return state.servers;
|
|
}
|
|
|
|
function joinServerLocal(serverId) {
|
|
var server = findServerById(serverId);
|
|
if (!server) return;
|
|
showJoinOverlay("Connecting to " + server.name + "...");
|
|
sendAction("joinServer", { serverId: serverId });
|
|
}
|
|
|
|
function joinDirectLocal(address, port) {
|
|
showJoinOverlay("Connecting to " + address + ":" + port + "...");
|
|
sendAction("openDirectConnect", { address: address, port: port });
|
|
}
|
|
|
|
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) {
|
|
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";
|
|
sendAction("switchTab", { tabName: tabName });
|
|
render();
|
|
}
|
|
|
|
function render() {
|
|
if (!root) return;
|
|
|
|
panel.classList.toggle("co-loading", state.isRefreshing);
|
|
|
|
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;
|
|
|
|
var listState = Object.assign({}, state, { servers: listServers });
|
|
if (state.activeTab === "favorites" || state.activeTab === "recent") {
|
|
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();
|
|
}
|
|
|
|
function bindUi() {
|
|
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 && e.key === "Escape") {
|
|
hideJoinOverlay();
|
|
return;
|
|
}
|
|
if (state.activeTab === "direct-connect") 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") 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 === "a") {
|
|
var vals = CO_DirectConnect.getValues(root);
|
|
joinDirectLocal(vals.address, vals.port);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (button === "a") {
|
|
joinSelected();
|
|
return;
|
|
}
|
|
if (button === "x") {
|
|
sendAction("refreshServerList", {});
|
|
return;
|
|
}
|
|
if (button === "y") {
|
|
var sel = getSelectedServer();
|
|
if (sel) toggleFavoriteLocal(sel.id);
|
|
return;
|
|
}
|
|
|
|
navigateFocus(button);
|
|
}
|
|
|
|
function joinSelected() {
|
|
var sel = getSelectedServer();
|
|
if (!sel) return;
|
|
joinServerLocal(sel.id);
|
|
}
|
|
|
|
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" && state.selectedFilterIndex === maxFilter) resetLocalFilters();
|
|
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 "joinStarted":
|
|
showJoinOverlay("Connecting...");
|
|
break;
|
|
case "joinFailed":
|
|
showJoinOverlay(event.reason || "Connection failed", true);
|
|
break;
|
|
case "joinSucceeded":
|
|
hideJoinOverlay();
|
|
addRecent(event.serverId);
|
|
break;
|
|
case "connectionStatusChanged":
|
|
state.connectionStatus = event.status || "Online";
|
|
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(serverId) {
|
|
var server = findServerById(serverId);
|
|
if (!server) return;
|
|
state.recent = state.recent.filter(function (s) { return s.id !== serverId; });
|
|
state.recent.unshift(server);
|
|
if (state.recent.length > 8) state.recent.pop();
|
|
}
|
|
|
|
window.setConnectionStatus = function (connected, logPrefix) {
|
|
state.connectionStatus = connected ? "Connected" : "Online";
|
|
render();
|
|
};
|
|
|
|
window.onBrowserShown = function () {
|
|
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; }
|
|
};
|
|
|
|
state.allServers = (window.CO_MOCK_SERVERS || []).slice();
|
|
applyLocalFilters();
|
|
bindUi();
|
|
render();
|
|
console.log("[CO] Server browser loaded");
|
|
})();
|