aboutsummaryrefslogtreecommitdiff
path: root/src/app/js
diff options
context:
space:
mode:
author0neGal <mail@0negal.com>2023-01-29 22:22:22 +0100
committer0neGal <mail@0negal.com>2023-01-29 22:22:22 +0100
commit052d121b06021729274ee7ac68b3d306c968687d (patch)
tree352b491b746555171a8bb7ce5e880b2d39c38b15 /src/app/js
parent64aad4e22abd95b67ee657723e31257419bed662 (diff)
parent4b94ba7ae03317e1dee75d637f7355f49c1f2086 (diff)
downloadViper-052d121b06021729274ee7ac68b3d306c968687d.tar.gz
Viper-052d121b06021729274ee7ac68b3d306c968687d.zip
Merge branch 'main' into linux-launch
Diffstat (limited to 'src/app/js')
-rw-r--r--src/app/js/browser.js554
-rw-r--r--src/app/js/launcher.js167
-rw-r--r--src/app/js/misc.js44
-rw-r--r--src/app/js/mods.js81
-rw-r--r--src/app/js/settings.js144
-rw-r--r--src/app/js/toast.js63
6 files changed, 1053 insertions, 0 deletions
diff --git a/src/app/js/browser.js b/src/app/js/browser.js
new file mode 100644
index 0000000..fcb79a2
--- /dev/null
+++ b/src/app/js/browser.js
@@ -0,0 +1,554 @@
+const Fuse = require("fuse.js");
+var fuse;
+var packages = [];
+
+var packagecount = 0;
+
+var Browser = {
+ maxentries: 50,
+ filters: {
+ getpkgs: () => {
+ let pkgs = [];
+ let other = [];
+ for (let i in packages) {
+ if (! Browser.filters.isfiltered(packages[i].categories)) {
+ pkgs.push(packages[i]);
+ } else {
+ other.push(packages[i]);
+ }
+ }
+
+ return pkgs;
+ },
+ get: () => {
+ let filtered = [];
+ let unfiltered = [];
+ let checks = browser.querySelectorAll("#filters .check");
+
+ for (let i = 0; i < checks.length; i++) {
+ if (! checks[i].classList.contains("checked")) {
+ filtered.push(checks[i].getAttribute("value"));
+ } else {
+ unfiltered.push(checks[i].getAttribute("value"));
+ }
+ }
+
+ return {
+ filtered,
+ unfiltered
+ };
+ },
+ isfiltered: (categories) => {
+ let filtered = Browser.filters.get().filtered;
+ let unfiltered = Browser.filters.get().unfiltered;
+ let state = false;
+
+ let filters = [
+ "Mods", "Skins",
+ "Client-side", "Server-side",
+ ];
+
+ let newcategories = [];
+ for (let i = 0; i < categories.length; i++) {
+ if (filters.includes(categories[i])) {
+ newcategories.push(categories[i]);
+ }
+ }; categories = newcategories;
+
+ if (categories.length == 0) {return true}
+ for (let i = 0; i < categories.length; i++) {
+ if (filtered.includes(categories[i])) {
+ state = true;
+ continue
+ } else if (unfiltered.includes(categories[i])) {
+ state = false;
+ continue
+ }
+
+ state = true;
+ }
+
+ return state;
+ },
+ toggle: (state) => {
+ if (state == false) {
+ filters.classList.remove("shown");
+ return
+ }
+
+ filters.classList.toggle("shown");
+ let filterRect = filter.getBoundingClientRect();
+ let spacing = parseInt(getComputedStyle(filters).getPropertyValue("--spacing"));
+
+ filters.style.top = filterRect.bottom - spacing;
+ filters.style.right = filterRect.right - filterRect.left + filterRect.width - (spacing / 2);
+ },
+ },
+ 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) {
+ Browser.filters.toggle(false);
+ overlay.classList.remove("shown");
+ browser.classList.remove("shown");
+ preview.classList.remove("shown");
+ return
+ }
+ }
+
+ browser.scrollTo(0, 0);
+ overlay.classList.toggle("shown");
+ browser.classList.toggle("shown");
+ },
+ loadfront: async () => {
+ Browser.loading();
+
+ packagecount = 0;
+
+ if (packages.length < 1) {
+ packages = await (await fetch("https://northstar.thunderstore.io/api/v1/package/")).json();
+
+ fuse = new Fuse(packages, {
+ keys: ["full_name"]
+ })
+ }
+
+ let pkgs = Browser.filters.getpkgs();
+ for (let i in pkgs) {
+ if (packagecount >= Browser.maxentries) {
+ Browser.endoflist();
+ break
+ }
+
+ new BrowserElFromObj(pkgs[i]);
+ packagecount++;
+ }
+ },
+ loading: (string) => {
+ if (Browser.filters.get().unfiltered.length == 0) {
+ string = lang("gui.browser.noresults");
+ }
+
+ if (string) {
+ browserEntries.innerHTML = `<div class="loading">${string}</div>`;
+ }
+
+ if (! browserEntries.querySelector(".loading")) {
+ browserEntries.innerHTML = `<div class="loading">${lang('gui.browser.loading')}</div>`;
+ }
+ },
+ endoflist: (isEnd) => {
+ let pkgs = [];
+ let filtered = Browser.filters.getpkgs();
+ for (let i = 0; i < filtered.length; i++) {
+ if ([packagecount + i]) {
+ pkgs.push(filtered[packagecount + i]);
+ } else {
+ break
+ }
+ }
+
+ if (browserEntries.querySelector(".message")) {
+ browserEntries.querySelector(".message").remove();
+ }
+
+ if (pkgs.length == 0 || isEnd) {
+ Browser.msg(`${lang('gui.browser.endoflist')}`);
+ return
+ }
+
+ Browser.msg(`<button id="loadmore">${lang("gui.browser.loadmore")}</button>`);
+ loadmore.addEventListener("click", () => {
+ Browser.loadpkgs(pkgs);
+ Browser.endoflist(pkgs);
+ })
+ },
+ search: (string) => {
+ Browser.loading();
+ let res = fuse.search(string);
+
+ if (res.length < 1) {
+ Browser.loading(lang("gui.browser.noresults"));
+ return
+ }
+
+ packagecount = 0;
+
+ let count = 0;
+ for (let i = 0; i < res.length; i++) {
+ if (count >= Browser.maxentries) {break}
+ if (Browser.filters.isfiltered(res[i].item.categories)) {continue}
+ new BrowserElFromObj(res[i].item);
+ count++;
+ }
+
+ if (count < 1) {
+ Browser.loading(lang("gui.browser.noresults"));
+ }
+ },
+ setbutton: (mod, string) => {
+ mod = normalize(mod);
+ if (browserEntries.querySelector(`#mod-${mod}`)) {
+ let elems = browserEntries.querySelectorAll(`.el#mod-${mod}`);
+
+ for (let i = 0; i < elems.length; i++) {
+ elems[i].querySelector(".text button").innerHTML = string;
+ }
+ } else {
+ let make = (str) => {
+ if (browserEntries.querySelector(`#mod-${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)
+ }
+ },
+ loadpkgs: (pkgs, clear) => {
+ if (clear) {packagecount = 0}
+
+ if (browserEntries.querySelector(".message")) {
+ browserEntries.querySelector(".message").remove();
+ }
+
+ let count = 0;
+ for (let i in pkgs) {
+ if (count >= Browser.maxentries) {
+ if (pkgs[i] === undefined) {
+ Browser.endoflist(true);
+ }
+
+ Browser.endoflist();
+ break
+ }
+
+ try {
+ new BrowserElFromObj(pkgs[i]);
+ }catch(e) {}
+
+ count++;
+ packagecount++;
+ }
+ },
+ msg: (html) => {
+ let msg = document.createElement("div");
+ msg.classList.add("message");
+ msg.innerHTML = html;
+
+ browserEntries.appendChild(msg);
+ }
+}
+
+function openExternal(url) {
+ require("electron").shell.openExternal(url);
+}
+
+var view = document.querySelector(".popup#preview webview");
+var Preview = {
+ show: () => {
+ preview.classList.add("shown");
+ },
+ hide: () => {
+ preview.classList.remove("shown");
+ },
+ set: (url, autoshow) => {
+ if (autoshow != false) {Preview.show()}
+ view.src = url;
+ document.querySelector("#preview #external").setAttribute("onclick", `openExternal("${url}")`);
+ }
+}
+
+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,
+ categories: pkg.categories,
+ description: pkg.description,
+ dependencies: pkg.dependencies,
+ })
+}
+
+function BrowserEl(properties) {
+ if (Browser.filters.isfiltered(properties.categories)) {return}
+
+ 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");
+ let normalized_mods = [];
+
+ for (let i = 0; i < modsobj.all; i++) {
+ normalized_mods.push(normalize(mods_list[i].Name));
+ }
+
+ if (normalized_mods.includes(normalize(properties.title))) {
+ installstr = lang("gui.browser.reinstall");
+
+ if (version.is_newer(properties.version, modsobj.all[i].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 (version.is_newer(properties.version, modsobj.all[i].Version)) {
+ installstr = lang("gui.browser.update");
+ }
+ }
+ }
+ }
+
+ let entry = document.createElement("div");
+ entry.classList.add("el");
+ entry.id = `mod-${normalize(properties.title)}`;
+
+ entry.innerHTML = `
+ <div class="image">
+ <img src="${properties.image}">
+ <img class="blur" src="${properties.image}">
+ </div>
+ <div class="text">
+ <div class="title">${properties.title}</div>
+ <div class="description">${properties.description}</div>
+ <button class="install" onclick=''>${installstr}</button>
+ <button class="info" onclick="Preview.set('${properties.url}')">${lang('gui.browser.view')}</button>
+ <button class="visual">${properties.version}</button>
+ <button class="visual">${lang("gui.browser.madeby")} ${properties.author}</button>
+ </div>
+ `
+
+ entry.querySelector("button.install").addEventListener("click", () => {
+ installFromURL(
+ properties.download,
+ JSON.stringify(properties.dependencies),
+ true, properties.author
+ )
+ })
+
+ browserEntries.appendChild(entry);
+}
+
+let recent_toasts = {};
+function add_recent_toast(name, timeout = 3000) {
+ if (recent_toasts[name]) {return}
+
+ recent_toasts[name] = true;
+
+ setTimeout(() => {
+ delete recent_toasts[name];
+ }, timeout)
+}
+
+ipcRenderer.on("removed-mod", (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("failed-mod", (event, modname) => {
+ if (recent_toasts["failed" + modname]) {return}
+ add_recent_toast("failed" + modname);
+
+ setButtons(true);
+ new Toast({
+ timeout: 10000,
+ scheme: "error",
+ title: lang("gui.toast.title.failed"),
+ description: lang("gui.toast.desc.failed")
+ })
+})
+
+ipcRenderer.on("duped-mod", (event, modname) => {
+ if (recent_toasts["duped" + modname]) {return}
+ add_recent_toast("duped" + modname);
+
+ setButtons(true);
+ new Toast({
+ timeout: 10000,
+ scheme: "warning",
+ title: lang("gui.toast.title.duped"),
+ description: modname + " " + lang("gui.toast.desc.duped")
+ })
+})
+
+ipcRenderer.on("no-internet", (event, modname) => {
+ setButtons(true);
+ new Toast({
+ timeout: 10000,
+ scheme: "error",
+ title: lang("gui.toast.noInternet.title"),
+ description: lang("gui.toast.noInternet.desc")
+ })
+})
+
+ipcRenderer.on("installed-mod", (event, mod) => {
+ if (recent_toasts["installed" + mod.name]) {return}
+ add_recent_toast("installed" + mod.name);
+
+ 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")
+ })
+
+ if (installqueue.length != 0) {
+ installFromURL(
+ "https://thunderstore.io/package/download/" + installqueue[0].pkg,
+ false, false, installqueue[0].author
+ )
+
+ installqueue.shift();
+ }
+})
+
+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", () => {
+ Browser.filters.toggle(false);
+ clearTimeout(searchtimeout);
+
+ if (searchstr != search.value) {
+ if (search.value.replaceAll(" ", "") == "") {
+ searchstr = "";
+ Browser.loadfront();
+ return
+ }
+
+ searchtimeout = setTimeout(() => {
+ Browser.search(search.value);
+ searchstr = search.value;
+ }, 500)
+ }
+})
+
+let events = ["scroll", "mousedown", "touchdown"];
+events.forEach((event) => {
+ browser.addEventListener(event, () => {
+ Preview.hide();
+
+ let mouseAt = document.elementsFromPoint(mouseX, mouseY);
+ if (! mouseAt.includes(document.querySelector("#filter"))
+ && ! mouseAt.includes(document.querySelector(".overlay"))) {
+ Browser.filters.toggle(false);
+ }
+ })
+});
+
+view.addEventListener("dom-ready", () => {
+ let css = [
+ fs.readFileSync(__dirname + "/css/theming.css", "utf8"),
+ fs.readFileSync(__dirname + "/css/webview.css", "utf8")
+ ]
+
+ view.insertCSS(css.join(" "));
+})
+
+view.addEventListener("did-stop-loading", () => {
+ view.style.display = "flex";
+ setTimeout(() => {
+ view.classList.remove("loading");
+ }, 200)
+})
+
+view.addEventListener("did-start-loading", () => {
+ view.style.display = "none";
+ view.classList.add("loading");
+})
+
+let mouseY = 0;
+let mouseX = 0;
+browser.addEventListener("mousemove", (event) => {
+ mouseY = event.clientY;
+ mouseX = event.clientX;
+})
+
+let checks = document.querySelectorAll(".check");
+for (let i = 0; i < checks.length; i++) {
+ checks[i].setAttribute("onclick",
+ "this.classList.toggle('checked');Browser.loadfront();search.value = ''"
+ )
+}
diff --git a/src/app/js/launcher.js b/src/app/js/launcher.js
new file mode 100644
index 0000000..5330b7a
--- /dev/null
+++ b/src/app/js/launcher.js
@@ -0,0 +1,167 @@
+const markdown = require("marked").parse;
+
+var servercount;
+var playercount;
+var masterserver;
+
+// Changes the main page
+// This is the tabs in the sidebar
+function page(page) {
+ let btns = document.querySelectorAll(".gamesContainer button");
+ let pages = document.querySelectorAll(".mainContainer .contentContainer");
+
+ for (let i = 0; i < pages.length; i++) {
+ pages[i].classList.add("hidden");
+ }
+
+ for (let i = 0; i < btns.length; i++) {
+ btns[i].classList.add("inactive");
+ }
+
+ pages[page].classList.remove("hidden");
+ btns[page].classList.remove("inactive");
+ bgHolder.setAttribute("bg", page);
+}; page(1)
+
+function formatRelease(notes) {
+ let content = "";
+
+ if (notes.length === 1) {
+ content = notes[0];
+ } else {
+ for (let release of notes) {
+ if (release.prerelease) {continue}
+ let new_content = "# " + release.name + "\n\n" + release.body + "\n\n\n";
+ content +=
+ "<div class='release-block'>\n"
+ + markdown(new_content, {breaks: true}) + "\n" +
+ "</div>";
+ }
+
+ content = content.replaceAll(/\@(\S+)/g, `<a href="https://github.com/$1">@$1</a>`);
+ }
+
+ return markdown(content, {
+ breaks: true
+ });
+}
+
+// Updates the Viper release notes
+ipcRenderer.on("vp-notes", (event, response) => {
+ vpReleaseNotes.innerHTML = formatRelease(response);
+});
+
+// Updates the Northstar release notes
+ipcRenderer.on("ns-notes", (event, response) => {
+ nsRelease.innerHTML = formatRelease(response);
+});
+
+async function loadVpReleases() {
+ ipcRenderer.send("get-vp-notes");
+}; loadVpReleases();
+
+async function loadNsReleases() {
+ ipcRenderer.send("get-ns-notes");
+}; loadNsReleases();
+
+// TODO: We gotta make this more automatic instead of switch statements
+// it's both not pretty, but adding more sections requires way too much
+// effort, compared to how it should be.
+function showVpSection(section) {
+ if (!["main", "release", "info", "credits"].includes(section)) throw new Error("unknown vp section");
+ vpMainBtn.removeAttribute("active");
+ vpReleaseBtn.removeAttribute("active");
+ vpInfoBtn.removeAttribute("active");
+
+ vpMain.classList.add("hidden");
+ vpReleaseNotes.classList.add("hidden");
+ vpInfo.classList.add("hidden");
+
+ switch(section) {
+ case "main":
+ vpMainBtn.setAttribute("active", "");
+ vpMain.classList.remove("hidden");
+ break;
+ case "release":
+ vpReleaseBtn.setAttribute("active", "");
+ vpReleaseNotes.classList.remove("hidden");
+ break;
+ case "info":
+ vpInfoBtn.setAttribute("active", "");
+ vpInfo.classList.remove("hidden");
+ break;
+ }
+}
+
+function showNsSection(section) {
+ if (!["main", "release", "mods"].includes(section)) {
+ throw new Error("unknown ns section");
+ }
+
+ nsMainBtn.removeAttribute("active");
+ nsModsBtn.removeAttribute("active");
+ nsReleaseBtn.removeAttribute("active");
+
+ nsMain.classList.add("hidden");
+ nsMods.classList.add("hidden");
+ nsRelease.classList.add("hidden");
+
+ switch(section) {
+ case "main":
+ nsMainBtn.setAttribute("active", "");
+ nsMain.classList.remove("hidden");
+ break;
+ case "mods":
+ nsModsBtn.setAttribute("active", "");
+ nsMods.style.display = "block";
+ nsMods.classList.remove("hidden");
+ break;
+ case "release":
+ nsReleaseBtn.setAttribute("active", "");
+ nsRelease.classList.remove("hidden");
+ break;
+ }
+}
+
+async function loadServers() {
+ serverstatus.classList.add("checking");
+
+ try {
+ let servers = await (await fetch("https://northstar.tf/client/servers")).json();
+ masterserver = true;
+
+ playercount = 0;
+ servercount = servers.length;
+
+ for (let i = 0; i < servers.length; i++) {
+ playercount += servers[i].playerCount
+ }
+ }catch (err) {
+ playercount = 0;
+ servercount = 0;
+ masterserver = false;
+ }
+
+ serverstatus.classList.remove("checking");
+
+ if (servercount == 0 || ! servercount || ! playercount) {masterserver = false}
+
+ let playerstr = lang("gui.server.players");
+ if (playercount == 1) {
+ playerstr = lang("gui.server.player");
+ }
+
+ if (masterserver) {
+ serverstatus.classList.add("up");
+ serverstatus.innerHTML = `${servercount} ${lang("gui.server.servers")} - ${playercount} ${playerstr}`;
+ } else {
+ serverstatus.classList.add("down");
+ serverstatus.innerHTML = lang("gui.server.offline");
+
+ }
+}; loadServers()
+
+// Refreshes every 5 minutes
+setInterval(() => {
+ loadServers();
+}, 300000)
diff --git a/src/app/js/misc.js b/src/app/js/misc.js
new file mode 100644
index 0000000..b35f239
--- /dev/null
+++ b/src/app/js/misc.js
@@ -0,0 +1,44 @@
+version = {
+ is_newer: (version1, version2) => {
+ version1 = version.format(version1, true).split(".");
+ version2 = version.format(version2, true).split(".");
+
+ for (let i = 0; i < version1.length; i++) {
+
+
+ let nums = [
+ parseInt(version1[i]) || 0,
+ parseInt(version2[i]) || 0
+ ];
+ if (nums[0] > nums[1]) {
+ return true;
+ } else if (nums[0] < nums[1]) {
+ return false;
+ }
+ }
+
+ return false;
+ },
+ format: (version_number, no_leading_v) => {
+ version_number = version_number.trim();
+
+ if (no_leading_v) {
+ if (version_number[0] == "v") {
+ return version_number.slice(1, version_number.length);
+ }
+
+ return version_number;
+ if (no_leading_v) {
+ return version_number
+ }
+
+ return "v" + version_number;
+ } else {
+ if (version_number[0] != "v") {
+ return "v" + version_number;
+ }
+ }
+
+ return version_number;
+ }
+}
diff --git a/src/app/js/mods.js b/src/app/js/mods.js
new file mode 100644
index 0000000..39e904e
--- /dev/null
+++ b/src/app/js/mods.js
@@ -0,0 +1,81 @@
+var mods = {};
+
+mods.load = (mods_obj) => {
+ modcount.innerHTML = `${lang("gui.mods.count")} ${mods_obj.all.length}`;
+
+ let normalized_names = [];
+
+ let set_mod = (mod) => {
+ let image_url = "";
+ let normalized_name = "mod-list-" + normalize(mod.Name);
+
+ normalized_names.push(normalized_name);
+
+ if (document.getElementById(normalized_name)) {
+ return;
+ }
+
+ let div = document.createElement("div");
+ div.classList.add("el");
+ div.id = normalized_name;
+
+ div.innerHTML += `
+ <div class="image">
+ <img src="${image_url}">
+ <img class="blur" src="${image_url}">
+ </div>
+ <div class="text">
+ <div class="title">${mod.Name}</div>
+ <div class="description">${mod.Description}</div>
+ <button class="red" onclick="mods.remove('${mod.Name}')">Remove</button>
+ <button class="visual">${mod.Version}</button>
+ <button class="visual">by ${mod.Author || "Unknown"}</button>
+ </div>
+ `;
+
+ if (! image_url) {
+ div.querySelector(".image").remove();
+ }
+
+ modsdiv.append(div);
+ }
+
+ for (let i = 0; i < mods_obj.all.length; i++) {
+ set_mod(mods_obj.all[i]);
+ }
+
+ let mod_els = document.querySelectorAll("#modsdiv .el");
+ for (let i = 0; i < mod_els.length; i++) {
+ if (! normalized_names.includes(mod_els[i].id)) {
+ mod_els[i].remove();
+ }
+ }
+}
+
+mods.remove = (mod) => {
+ if (mod.match(/^northstar\./)) {
+ if (! confirm(lang("gui.mods.required.confirm"))) {
+ return;
+ }
+ } else if (mod == "allmods") {
+ if (! confirm(lang("gui.mods.removeall.confirm"))) {
+ return;
+ }
+ }
+
+ ipcRenderer.send("remove-mod", mod);
+}
+
+mods.toggle = (mod) => {
+ if (mod.match(/^Northstar\./)) {
+ if (! confirm(lang("gui.mods.required.confirm"))) {
+ return;
+ }
+ } else if (mod == "allmods") {
+ if (! confirm(lang("gui.mods.toggleall.confirm"))) {
+ return;
+ }
+ }
+
+ ipcRenderer.send("toggle-mod", mod);
+}
diff --git a/src/app/js/settings.js b/src/app/js/settings.js
new file mode 100644
index 0000000..34975fe
--- /dev/null
+++ b/src/app/js/settings.js
@@ -0,0 +1,144 @@
+var Settings = {
+ toggle: (state) => {
+ if (state) {
+ Settings.load();
+ options.scrollTo(0, 0);
+ overlay.classList.add("shown");
+ options.classList.add("shown");
+
+ return
+ } else if (! state) {
+ if (state != undefined) {
+ overlay.classList.remove("shown");
+ options.classList.remove("shown");
+ return
+ }
+ }
+
+ Settings.load();
+ options.scrollTo(0, 0);
+ overlay.classList.toggle("shown");
+ options.classList.toggle("shown");
+ },
+ apply: () => {
+ settings = {...settings, ...Settings.get()};
+ ipcRenderer.send("save-settings", Settings.get());
+ },
+ reloadSwitches: () => {
+ let switches = document.querySelectorAll(".switch");
+
+ for (let i = 0; i < switches.length; i++) {
+ switches[i].setAttribute("onclick", `Settings.switch(${i})`);
+ }
+ },
+ switch: (element, state) => {
+ let switches = document.querySelectorAll(".switch");
+ if (switches[element]) {
+ element = switches[element];
+ }
+
+ let on = () => {
+ element.classList.add("on");
+ element.classList.remove("off");
+ }
+
+ let off = () => {
+ element.classList.add("off");
+ element.classList.remove("on");
+ }
+
+ if (state != undefined) {
+ if (state) {on()} else {off()}
+ } else {
+ if (element.classList.contains("on")) {off()} else {on()}
+ }
+
+ Settings.reloadSwitches();
+ },
+ get: () => {
+ let opts = {};
+ let options = document.querySelectorAll(".option");
+
+ for (let i = 0; i < options.length; i++) {
+ let optName = options[i].getAttribute("name");
+ if (options[i].querySelector(".actions input")) {
+ let input = options[i].querySelector(".actions input").value;
+ if (options[i].getAttribute("type")) {
+ opts[optName] = input.split(" ");
+ } else {
+ opts[optName] = input;
+ }
+ } else if (options[i].querySelector(".actions select")) {
+ opts[optName] = options[i].querySelector(".actions select").value;
+ } else if (options[i].querySelector(".actions .switch")) {
+ if (options[i].querySelector(".actions .switch.on")) {
+ opts[optName] = true;
+ } else {
+ opts[optName] = false;
+ }
+ }
+ }
+
+ return opts;
+ },
+ load: () => {
+ let options = document.querySelectorAll(".option");
+
+ for (let i = 0; i < options.length; i++) {
+ let optName = options[i].getAttribute("name");
+ if (optName == "forcedlang") {
+ let div = options[i].querySelector("select");
+
+ div.innerHTML = "";
+ let langs = fs.readdirSync(__dirname + "/../lang");
+ for (let i in langs) {
+ title = JSON.parse(fs.readFileSync(__dirname + `/../lang/${langs[i]}`, "utf8"))["lang.title"];
+ if (title) {
+ div.innerHTML += `<option value="${langs[i].replace(/\..*$/, '')}">${title}</option>`
+ }
+
+ }
+
+ div.value = settings.forcedlang;
+ continue;
+ }
+
+ if (settings[optName] != undefined) {
+ switch(typeof settings[optName]) {
+ case "string":
+ options[i].querySelector(".actions input").value = settings[optName];
+ break
+ case "object":
+ options[i].querySelector(".actions input").value = settings[optName].join(" ");
+ break
+ case "boolean":
+ let switchDiv = options[i].querySelector(".actions .switch");
+ if (settings[optName]) {
+ switchDiv.classList.add("on");
+ switchDiv.classList.remove("off");
+ } else {
+ switchDiv.classList.add("off");
+ switchDiv.classList.remove("on");
+ }
+ break
+
+ }
+ }
+ }
+
+ if (process.platform == "win32") {
+ let linuxopts = document.querySelectorAll(".options .linuxopt");
+ for (let i = 0; i < linuxopts.length; i++) {
+ linuxopts[i].style.display = "none";
+ }
+ }
+
+ ipcRenderer.send("can-autoupdate");
+ ipcRenderer.on("cant-autoupdate", () => {
+ document.querySelector(".option[name=autoupdate]").style.display = "none";
+ })
+ }
+}
+
+Settings.reloadSwitches();
+Settings.load();
diff --git a/src/app/js/toast.js b/src/app/js/toast.js
new file mode 100644
index 0000000..3bc1745
--- /dev/null
+++ b/src/app/js/toast.js
@@ -0,0 +1,63 @@
+function Toast(properties) {
+ let toast = {
+ fg: "#000000",
+ bg: "#FFFFFF",
+ timeout: 3000,
+ callback: () => {},
+ title: "Untitled Toast",
+ description: "No description provided for toast",
+ ...properties
+ }
+
+ switch(toast.scheme) {
+ case "error":
+ toast.fg = "#FFFFFF";
+ toast.bg = "rgb(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.addEventListener("click", () => {
+ dismissToast(id);
+ toast.callback();
+ })
+
+ 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)
+ }
+}