feat(webui): add Mods tab with "Add Riven" option (#234)
This commit is contained in:
parent
2e8c94d34b
commit
8eb11007a7
@ -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 };
|
||||||
|
@ -2,17 +2,66 @@
|
|||||||
<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
|
||||||
|
class="navbar-toggler d-lg-none"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="offcanvas"
|
||||||
|
data-bs-target="#sidebar"
|
||||||
|
aria-controls="sidebar"
|
||||||
|
aria-label="Toggle sidebar"
|
||||||
|
>
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<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>
|
||||||
|
</nav>
|
||||||
|
<div class="container pt-3 pb-3" id="main-view">
|
||||||
|
<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.
|
||||||
|
Visiting the navigation should be the easiest way to trigger that.
|
||||||
|
</p>
|
||||||
|
<div data-route="/webui/">
|
||||||
<p>Login using your OpenWF account credentials.</p>
|
<p>Login using your OpenWF account credentials.</p>
|
||||||
<form onsubmit="doLogin();return false;">
|
<form onsubmit="doLogin();return false;">
|
||||||
<label for="email">Email address</label>
|
<label for="email">Email address</label>
|
||||||
@ -24,12 +73,7 @@
|
|||||||
<button class="btn btn-primary" type="submit">Login</button>
|
<button class="btn btn-primary" type="submit">Login</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="main-view" class="d-none">
|
<div data-route="/webui/inventory">
|
||||||
<p>Hello, <b class="displayname"></b>! <a href="#" onclick="logout();">Logout</a></p>
|
|
||||||
<p class="mb-4">
|
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<h5 class="card-header">Add Items</h5>
|
<h5 class="card-header">Add Items</h5>
|
||||||
<form class="card-body input-group" onsubmit="doAcquireMiscItems();return false;">
|
<form class="card-body input-group" onsubmit="doAcquireMiscItems();return false;">
|
||||||
@ -47,7 +91,11 @@
|
|||||||
<tbody id="warframe-list"></tbody>
|
<tbody id="warframe-list"></tbody>
|
||||||
</table>
|
</table>
|
||||||
<form class="input-group" onsubmit="doAcquireWarframe();return false;">
|
<form class="input-group" onsubmit="doAcquireWarframe();return false;">
|
||||||
<input class="form-control" id="warframe-to-acquire" list="datalist-warframes" />
|
<input
|
||||||
|
class="form-control"
|
||||||
|
id="warframe-to-acquire"
|
||||||
|
list="datalist-warframes"
|
||||||
|
/>
|
||||||
<button class="btn btn-primary" type="submit">Add</button>
|
<button class="btn btn-primary" type="submit">Add</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -69,6 +117,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div data-route="/webui/mods">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<h5 class="card-header">Add Riven</h5>
|
||||||
|
<form class="card-body" onsubmit="doAcquireRiven();return false;">
|
||||||
|
<select class="form-control mb-3" id="addriven-type">
|
||||||
|
<option value="LotusArchgunRandomModRare">LotusArchgunRandomModRare</option>
|
||||||
|
<option value="LotusModularMeleeRandomModRare">LotusModularMeleeRandomModRare</option>
|
||||||
|
<option value="LotusModularPistolRandomModRare">LotusModularPistolRandomModRare</option>
|
||||||
|
<option value="LotusPistolRandomModRare">LotusPistolRandomModRare</option>
|
||||||
|
<option value="LotusRifleRandomModRare" selected>LotusRifleRandomModRare</option>
|
||||||
|
<option value="LotusShotgunRandomModRare">LotusShotgunRandomModRare</option>
|
||||||
|
<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>
|
||||||
<datalist id="datalist-warframes"></datalist>
|
<datalist id="datalist-warframes"></datalist>
|
||||||
<datalist id="datalist-weapons"></datalist>
|
<datalist id="datalist-weapons"></datalist>
|
||||||
@ -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>
|
||||||
|
@ -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
317
static/webui/single.js
Normal 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
17
static/webui/style.css
Normal 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;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user