From 8eb11007a71708a9dee728ce8338214ecc02e8e3 Mon Sep 17 00:00:00 2001 From: Sainan Date: Tue, 28 May 2024 13:28:57 +0200 Subject: [PATCH] feat(webui): add Mods tab with "Add Riven" option (#234) --- src/routes/webui.ts | 13 +- static/webui/index.html | 172 ++++++++++++++++------ static/webui/script.js | 93 +++++++++++- static/webui/single.js | 317 ++++++++++++++++++++++++++++++++++++++++ static/webui/style.css | 17 +++ 5 files changed, 555 insertions(+), 57 deletions(-) create mode 100644 static/webui/single.js create mode 100644 static/webui/style.css diff --git a/src/routes/webui.ts b/src/routes/webui.ts index 0010f2e0..998953fc 100644 --- a/src/routes/webui.ts +++ b/src/routes/webui.ts @@ -2,6 +2,7 @@ import express from "express"; import path from "path"; const webuiRouter = express.Router(); +const rootDir = path.join(__dirname, "../.."); // Redirect / to /webui/ webuiRouter.get("/", (_req, res) => { @@ -16,7 +17,15 @@ webuiRouter.use("/webui", (req, res, next) => { next(); }); -// Serve static files from the webui directory -webuiRouter.use("/webui", express.static(path.join(__dirname, "../..", "static/webui"))); +// Serve virtual routes +webuiRouter.get("/webui/inventory", (_req, res) => { + res.sendFile(path.join(rootDir, "static/webui/index.html")); +}); +webuiRouter.get("/webui/mods", (_req, res) => { + res.sendFile(path.join(rootDir, "static/webui/index.html")); +}); + +// Serve static files +webuiRouter.use("/webui", express.static(path.join(rootDir, "static/webui"))); export { webuiRouter }; diff --git a/static/webui/index.html b/static/webui/index.html index d3770b59..c8556dc3 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -2,70 +2,142 @@ OpenWF WebUI + + -
-

OpenWF WebUI

-
-

Login using your OpenWF account credentials.

-
- - -
- - -
- -
+ +
+ +
+

Note: Changes made here will only be reflected in-game when the game re-downloads your inventory. Visiting the navigation should be the easiest way to trigger that.

-
-
Add Items
-
- - - +
+

Login using your OpenWF account credentials.

+ + + +
+ + +
+
-
-
-
-
Warframes
-
- - -
-
- - -
+
+
+
Add Items
+
+ + + +
+
+
+
+
+
Warframes
+
+ + +
+
+ + +
+
+
+
+
+
+
Weapons
+
+ + +
+
+ + +
+
-
-
-
Weapons
-
- - -
-
- - -
-
-
+
+
+
+
Add Riven
+
+ + + + Need help with the fingerprint? +
@@ -79,6 +151,12 @@ crossorigin="anonymous" > + + diff --git a/static/webui/script.js b/static/webui/script.js index ef61d4f8..cf9d05fb 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -1,6 +1,7 @@ function doLogin() { localStorage.setItem("email", $("#email").val()); localStorage.setItem("password", $("#password").val()); + $("#email, #password").val(""); loginFromLocalStorage(); } @@ -20,10 +21,12 @@ function loginFromLocalStorage() { }) }); req.done(data => { - $("#login-view").addClass("d-none"); - $("#main-view").removeClass("d-none"); + if (single.getCurrentPath() == "/webui/") { + single.loadRoute("/webui/inventory"); + } $(".displayname").text(data.DisplayName); window.accountId = data.id; + window.authz = "accountId=" + data.id + "&nonce=" + data.Nonce; updateInventory(); }); req.fail(() => { @@ -35,14 +38,25 @@ function loginFromLocalStorage() { function logout() { localStorage.removeItem("email"); localStorage.removeItem("password"); - $("#login-view").removeClass("d-none"); - $("#main-view").addClass("d-none"); } if (localStorage.getItem("email") && localStorage.getItem("password")) { loginFromLocalStorage(); } +single.on("route_load", function (event) { + if (event.route.paths[0] != "/webui/") { + // Authorised route? + if (!localStorage.getItem("email")) { + // Not logged in? + return single.loadRoute("/webui/"); // Show login screen + } + $("body").addClass("logged-in"); + } else { + $("body").removeClass("logged-in"); + } +}); + window.itemListPromise = new Promise(resolve => { const req = $.get("/custom/getItemLists"); req.done(data => { @@ -61,7 +75,7 @@ window.itemListPromise = new Promise(resolve => { }); function updateInventory() { - const req = $.get("/api/inventory.php?accountId=" + window.accountId); + const req = $.get("/api/inventory.php?" + window.authz); req.done(data => { window.itemListPromise.then(itemMap => { document.getElementById("warframe-list").innerHTML = ""; @@ -215,7 +229,7 @@ function addGearExp(category, oid, xp) { } ]; $.post({ - url: "/api/missionInventoryUpdate.php?accountId=" + window.accountId, + url: "/api/missionInventoryUpdate.php?" + window.authz, contentType: "text/plain", data: JSON.stringify(data) }).done(function () { @@ -235,7 +249,7 @@ function disposeOfGear(category, oid) { } ]; $.post({ - url: "/api/sell.php?accountId=" + window.accountId, + url: "/api/sell.php?" + window.authz, contentType: "text/plain", data: JSON.stringify(data) }).done(function () { @@ -250,7 +264,7 @@ function doAcquireMiscItems() { return; } $.post({ - url: "/api/missionInventoryUpdate.php?accountId=" + window.accountId, + url: "/api/missionInventoryUpdate.php?" + window.authz, contentType: "text/plain", data: JSON.stringify({ MiscItems: [ @@ -268,3 +282,66 @@ function doAcquireMiscItems() { $("#miscitem-name").on("input", () => { $("#miscitem-name").removeClass("is-invalid"); }); + +function doAcquireRiven() { + let fingerprint; + try { + fingerprint = JSON.parse($("#addriven-fingerprint").val()); + if (typeof fingerprint !== "object") { + fingerprint = JSON.parse(fingerprint); + } + } catch (e) {} + if ( + typeof fingerprint !== "object" || + !("compat" in fingerprint) || + !("pol" in fingerprint) || + !("buffs" in fingerprint) + ) { + $("#addriven-fingerprint").addClass("is-invalid").focus(); + return; + } + const uniqueName = "/Lotus/Upgrades/Mods/Randomized/" + $("#addriven-type").val(); + // Add riven type to inventory + $.post({ + url: "/api/missionInventoryUpdate.php?" + window.authz, + contentType: "text/plain", + data: JSON.stringify({ + RawUpgrades: [ + { + ItemType: uniqueName, + ItemCount: 1 + } + ] + }) + }).done(function () { + // Get riven's assigned id + $.get("/api/inventory.php?" + window.authz).done(data => { + for (const rawUpgrade of data.RawUpgrades) { + if (rawUpgrade.ItemType === uniqueName) { + // Add fingerprint to riven + $.post({ + url: "/api/artifacts.php?" + window.authz, + contentType: "text/plain", + data: JSON.stringify({ + Upgrade: { + ItemType: uniqueName, + UpgradeFingerprint: JSON.stringify(fingerprint), + ItemId: rawUpgrade.LastAdded + }, + LevelDiff: 0, + Cost: 0, + FusionPointCost: 0 + }) + }).done(function () { + alert("Successfully added."); + }); + break; + } + } + }); + }); +} + +$("#addriven-fingerprint").on("input", () => { + $("#addriven-fingerprint").removeClass("is-invalid"); +}); diff --git a/static/webui/single.js b/static/webui/single.js new file mode 100644 index 00000000..928d58e3 --- /dev/null +++ b/static/webui/single.js @@ -0,0 +1,317 @@ +(function () { + let head_include = document.body == null, + style = document.createElement("style"); + style.textContent = `[data-route]:not(.route-visible){display:none}`; + document.head.appendChild(style); + + class EventEmitter { + constructor() { + this.event_handlers = {}; + } + + on(event_name, func) { + if (typeof func != "function") { + throw "Event handler has to be a function."; + } + this.event_handlers[event_name] = func; + return this; + } + + off(event_name) { + delete this.event_handlers[event_name]; + return this; + } + + fire(event_name, args) { + if (event_name in this.event_handlers) { + this.event_handlers[event_name].call(this, args); + } + return this; + } + } + + class Route extends EventEmitter { + constructor(overlay, elm, paths) { + super(); + this.overlay = overlay; + this.elm = elm; + this.paths = paths; + this.title = undefined; + if (elm.hasAttribute("data-title")) { + this.title = this.elm.getAttribute("data-title"); + this.elm.removeAttribute("data-title"); + } else if (document.querySelector("title") != null) { + this.title = document.querySelector("title").textContent; + } else { + this.title = this.paths[0]; + } + } + + get element() { + return this.elm; + } + + isCurrent() { + return this.elm.classList.contains("route-current"); + } + + isVisible() { + return this.elm.classList.contains("route-visible"); + } + } + + class MultiRoute extends Route { + constructor(overlay, elm, paths_data) { + if (overlay) { + paths_data = paths_data.substr(9); + } + let paths = []; + paths_data.split(",").forEach(name => { + paths.push(name.trim()); + }); + super(overlay, elm, paths); + } + + getCanonicalPath() { + if (this.paths[0].substr(0, 1) == "/") { + return this.paths[0]; + } + if (this.paths.length > 1) { + return this.paths[1]; + } + return "/" + this.paths[0]; + } + } + + class StandardRoute extends MultiRoute { + constructor(overlay, elm, paths) { + super(overlay, elm, elm.getAttribute("data-route")); + } + } + + class RegexRoute extends Route { + constructor(overlay, elm) { + let regexp = elm.getAttribute("data-route").substr(2); + if (overlay) { + regexp = regexp.substr(9); + } + super(overlay, elm, [regexp]); + this.regex = new RegExp(regexp); + } + + getArgs(path) { + if (path === undefined) { + path = single.getCurrentPath(); + } + let res = this.regex.exec(path); + if (res && res.length > 0) { + return res; + } + return false; + } + } + + class SingleApp extends EventEmitter { + constructor() { + super(); + this.routes = []; + this.routes_populated = false; + if (!head_include) { + this.populateRoutes(); + } + window.onpopstate = event => { + event.preventDefault(); + single.loadRoute(); + }; + this.timeouts = []; + this.intervals = []; + } + + populateRoutes() { + if (this.routes_populated) { + return; + } + document.body.querySelectorAll("[data-route]").forEach(elm => { + let data = elm.getAttribute("data-route"), + overlay = false; + if (data.substr(0, 9) == "overlay: ") { + data = data.substr(9); + overlay = true; + } + if (data.substr(0, 2) == "~ ") { + this.routes.push(new RegexRoute(overlay, elm)); + } else { + this.routes.push(new StandardRoute(overlay, elm)); + } + }); + if (this.routes.length == 0) { + console.error("[single.js] You need to define at least one route"); + } + this.routes.forEach(route => { + route.paths.forEach(path => { + for (let i = 0; i < this.routes; i++) { + if (this.routes[i] !== route && this.routes[i].paths.indexOf(path) > -1) { + console.error("[single.js] Duplicate path: " + path); + } + } + }); + }); + document.body.addEventListener("click", event => { + let elm = event.target; + while (elm && !(elm instanceof HTMLAnchorElement)) { + elm = elm.parentNode; + } + if ( + elm instanceof HTMLAnchorElement && + !elm.hasAttribute("target") && + elm.hasAttribute("href") && + elm.getAttribute("href").substr(0, 1) == "/" + ) { + event.preventDefault(); + single.loadRoute(new URL(elm.href)); + } + }); + this.routes_populated = true; + } + + getRoute(route) { + this.populateRoutes(); + let is_elm = route instanceof HTMLElement; + if (is_elm) { + if (!route.hasAttribute("data-route")) { + throw "Invalid route element: " + route; + } + route = route.getAttribute("data-route"); + if (route.substr(0, 9) == "overlay: ") { + route = route.substr(9); + } + if (route.substr(0, 2) == "~ ") { + route = route.substr(2); + } else { + route = route.split(",")[0]; + } + } else { + if (route.substr(0, 9) == "overlay: ") { + route = route.substr(9); + } + if (route.substr(0, 2) == "~ ") { + route = route.substr(2); + } + } + for (let i = 0; i < this.routes.length; i++) { + if (this.routes[i].paths.indexOf(route) > -1) { + return this.routes[i]; + } + } + if (!is_elm) { + return this.getRoutes(route)[0]; + } + } + + getRoutes(route) { + let routes = []; + try { + document.querySelectorAll(route).forEach(elm => { + try { + let route = this.getRoute(elm); + if (route) { + routes.push(route); + } + } catch (ignored) {} + }); + } catch (ignored) {} + return routes; + } + + loadRoute(path) { + this.populateRoutes(); + this.timeouts.forEach(clearTimeout); + this.intervals.forEach(clearInterval); + if (path === undefined) { + path = new URL(location.href); + } else if (typeof path == "string" && path.substr(0, 1) == "/") { + path = new URL(location.protocol + location.hostname + path); + } + let route, + args = false, + urlextra = ""; + if (path instanceof URL) { + urlextra = path.search + path.hash; + path = decodeURIComponent(path.pathname); + } + for (let i = 0; i < this.routes.length; i++) { + if (this.routes[i] instanceof RegexRoute) { + args = this.routes[i].getArgs(path); + if (args !== false) { + route = this.routes[i]; + break; + } + } else if (this.routes[i].paths.indexOf(path) > -1) { + route = this.routes[i]; + break; + } + } + if (route === undefined) { + route = this.getRoute("404"); + if (route === null) { + route = this.routes[0]; + path = route.getCanonicalPath(); + } + } + if (path.substr(0, 1) != "/") { + path = route.getCanonicalPath(); + } + if (args === false) { + args = [path]; + } + route.fire("beforeload", args); + this.fire("route_beforeload", { + route: route, + args: args + }); + this.routes.forEach(r => { + if (r !== route) { + r.elm.classList.remove("route-current"); + if (!route.overlay || r.overlay) { + r.elm.classList.remove("route-visible"); + } + } + }); + route.elm.classList.add("route-current", "route-visible"); + path += urlextra; + if (this.getCurrentPath() != path) { + history.pushState({}, route.title, path); + } + document.querySelector("title").textContent = route.title; + this.fire("route_load", { + route: route, + args: args + }); + route.fire("load", args); + } + + getCurrentRoute() { + return this.getRoute(".route-current"); + } + + getCurrentPath() { + return location.pathname + location.search + location.hash; + } + + setTimeout(f, i) { + this.timeouts.push(window.setTimeout(f, i)); + } + + setInterval(f, i) { + this.intervals.push(window.setInterval(f, i)); + } + } + + console.assert(!("single" in window)); + window.single = new SingleApp(); + if (["interactive", "complete"].indexOf(document.readyState) > -1) { + window.single.loadRoute(); + } else { + document.addEventListener("DOMContentLoaded", () => window.single.loadRoute()); + } +})(); diff --git a/static/webui/style.css b/static/webui/style.css new file mode 100644 index 00000000..ac9bf2a1 --- /dev/null +++ b/static/webui/style.css @@ -0,0 +1,17 @@ +@media (min-width: 992px) { + body.logged-in #main-view { + display: grid; + grid-template-columns: 1fr 8fr; + gap: 1.5rem; + } + + body:not(.logged-in) #sidebar { + display: none; + } +} + +body:not(.logged-in) .navbar-toggler, +body:not(.logged-in) .nav-item.dropdown, +body:not(.logged-in) #refresh-note { + display: none; +}