aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/app')
-rw-r--r--src/app/browser.js264
-rw-r--r--src/app/icons/no-image.pngbin0 -> 1959 bytes
-rw-r--r--src/app/icons/viper.pngbin137107 -> 59028 bytes
-rw-r--r--src/app/index.html18
-rw-r--r--src/app/main.css298
-rw-r--r--src/app/main.js23
-rw-r--r--src/app/toast.js59
7 files changed, 627 insertions, 35 deletions
diff --git a/src/app/browser.js b/src/app/browser.js
new file mode 100644
index 0000000..ded12fa
--- /dev/null
+++ b/src/app/browser.js
@@ -0,0 +1,264 @@
+const Fuse = require("fuse.js");
+var fuse;
+var packages = [];
+
+var Browser = {
+ maxentries: 50,
+ toggle: (state) => {
+ if (state) {
+ browser.scrollTo(0, 0);
+ overlay.classList.add("shown")
+ browser.classList.add("shown")
+
+ if (browserEntries.querySelectorAll(".el").length == 0) {
+ Browser.loadfront();
+ }
+ return
+ } else if (! state) {
+ if (state != undefined) {
+ overlay.classList.remove("shown")
+ browser.classList.remove("shown")
+ return
+ }
+ }
+
+ browser.scrollTo(0, 0);
+ overlay.classList.toggle("shown")
+ browser.classList.toggle("shown")
+ },
+ loadfront: async () => {
+ Browser.loading();
+
+ if (packages.length < 1) {
+ packages = await (await fetch("https://northstar.thunderstore.io/api/v1/package/")).json();
+
+ fuse = new Fuse(packages, {
+ keys: ["full_name"]
+ })
+ }
+
+ for (let i in packages) {
+ if (i == Browser.maxentries) {Browser.endoflist();break}
+ new BrowserElFromObj(packages[i]);
+ }
+ },
+ loading: (string) => {
+ if (string) {
+ browserEntries.innerHTML = `<div class="loading">${string}</div>`;
+ }
+
+ if (! browserEntries.querySelector(".loading")) {
+ browserEntries.innerHTML = `<div class="loading">${lang('gui.browser.loading')}</div>`;
+ }
+ },
+ endoflist: () => {
+ browserEntries.innerHTML += `<div class="message">${lang('gui.browser.endoflist')}</div>`
+ },
+ search: (string) => {
+ Browser.loading();
+ let res = fuse.search(string);
+
+ if (res.length < 1) {
+ Browser.loading("No results...")
+ return
+ }
+
+ for (let i = 0; i < res.length; i++) {
+ if (i == Browser.maxentries) {Browser.endoflist();break}
+ new BrowserElFromObj(res[i].item);
+ }
+ },
+ setbutton: (mod, string) => {
+ mod = normalize(mod);
+ if (document.getElementById(mod)) {
+ let elems = document.querySelectorAll(`#${mod}`);
+
+ for (let i = 0; i < elems.length; i++) {
+ elems[i].querySelector(".text button").innerHTML = string;
+ }
+ } else {
+ let make = (str) => {
+ if (document.getElementById(str)) {
+ return Browser.setbutton(str, string);
+ } else {
+ return false;
+ }
+ }
+
+ setTimeout(() => {
+ for (let i = 0; i < modsobj.all.length; i++) {
+ let modname = normalize(modsobj.all[i].Name);
+ let modfolder = normalize(modsobj.all[i].FolderName);
+
+ if (mod.includes(modname)) {
+ if (! make(modname)) {
+ if (modsobj.all[i].ManifestName) {
+ make(normalize(modsobj.all[i].ManifestName));
+ }
+ }
+ }
+ else if (mod.includes(modfolder)) {make(modfolder);break}
+ }
+ }, 1501)
+ }
+ }
+}
+
+document.body.addEventListener("keyup", (e) => {
+ if (e.key == "Escape") {Browser.toggle(false)}
+})
+
+function BrowserElFromObj(obj) {
+ let pkg = {...obj, ...obj.versions[0]};
+
+ new BrowserEl({
+ title: pkg.name,
+ image: pkg.icon,
+ author: pkg.owner,
+ url: pkg.package_url,
+ download: pkg.download_url,
+ version: pkg.version_number,
+ description: pkg.description
+ })
+}
+
+function BrowserEl(properties) {
+ properties = {
+ title: "No name",
+ version: "1.0.0",
+ image: "icons/no-image.png",
+ author: "Unnamed Pilot",
+ description: "No description",
+ ...properties
+ }
+
+ if (properties.version[0] != "v") {
+ properties.version = "v" + properties.version;
+ }
+
+ if (browserEntries.querySelector(".loading")) {
+ browserEntries.innerHTML = "";
+ }
+
+ let installstr = lang("gui.browser.install");
+
+ if (normalize(modsdiv.innerText.split("\n")).includes(normalize(properties.title))) {
+ installstr = lang("gui.browser.reinstall");
+
+ for (let i = 0; i < modsobj.all.length; i++) {
+ if (normalize(modsobj.all[i].Name) == normalize(properties.title)
+ && "v" + modsobj.all[i].Version != properties.version) {
+
+ installstr = lang("gui.browser.update");
+ }
+ }
+ } else {
+ for (let i = 0; i < modsobj.all.length; i++) {
+ let title = normalize(properties.title);
+ let folder = normalize(modsobj.all[i].FolderName);
+ let manifestname = null;
+ if (modsobj.all[i].ManifestName) {
+ manifestname = normalize(modsobj.all[i].ManifestName);
+ }
+
+ if (title.includes(folder) || title.includes(manifestname)) {
+ installstr = lang("gui.browser.reinstall");
+
+ if (folder == title
+ && "v" + modsobj.all[i].Version != properties.version) {
+
+ installstr = lang("gui.browser.update");
+ }
+ }
+ }
+ }
+
+ browserEntries.innerHTML += `
+ <div class="el" id="${normalize(properties.title)}">
+ <div class="image">
+ <img src="${properties.image}">
+ </div>
+ <div class="text">
+ <div class="title">${properties.title}</div>
+ <div class="description">${properties.description}</div>
+ <button onclick="installFromURL('${properties.download}')">${installstr}</button>
+ <button onclick="require('electron').shell.openExternal('${properties.url}')">${lang('gui.browser.info')}</button>
+ <button class="visual">${properties.version}</button>
+ <button class="visual">${lang("gui.browser.madeby")} ${properties.author}</button>
+ </div>
+ </div>
+ `
+}
+
+ipcRenderer.on("removedmod", (event, mod) => {
+ setButtons(true);
+ Browser.setbutton(mod.name, lang("gui.browser.install"));
+ if (mod.manifestname) {
+ Browser.setbutton(mod.manifestname, lang("gui.browser.install"));
+ }
+})
+
+ipcRenderer.on("failedmod", (event, modname) => {
+ setButtons(true);
+ new Toast({
+ timeout: 10000,
+ scheme: "error",
+ title: lang("gui.toast.title.failed"),
+ description: lang("gui.toast.desc.failed")
+ })
+})
+
+ipcRenderer.on("installedmod", (event, mod) => {
+ setButtons(true);
+ Browser.setbutton(mod.name, lang("gui.browser.reinstall"));
+
+ if (mod.malformed) {
+ new Toast({
+ timeout: 8000,
+ scheme: "warning",
+ title: lang("gui.toast.title.malformed"),
+ description: mod.name + " " + lang("gui.toast.desc.malformed")
+ })
+ }
+
+ new Toast({
+ scheme: "success",
+ title: lang("gui.toast.title.installed"),
+ description: mod.name + " " + lang("gui.toast.desc.installed")
+ })
+})
+
+function normalize(items) {
+ let main = (string) => {
+ return string.replaceAll(" ", "").replaceAll(".", "").replaceAll("-", "").replaceAll("_", "").toLowerCase()
+ }
+ if (typeof items == "string") {
+ return main(items)
+ } else {
+ let newArray = [];
+ for (let i = 0; i < items.length; i++) {
+ newArray.push(main(items[i]));
+ }
+
+ return newArray;
+ }
+}
+
+let searchtimeout;
+let searchstr = "";
+search.addEventListener("keyup", () => {
+ clearTimeout(searchtimeout);
+
+ if (searchstr != search.value) {
+ if (search.value.replaceAll(" ", "") == "") {
+ searchstr = "";
+ Browser.loadfront();
+ return
+ }
+
+ searchtimeout = setTimeout(() => {
+ Browser.search(search.value);
+ searchstr = search.value;
+ }, 500)
+ }
+})
diff --git a/src/app/icons/no-image.png b/src/app/icons/no-image.png
new file mode 100644
index 0000000..43265d1
--- /dev/null
+++ b/src/app/icons/no-image.png
Binary files differ
diff --git a/src/app/icons/viper.png b/src/app/icons/viper.png
index 281f3dd..69b1cce 100644
--- a/src/app/icons/viper.png
+++ b/src/app/icons/viper.png
Binary files differ
diff --git a/src/app/index.html b/src/app/index.html
index fe383a0..8222416 100644
--- a/src/app/index.html
+++ b/src/app/index.html
@@ -7,11 +7,26 @@
</head>
<body>
<div id="bgHolder"></div>
+ <div id="toasts"></div>
<div id="winbtns">
<div id="minimize" onclick="ipcRenderer.send('minimize')"></div>
<div id="close" onclick="ipcRenderer.send('exit')"></div>
</div>
+
+
+ <div id="overlay" onclick="Browser.toggle(false)"></div>
+ <div id="browser">
+ <div id="misc">
+ <input id="search" placeholder="%%gui.browser.search%%">
+ <button id="close" onclick="Browser.toggle(false)">
+ <img src="icons/close.png">
+ </button>
+ </div>
+ <div id="browserEntries">
+ <div class="loading">%%gui.browser.loading%%</div>
+ </div>
+ </div>
<nav class="gamesContainer">
<button id="vpBtn" onclick="page(0)"></button>
@@ -79,6 +94,7 @@
<button id="togglemod" onclick="selected().toggle()">%%gui.mods.toggle%%</button>
<button id="toggleall" onclick="selected(true).toggle(true)">%%gui.mods.toggleall%%</button>
<button id="installmod" onclick="installmod()">%%gui.mods.install%%</button>
+ <button id="installmod" onclick="Browser.toggle(true)">%%gui.mods.find%%</button>
</div>
</div>
</div>
@@ -102,6 +118,8 @@
<script src="lang.js"></script>
<script src="main.js"></script>
+ <script src="toast.js"></script>
+ <script src="browser.js"></script>
<script src="launcher.js"></script>
</body>
</html>
diff --git a/src/app/main.css b/src/app/main.css
index de0db46..a532490 100644
--- a/src/app/main.css
+++ b/src/app/main.css
@@ -9,7 +9,11 @@
--selbg: rgba(80, 80, 80, 0.5);
--redbg: linear-gradient(45deg, var(--red), #FA4343);
--bluebg: linear-gradient(45deg, var(--blue), #7380ED);
+}
+#browser, #modsdiv {
+ outline: 1px solid #444444;
+ border: 3px solid var(--bg);
}
::-webkit-scrollbar {
@@ -26,12 +30,191 @@
background: var(--red);
}
+::selection {
+ color: black;
+ background: var(--red);
+}
+
+body {
+ margin: 0;
+ overflow: hidden;
+ user-select: none;
+}
+
+body, button, input {font-family: "Roboto", sans-serif}
+
+button {outline: none}
+b, strong {font-weight: 700}
+body, input, button {font-weight: 500}
+
+button {
+ border: none;
+ color: white;
+ outline: none;
+ cursor: pointer;
+ font-weight: 700;
+ padding: 5px 10px;
+ border-radius: 5px;
+ transition: 0.2s ease-in-out;
+}
+
.playBtn, .gamesContainer button, #winbtns div {
cursor: pointer;
}
-#winbtns {
+#browser {
+ --spacing: var(--padding);
+
+ z-index: 2;
+ opacity: 0.0;
+ position: fixed;
+ overflow-y: scroll;
+ top: var(--spacing);
+ pointer-events: none;
+ left: var(--spacing);
+ background: var(--bg);
+ right: var(--spacing);
+ bottom: var(--spacing);
+ transform: scale(0.98);
+ backdrop-filter: blur(15px);
+ border-radius: calc(var(--padding) / 3);
+ transition: opacity 0.15s ease-in-out, transform 0.15s ease-in-out;
+}
+
+#browser.shown {
+ opacity: 1.0;
+ pointer-events: all;
+ transform: scale(1.0);
+}
+
+#overlay {
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
z-index: 1;
+ opacity: 0.0;
+ position: fixed;
+ background: var(--bg);
+ pointer-events: none;
+ transition: opacity 0.15s ease-in-out;
+}
+
+#overlay.shown {
+ opacity: 0.8;
+ pointer-events: all;
+}
+
+@keyframes fadein {
+ 0% {opacity: 0.0}
+ 100% {opacity: 1.0}
+}
+
+#browser .el, #browser #misc, #browser .loading {
+ --spacing: calc(var(--padding) / 2);
+ --height: calc(var(--padding) * 3);
+ --mischeight: calc(var(--padding) * 1.5);
+
+ animation-duration: 0.15s;
+ animation-iteration-count: 1;
+ animation-name: fadein;
+ animation-fill-mode: forwards;
+ animation-timing-function: ease-in-out;
+
+ opacity: 0.0;
+ transition: 0.15s ease-in-out;
+}
+
+#browser .el, #browser #search, #browser #close {
+ color: white;
+ display: flex;
+ align-items: center;
+ height: var(--height);
+ margin: var(--spacing);
+ padding: var(--spacing);
+ background: var(--selbg);
+ border-radius: var(--spacing);
+ width: calc(100% - var(--spacing) * 4);
+}
+
+#browser #misc, #browser #search {
+ --height: var(--mischeight);
+}
+
+#browser #misc {
+ display: flex;
+}
+
+#browser #search {
+ border: none;
+ outline: none;
+ transition: filter 0.15s ease-in-out;
+ width: calc(100% - var(--spacing) * 2);
+}
+
+#browser #search:focus {
+ filter: brightness(1.5);
+}
+
+#browser #close {
+ --height: calc(var(--padding) * 1.5);
+
+ padding: 0px;
+ margin-left: 0px;
+ width: var(--height);
+}
+
+#browser #close img {
+ opacity: 0.6;
+ width: var(--height);
+ height: var(--height);
+ transform: scale(0.6);
+}
+
+#browser .loading {
+ width: 100%;
+ color: white;
+ display: flex;
+ text-align: center;
+ align-items: center;
+ justify-content: center;
+ height: calc(100% - var(--mischeight) - var(--height));
+}
+
+#browser .message {
+ color: white;
+ text-align: center;
+ margin: var(--padding);
+ width: calc(100% - var(--padding));
+}
+
+#browser .el .image, #browser .el .image img {
+ width: var(--height);
+ height: var(--height);
+ margin-right: var(--spacing);
+ border-radius: var(--spacing);
+}
+
+#browser .el .text {
+ overflow: hidden;
+}
+
+#browser .el .title, #browser .el .description {
+ height: 1.2em;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+#browser .el .title {font-size: 1.2em}
+#browser .el .description {font-size: 0.8em}
+#browser .el button {
+ background: var(--blue);
+ margin-top: var(--spacing);
+}
+
+#winbtns {
+ z-index: 2;
display: flex;
position: fixed;
top: var(--padding);
@@ -54,40 +237,18 @@
#winbtns div:hover {opacity: 1.0}
#winbtns div:active {transform: scale(0.98)}
-body {
- margin: 0;
- overflow: hidden;
- user-select: none;
- font-family: "Roboto", sans-serif;
-}
-
-button {outline: none}
-b, strong {font-weight: 700}
-body, input, button {font-weight: 500}
-
-button {
- border: none;
- color: white;
- outline: none;
- font-weight: 700;
- padding: 5px 10px;
- border-radius: 5px;
- transition: 0.2s ease-in-out;
-}
-
button:hover {filter: brightness(110%)}
button:active {filter: brightness(90%)}
img {pointer-events: none}
#bgHolder {
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- pointer-events: none;
- position: fixed;
- transform: scale(1.1);
+ top: -5px;
+ left: -5px;
+ right: -5px;
+ bottom: -5px;
+ z-index: -1;
+ position: absolute;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
@@ -162,7 +323,7 @@ img {pointer-events: none}
}
.contentContainer {
- width: 90%;
+ width: 85%;
color: white;
flex-grow: 1;
opacity: 1.0;
@@ -346,6 +507,12 @@ button:disabled {
pointer-events: none;
}
+button.visual {
+ opacity: 1.0;
+ pointer-events: none;
+ background: transparent !important;
+}
+
code {
font-size: 16px;
padding: 2px 5px;
@@ -385,16 +552,81 @@ code {
}
.modbtns button {
- margin-left: calc(var(--padding) / 3);
+ margin-left: var(--spacing);
+ --spacing: calc(var(--padding) / 3);
+ margin-top: calc(var(--spacing) / 2);
+ margin-bottom: calc(var(--spacing) / 2);
+}
+
+#toasts {
+ position: fixed;
+ z-index: 100000;
+ right: calc(var(--padding) * 1.5);
+ bottom: calc(var(--padding) * 1.5);
+}
+
+@keyframes bodyfadeaway {
+ 0% {opacity: 0.0; transform: scale(0.95)}
+ 100% {opacity: 1.0; transform: scale(1.0)}
+}
+
+#toasts .toast {
+ width: 300px;
+ opacity: 0.0;
+ cursor: pointer;
+ overflow: hidden;
+ max-height: 100vh;
+ background: #FFFFFF;
+ transform: scale(0.95);
+ transition: 0.2s ease-in-out;
+ padding: calc(var(--padding) / 2);
+ margin-top: calc(var(--padding) / 2);
+ border-radius: calc(var(--padding) / 2.5);
+ box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.2);
+
+ animation-duration: 0.2s;
+ animation-iteration-count: 1;
+ animation-name: bodyfadeaway;
+ animation-fill-mode: forwards;
+ animation-timing-function: ease-in-out;
+}
+
+#toasts .toast.hidden {
+ margin-top: 0px;
+ max-height: 0px;
+ padding-top: 0px;
+ padding-bottom: 0px;
+ filter: opacity(0.0);
+ transform: scale(0.95);
+}
+
+#toasts .toast:not(.hidden):hover {filter: opacity(0.9)}
+#toasts .toast:not(.hidden):active {filter: opacity(0.8)}
+
+.toast .title {
+}
+
+.toast .description {
+ opacity: 0.8;
+ font-size: 0.8em;
+ font-weight: 600;
}
/* drag control */
-#bgHolder {
+#bgHolder,
+.contentContainer,
+.gamesContainer {
user-select: none;
-webkit-app-region: drag;
}
-a, button, .contentMenu, #close, #nsRelease, #vpReleaseNotes, .mod {
+#overlay.shown ~ #bgHolder,
+#overlay.shown ~ .contentContainer,
+#overlay.shown ~ .gamesContainer {
+ -webkit-app-region: no-drag;
+}
+
+a, button, #close, #nsRelease, #vpReleaseNotes, .mod, #overlay, #modsdiv, #winbtns, .contentMenu {
-webkit-app-region: no-drag;
}
diff --git a/src/app/main.js b/src/app/main.js
index 5f0cc9a..169f86f 100644
--- a/src/app/main.js
+++ b/src/app/main.js
@@ -3,6 +3,7 @@ const path = require("path");
const { ipcRenderer, shell } = require("electron");
const lang = require("../lang");
+var modsobj = {};
let shouldInstallNorthstar = false;
// Base settings
@@ -26,13 +27,11 @@ if (fs.existsSync("viper.json")) {
settings.zip = path.join(settings.gamepath + "/northstar.zip");
if (settings.gamepath.length === 0) {
- alert(lang("general.missingpath"));
setpath(false);
} else {
setpath(true);
}
} else {
- alert(lang("general.missingpath"));
setpath();
}
@@ -68,6 +67,15 @@ function log(msg) {
// updating/installing Northstar.
function setButtons(state) {
playNsBtn.disabled = !state;
+
+ let disablearray = (array) => {
+ for (let i = 0; i < array.length; i++) {
+ array[i].disabled = !state;
+ }
+ }
+
+ disablearray(document.querySelectorAll("#nsMods .buttons.modbtns button"))
+ disablearray(document.querySelectorAll("#browser #browserEntries .text button"))
}
// Frontend part of updating Northstar
@@ -150,12 +158,20 @@ function selected(all) {
// Tells the main process to install a mod
function installmod() {
+ setButtons(false);
ipcRenderer.send("installmod")
}
+// Tells the main process to install a mod from a URL
+function installFromURL(url) {
+ setButtons(false);
+ ipcRenderer.send("installfromurl", url)
+}
+
// Frontend part of settings a new game path
ipcRenderer.on("newpath", (event, newpath) => {
settings.gamepath = newpath;
+ ipcRenderer.send("guigetmods");
})
// Continuation of log()
@@ -164,6 +180,9 @@ ipcRenderer.on("alert", (event, msg) => {alert(msg)})
// Updates the installed mods
ipcRenderer.on("mods", (event, mods) => {
+ modsobj = mods;
+ if (! mods) {return}
+
modcount.innerHTML = `${lang("gui.mods.count")} ${mods.all.length}`;
modsdiv.innerHTML = "";
diff --git a/src/app/toast.js b/src/app/toast.js
new file mode 100644
index 0000000..2a8555e
--- /dev/null
+++ b/src/app/toast.js
@@ -0,0 +1,59 @@
+function Toast(properties) {
+ let toast = {
+ fg: "#000000",
+ bg: "#FFFFFF",
+ timeout: 3000,
+ title: "Untitled Toast",
+ description: "No description provided for toast",
+ ...properties
+ }
+
+ switch(toast.scheme) {
+ case "error":
+ toast.fg = "#FFFFFF";
+ toast.bg = "var(--red)";
+ break
+ case "success":
+ toast.fg = "#FFFFFF";
+ toast.bg = "#60D394";
+ break
+ case "warning":
+ toast.fg = "#FFFFFF";
+ toast.bg = "#FF9B85";
+ break
+ }
+
+
+ let id = Date.now();
+ if (document.getElementById(id)) {id = id + 1}
+ let el = document.createElement("div");
+
+ el.classList.add("toast");
+
+ el.style.color = toast.fg;
+ el.style.background = toast.bg;
+
+ el.id = id;
+ el.setAttribute("onclick", `dismissToast(${id})`);
+
+ el.innerHTML = `
+ <div class="title">${toast.title}</div>
+ <div class="description">${toast.description}</div>
+ `
+
+ toasts.appendChild(el);
+
+ setTimeout(() => {
+ dismissToast(id);
+ }, toast.timeout)
+}
+
+function dismissToast(id) {
+ id = document.getElementById(id);
+ if (id) {
+ id.classList.add("hidden");
+ setTimeout(() => {
+ id.remove();
+ }, 500)
+ }
+}