feat(webui): add Mods tab with "Add Riven" option (#234)

This commit is contained in:
Sainan 2024-05-28 13:28:57 +02:00 committed by GitHub
parent 2e8c94d34b
commit 8eb11007a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 555 additions and 57 deletions

View File

@ -2,6 +2,7 @@ import express from "express";
import path from "path"; import path from "path";
const webuiRouter = express.Router(); const webuiRouter = express.Router();
const rootDir = path.join(__dirname, "../..");
// Redirect / to /webui/ // Redirect / to /webui/
webuiRouter.get("/", (_req, res) => { webuiRouter.get("/", (_req, res) => {
@ -16,7 +17,15 @@ webuiRouter.use("/webui", (req, res, next) => {
next(); next();
}); });
// Serve static files from the webui directory // Serve virtual routes
webuiRouter.use("/webui", express.static(path.join(__dirname, "../..", "static/webui"))); 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 }; export { webuiRouter };

View File

@ -2,70 +2,142 @@
<html lang="en" data-bs-theme="dark"> <html lang="en" data-bs-theme="dark">
<head> <head>
<title>OpenWF WebUI</title> <title>OpenWF WebUI</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link <link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous" crossorigin="anonymous"
/> />
<link rel="stylesheet" href="style.css" />
</head> </head>
<body> <body>
<div class="container pt-3 pb-3"> <nav class="navbar sticky-top bg-body-tertiary">
<h1>OpenWF WebUI</h1> <div class="container">
<div id="login-view"> <button
<p>Login using your OpenWF account credentials.</p> class="navbar-toggler d-lg-none"
<form onsubmit="doLogin();return false;"> type="button"
<label for="email">Email address</label> data-bs-toggle="offcanvas"
<input class="form-control" type="email" id="email" required /> data-bs-target="#sidebar"
<br /> aria-controls="sidebar"
<label for="password">Password</label> aria-label="Toggle sidebar"
<input class="form-control" type="password" id="password" required /> >
<br /> <span class="navbar-toggler-icon"></span>
<button class="btn btn-primary" type="submit">Login</button> </button>
</form> <a class="navbar-brand">OpenWF WebUI</a>
<div class="nav-item dropdown">
<button
class="nav-link dropdown-toggle displayname"
data-bs-toggle="dropdown"
aria-expanded="false"
></button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="/webui/" onclick="logout();">Logout</a></li>
</ul>
</div>
</div> </div>
<div id="main-view" class="d-none"> </nav>
<p>Hello, <b class="displayname"></b>! <a href="#" onclick="logout();">Logout</a></p> <div class="container pt-3 pb-3" id="main-view">
<p class="mb-4"> <div class="offcanvas-lg offcanvas-start" tabindex="-1" id="sidebar" aria-labelledby="sidebarLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="sidebarLabel">Sidebar</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="offcanvas"
data-bs-target="#sidebar"
aria-label="Close"
></button>
</div>
<div class="offcanvas-body">
<ul>
<li><a href="/webui/inventory">Inventory</a></li>
<li><a href="/webui/mods">Mods</a></li>
</ul>
</div>
</div>
<div>
<p id="refresh-note" class="mb-4">
Note: Changes made here will only be reflected in-game when the game re-downloads your inventory. 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. Visiting the navigation should be the easiest way to trigger that.
</p> </p>
<div class="card mb-4"> <div data-route="/webui/">
<h5 class="card-header">Add Items</h5> <p>Login using your OpenWF account credentials.</p>
<form class="card-body input-group" onsubmit="doAcquireMiscItems();return false;"> <form onsubmit="doLogin();return false;">
<input class="form-control" id="miscitem-count" type="number" min="1" value="1" /> <label for="email">Email address</label>
<input class="form-control" id="miscitem-type" list="datalist-miscitems" /> <input class="form-control" type="email" id="email" required />
<button class="btn btn-primary" type="submit">Add</button> <br />
<label for="password">Password</label>
<input class="form-control" type="password" id="password" required />
<br />
<button class="btn btn-primary" type="submit">Login</button>
</form> </form>
</div> </div>
<div class="row"> <div data-route="/webui/inventory">
<div class="col-lg-6"> <div class="card mb-4">
<div class="card mb-4"> <h5 class="card-header">Add Items</h5>
<h5 class="card-header">Warframes</h5> <form class="card-body input-group" onsubmit="doAcquireMiscItems();return false;">
<div class="card-body"> <input class="form-control" id="miscitem-count" type="number" min="1" value="1" />
<table class="table table-striped w-100"> <input class="form-control" id="miscitem-type" list="datalist-miscitems" />
<tbody id="warframe-list"></tbody> <button class="btn btn-primary" type="submit">Add</button>
</table> </form>
<form class="input-group" onsubmit="doAcquireWarframe();return false;"> </div>
<input class="form-control" id="warframe-to-acquire" list="datalist-warframes" /> <div class="row">
<button class="btn btn-primary" type="submit">Add</button> <div class="col-lg-6">
</form> <div class="card mb-4">
<h5 class="card-header">Warframes</h5>
<div class="card-body">
<table class="table table-striped w-100">
<tbody id="warframe-list"></tbody>
</table>
<form class="input-group" onsubmit="doAcquireWarframe();return false;">
<input
class="form-control"
id="warframe-to-acquire"
list="datalist-warframes"
/>
<button class="btn btn-primary" type="submit">Add</button>
</form>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card mb-4">
<h5 class="card-header">Weapons</h5>
<div class="card-body">
<table class="table table-striped w-100">
<tbody id="weapon-list"></tbody>
</table>
<form class="input-group" onsubmit="doAcquireWeapon();return false;">
<input class="form-control" id="weapon-to-acquire" list="datalist-weapons" />
<button class="btn btn-primary" type="submit">Add</button>
</form>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-lg-6"> </div>
<div class="card mb-4"> <div data-route="/webui/mods">
<h5 class="card-header">Weapons</h5> <div class="card mb-4">
<div class="card-body"> <h5 class="card-header">Add Riven</h5>
<table class="table table-striped w-100"> <form class="card-body" onsubmit="doAcquireRiven();return false;">
<tbody id="weapon-list"></tbody> <select class="form-control mb-3" id="addriven-type">
</table> <option value="LotusArchgunRandomModRare">LotusArchgunRandomModRare</option>
<form class="input-group" onsubmit="doAcquireWeapon();return false;"> <option value="LotusModularMeleeRandomModRare">LotusModularMeleeRandomModRare</option>
<input class="form-control" id="weapon-to-acquire" list="datalist-weapons" /> <option value="LotusModularPistolRandomModRare">LotusModularPistolRandomModRare</option>
<button class="btn btn-primary" type="submit">Add</button> <option value="LotusPistolRandomModRare">LotusPistolRandomModRare</option>
</form> <option value="LotusRifleRandomModRare" selected>LotusRifleRandomModRare</option>
</div> <option value="LotusShotgunRandomModRare">LotusShotgunRandomModRare</option>
</div> <option value="PlayerMeleeWeaponRandomModRare">PlayerMeleeWeaponRandomModRare</option>
</select>
<textarea
id="addriven-fingerprint"
class="form-control mb-3"
placeholder="Fingerprint"
></textarea>
<button class="btn btn-primary" style="margin-right: 5px" type="submit">Add</button>
<a href="https://riven.builds.wf/" target="_blank">Need help with the fingerprint?</a>
</form>
</div> </div>
</div> </div>
</div> </div>
@ -79,6 +151,12 @@
crossorigin="anonymous" crossorigin="anonymous"
></script> ></script>
<script src="https://cdn.jsdelivr.net/gh/angeal185/whirlpool-js/dist/whirlpool-js.min.js"></script> <script src="https://cdn.jsdelivr.net/gh/angeal185/whirlpool-js/dist/whirlpool-js.min.js"></script>
<script src="single.js"></script>
<script src="script.js"></script> <script src="script.js"></script>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"
></script>
</body> </body>
</html> </html>

View File

@ -1,6 +1,7 @@
function doLogin() { function doLogin() {
localStorage.setItem("email", $("#email").val()); localStorage.setItem("email", $("#email").val());
localStorage.setItem("password", $("#password").val()); localStorage.setItem("password", $("#password").val());
$("#email, #password").val("");
loginFromLocalStorage(); loginFromLocalStorage();
} }
@ -20,10 +21,12 @@ function loginFromLocalStorage() {
}) })
}); });
req.done(data => { req.done(data => {
$("#login-view").addClass("d-none"); if (single.getCurrentPath() == "/webui/") {
$("#main-view").removeClass("d-none"); single.loadRoute("/webui/inventory");
}
$(".displayname").text(data.DisplayName); $(".displayname").text(data.DisplayName);
window.accountId = data.id; window.accountId = data.id;
window.authz = "accountId=" + data.id + "&nonce=" + data.Nonce;
updateInventory(); updateInventory();
}); });
req.fail(() => { req.fail(() => {
@ -35,14 +38,25 @@ function loginFromLocalStorage() {
function logout() { function logout() {
localStorage.removeItem("email"); localStorage.removeItem("email");
localStorage.removeItem("password"); localStorage.removeItem("password");
$("#login-view").removeClass("d-none");
$("#main-view").addClass("d-none");
} }
if (localStorage.getItem("email") && localStorage.getItem("password")) { if (localStorage.getItem("email") && localStorage.getItem("password")) {
loginFromLocalStorage(); 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 => { window.itemListPromise = new Promise(resolve => {
const req = $.get("/custom/getItemLists"); const req = $.get("/custom/getItemLists");
req.done(data => { req.done(data => {
@ -61,7 +75,7 @@ window.itemListPromise = new Promise(resolve => {
}); });
function updateInventory() { function updateInventory() {
const req = $.get("/api/inventory.php?accountId=" + window.accountId); const req = $.get("/api/inventory.php?" + window.authz);
req.done(data => { req.done(data => {
window.itemListPromise.then(itemMap => { window.itemListPromise.then(itemMap => {
document.getElementById("warframe-list").innerHTML = ""; document.getElementById("warframe-list").innerHTML = "";
@ -215,7 +229,7 @@ function addGearExp(category, oid, xp) {
} }
]; ];
$.post({ $.post({
url: "/api/missionInventoryUpdate.php?accountId=" + window.accountId, url: "/api/missionInventoryUpdate.php?" + window.authz,
contentType: "text/plain", contentType: "text/plain",
data: JSON.stringify(data) data: JSON.stringify(data)
}).done(function () { }).done(function () {
@ -235,7 +249,7 @@ function disposeOfGear(category, oid) {
} }
]; ];
$.post({ $.post({
url: "/api/sell.php?accountId=" + window.accountId, url: "/api/sell.php?" + window.authz,
contentType: "text/plain", contentType: "text/plain",
data: JSON.stringify(data) data: JSON.stringify(data)
}).done(function () { }).done(function () {
@ -250,7 +264,7 @@ function doAcquireMiscItems() {
return; return;
} }
$.post({ $.post({
url: "/api/missionInventoryUpdate.php?accountId=" + window.accountId, url: "/api/missionInventoryUpdate.php?" + window.authz,
contentType: "text/plain", contentType: "text/plain",
data: JSON.stringify({ data: JSON.stringify({
MiscItems: [ MiscItems: [
@ -268,3 +282,66 @@ function doAcquireMiscItems() {
$("#miscitem-name").on("input", () => { $("#miscitem-name").on("input", () => {
$("#miscitem-name").removeClass("is-invalid"); $("#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");
});

317
static/webui/single.js Normal file
View File

@ -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());
}
})();

17
static/webui/style.css Normal file
View File

@ -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;
}