aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app/browser.js4
-rw-r--r--src/app/icons/apply.pngbin0 -> 2555 bytes
-rw-r--r--src/app/icons/settings.pngbin0 -> 2496 bytes
-rw-r--r--src/app/index.html70
-rw-r--r--src/app/main.css146
-rw-r--r--src/app/main.js9
-rw-r--r--src/app/settings.js108
-rw-r--r--src/index.js14
-rw-r--r--src/lang/en.json13
-rw-r--r--src/lang/es.json13
-rw-r--r--src/lang/fr.json13
-rw-r--r--src/utils.js33
12 files changed, 372 insertions, 51 deletions
diff --git a/src/app/browser.js b/src/app/browser.js
index ded12fa..275e16e 100644
--- a/src/app/browser.js
+++ b/src/app/browser.js
@@ -104,10 +104,6 @@ var Browser = {
}
}
-document.body.addEventListener("keyup", (e) => {
- if (e.key == "Escape") {Browser.toggle(false)}
-})
-
function BrowserElFromObj(obj) {
let pkg = {...obj, ...obj.versions[0]};
diff --git a/src/app/icons/apply.png b/src/app/icons/apply.png
new file mode 100644
index 0000000..915f809
--- /dev/null
+++ b/src/app/icons/apply.png
Binary files differ
diff --git a/src/app/icons/settings.png b/src/app/icons/settings.png
new file mode 100644
index 0000000..3f7715a
--- /dev/null
+++ b/src/app/icons/settings.png
Binary files differ
diff --git a/src/app/index.html b/src/app/index.html
index b5fc8cb..ac1e276 100644
--- a/src/app/index.html
+++ b/src/app/index.html
@@ -10,6 +10,7 @@
<div id="toasts"></div>
<div id="winbtns">
+ <div id="settings" onclick="Settings.toggle(true)"></div>
<div id="minimize" onclick="ipcRenderer.send('minimize')"></div>
<div id="close" onclick="ipcRenderer.send('exit')"></div>
</div>
@@ -21,9 +22,71 @@
</div>
</div>
- <div id="overlay" onclick="Browser.toggle(false)"></div>
- <div id="browser">
- <div id="misc">
+ <div id="overlay" onclick="Browser.toggle(false);Settings.toggle(false)"></div>
+ <div class="popup" id="options">
+ <div class="misc">
+ <div style="width:100%"></div>
+ <button id="apply" onclick="Settings.toggle(false);Settings.apply()">
+ <img src="icons/apply.png">
+ %%gui.settings.save%%
+ </button>
+ <button id="close" onclick="Settings.toggle(false);Settings.load()">
+ <img src="icons/close.png">
+ %%gui.settings.discard%%
+ </button>
+ </div>
+ <div class="options">
+ <h2>Northstar</h2>
+ <div class="option" name="nsargs">
+ <div class="text">
+ %%gui.settings.nsargs.title%%
+ <div class="desc">
+ %%gui.settings.nsargs.desc%%
+ </div>
+ </div>
+ <div class="actions">
+ <input>
+ </div>
+ </div>
+ <h2>Updates</h2>
+ <div class="option" name="autoupdate">
+ <div class="text">
+ %%gui.settings.autoupdate.title%%
+ <div class="desc">
+ %%gui.settings.autoupdate.desc%%
+ </div>
+ </div>
+ <div class="actions">
+ <button class="switch on"></button>
+ </div>
+ </div>
+ <div class="option" name="nsupdate">
+ <div class="text">
+ %%gui.settings.nsupdate.title%%
+ <div class="desc">
+ %%gui.settings.nsupdate.desc%%
+ </div>
+ </div>
+ <div class="actions">
+ <button class="switch on"></button>
+ </div>
+ </div>
+ <div class="option" name="excludes" type="array">
+ <div class="text">
+ %%gui.settings.excludes.title%%
+ <div class="desc">
+ %%gui.settings.excludes.desc%%
+ </div>
+ </div>
+ <div class="actions">
+ <input type="text">
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="popup" id="browser">
+ <div class="misc">
<input id="search" placeholder="%%gui.browser.search%%">
<button id="close" onclick="Browser.toggle(false)">
<img src="icons/close.png">
@@ -126,6 +189,7 @@
<script src="main.js"></script>
<script src="toast.js"></script>
<script src="browser.js"></script>
+ <script src="settings.js"></script>
<script src="launcher.js"></script>
</body>
</html>
diff --git a/src/app/main.css b/src/app/main.css
index dcc4e8a..734f60b 100644
--- a/src/app/main.css
+++ b/src/app/main.css
@@ -1,17 +1,17 @@
:root {
- --red: #C7777F;
- --blue: #81A1C1;
- --orange: #D59783;
+ --red: 199, 119, 127;
+ --blue: 129, 161, 193;
+ --orange: 213, 151, 131;
--padding: 25px;
--bg: rgba(0, 0, 0, 0.5);
--selbg: rgba(80, 80, 80, 0.5);
- --redbg: linear-gradient(45deg, var(--red), #FA4343);
- --bluebg: linear-gradient(45deg, var(--blue), #7380ED);
+ --redbg: linear-gradient(45deg, rgb(var(--red)), #FA4343);
+ --bluebg: linear-gradient(45deg, rgb(var(--blue)), #7380ED);
}
-#browser, #modsdiv {
+.popup, #modsdiv {
outline: 1px solid #444444;
border: 3px solid var(--bg);
}
@@ -27,12 +27,12 @@
::-webkit-scrollbar-thumb {
border-radius: 10px;
- background: var(--red);
+ background: rgb(var(--red));
}
::selection {
color: black;
- background: var(--red);
+ background: rgb(var(--red));
}
body {
@@ -66,7 +66,7 @@ button {
cursor: pointer;
}
-#browser {
+.popup {
--spacing: var(--padding);
z-index: 2;
@@ -85,7 +85,7 @@ button {
transition: opacity 0.15s ease-in-out, transform 0.15s ease-in-out;
}
-#browser.shown {
+.popup.shown {
opacity: 1.0;
pointer-events: all;
transform: scale(1.0);
@@ -114,7 +114,7 @@ button {
100% {opacity: 1.0}
}
-#browser .el, #browser #misc, #browser .loading {
+.popup .el, .popup .misc, .popup .loading {
--spacing: calc(var(--padding) / 2);
--height: calc(var(--padding) * 3);
--mischeight: calc(var(--padding) * 1.5);
@@ -129,7 +129,7 @@ button {
transition: 0.15s ease-in-out;
}
-#browser .el, #browser #search, #browser #close {
+.popup .el, .popup #search, .option .actions input, .popup #close, .popup .misc button {
color: white;
display: flex;
align-items: center;
@@ -141,41 +141,48 @@ button {
width: calc(100% - var(--spacing) * 4);
}
-#browser #misc, #browser #search {
+.popup .misc, .popup #search, .option .actions input {
--height: var(--mischeight);
}
-#browser #misc {
+.popup .misc {
display: flex;
}
-#browser #search {
+.popup #search, .option .actions input {
border: none;
outline: none;
transition: filter 0.15s ease-in-out;
width: calc(100% - var(--spacing) * 2);
}
-#browser #search:focus {
+.popup #search:focus, .option .actions input:focus, .option .actions button:active {
filter: brightness(1.5);
}
-#browser #close {
+.popup .misc button {
--height: calc(var(--padding) * 1.5);
padding: 0px;
margin-left: 0px;
- width: var(--height);
+ padding: 0px !important;
+ width: var(--height) !important;
}
-#browser #close img {
+.popup .misc button img {
opacity: 0.6;
width: var(--height);
- height: var(--height);
transform: scale(0.6);
+ height: var(--height) !important;
+}
+
+#options.popup .misc button {
+ margin-left: 0px;
+ width: auto !important;
+ padding-right: calc(var(--padding) / 2) !important;
}
-#browser .loading {
+.popup .loading {
width: 100%;
color: white;
display: flex;
@@ -185,38 +192,104 @@ button {
height: calc(100% - var(--mischeight) - var(--height));
}
-#browser .message {
+.popup .message {
color: white;
text-align: center;
margin: var(--padding);
width: calc(100% - var(--padding));
}
-#browser .el .image, #browser .el .image img {
+.popup .el .image, .popup .el .image img {
width: var(--height);
height: var(--height);
margin-right: var(--spacing);
border-radius: var(--spacing);
}
-#browser .el .text {
+.popup .el .text {
overflow: hidden;
}
-#browser .el .title, #browser .el .description {
+.popup .el .title, .popup .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);
+.popup .el .title {font-size: 1.2em}
+.popup .el .description {font-size: 0.8em}
+.popup .el button {
+ background: rgb(var(--blue));
margin-top: var(--spacing);
}
+.popup .options {
+ color: white;
+ margin: calc(var(--padding) / 2);
+}
+
+.popup .options .option {
+ width: 100%;
+ display: flex;
+ margin-bottom: var(--padding);
+ justify-content: space-between;
+}
+
+.option .text {font-weight: 600}
+.option .text .desc {
+ opacity: 0.8;
+ font-weight: 500;
+ font-size: 0.9em;
+ max-width: 400px;
+ margin-top: calc(var(--padding) / 3);
+}
+
+.option .actions input {
+ width: 100%;
+ margin: 0px;
+ --spacing: calc(var(--padding) / 3);
+}
+
+.option[type=array] .actions input {
+ word-spacing: 15px;
+ margin-right: 15vw;
+}
+
+.option .actions button {
+ background: var(--selbg);
+}
+
+.switch {
+ width: 50px;
+ height: 25px;
+ border-radius: 50px;
+}
+
+.switch.on {
+ background: rgba(var(--red), 0.2) !important;
+}
+
+.switch::after {
+ left: -5px;
+ width: 15px;
+ height: 15px;
+ content: " ";
+ display: block;
+ background: red;
+ position: relative;
+ border-radius: 50px;
+ background: var(--bg);
+ transition: 0.2s ease-in-out;
+}
+
+.switch.on::after {
+ left: 15px;
+ width: 20px;
+ opacity: 0.5;
+ background: rgb(var(--red));
+}
+
#winbtns {
z-index: 2;
display: flex;
@@ -237,9 +310,10 @@ button {
#winbtns #close {background-image: url("icons/close.png")}
#winbtns #minimize {background-image: url("icons/minimize.png")}
+#winbtns #settings {background-image: url("icons/settings.png")}
#winbtns div:hover {opacity: 1.0}
-#winbtns div:active {transform: scale(0.98)}
+#winbtns div:active {transform: scale(0.95)}
button:hover {filter: brightness(110%)}
button:active {filter: brightness(90%)}
@@ -388,7 +462,7 @@ img {pointer-events: none}
text-align: center;
position: relative;
border-radius: 50px;
- background: var(--red);
+ background: rgb(var(--red));
left: calc(50% - 15px);
transition: 0.2s ease-in-out;
}
@@ -453,7 +527,7 @@ img {pointer-events: none}
}
a {
- color: var(--red);
+ color: rgb(var(--red));
text-decoration: none;
transition: filter 0.2s ease-in;
}
@@ -500,12 +574,12 @@ a:hover {
}
.simplebar-scrollbar:before {
- background: var(--red) !important;
+ background: rgb(var(--red)) !important;
}
-#installmod {background: var(--blue)}
-#togglemod, #toggleall {background: var(--orange)}
-#northstar, #removeall, #removemod {background: var(--red)}
+#installmod {background: rgb(var(--blue))}
+#togglemod, #toggleall {background: rgb(var(--orange))}
+#northstar, #removeall, #removemod {background: rgb(var(--red))}
button:disabled {
opacity: 0.5;
pointer-events: none;
diff --git a/src/app/main.js b/src/app/main.js
index 97937a9..a781ef1 100644
--- a/src/app/main.js
+++ b/src/app/main.js
@@ -8,7 +8,9 @@ let shouldInstallNorthstar = false;
// Base settings
var settings = {
+ nsargs: "",
gamepath: "",
+ nsupdate: true,
autoupdate: true,
zip: "/northstar.zip",
lang: navigator.language,
@@ -275,6 +277,13 @@ document.addEventListener("drop", (e) => {
installFromPath(event.dataTransfer.files[0].path)
});
+document.body.addEventListener("keyup", (e) => {
+ if (e.key == "Escape") {
+ Browser.toggle(false);
+ Settings.toggle(false);
+ }
+})
+
document.body.addEventListener("click", event => {
if (event.target.tagName.toLowerCase() === "a" && event.target.protocol != "file:") {
event.preventDefault();
diff --git a/src/app/settings.js b/src/app/settings.js
new file mode 100644
index 0000000..902e57b
--- /dev/null
+++ b/src/app/settings.js
@@ -0,0 +1,108 @@
+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("savesettings", 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) => {
+ let switches = document.querySelectorAll(".switch");
+ element = switches[element];
+
+ if (element.classList.contains("on")) {
+ element.classList.add("off");
+ element.classList.remove("on");
+ } else {
+ element.classList.add("on");
+ element.classList.remove("off");
+ }
+
+ 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 .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 (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
+
+ }
+ }
+ }
+
+ ipcRenderer.send("can-autoupdate");
+ ipcRenderer.on("cant-autoupdate", () => {
+ document.querySelector(".option[name=autoupdate]").style.display = "none";
+ })
+ }
+}
+
+Settings.reloadSwitches();
+Settings.load();
diff --git a/src/index.js b/src/index.js
index 95115b4..265e13d 100644
--- a/src/index.js
+++ b/src/index.js
@@ -53,11 +53,23 @@ function start() {
ipcMain.on("installedmod", (event, modname) => {win.webContents.send("installedmod", modname)});
ipcMain.on("guigetmods", (event, ...args) => {win.webContents.send("mods", utils.mods.list())});
+ ipcMain.on("savesettings", (event, obj) => {utils.saveSettings(obj)})
+
+ ipcMain.on("can-autoupdate", (event) => {
+ if (! require("electron-updater").autoUpdater.isUpdaterActive()) {
+ win.webContents.send("cant-autoupdate")
+ }
+ })
+
win.webContents.on("dom-ready", () => {
win.webContents.send("mods", utils.mods.list());
});
- if (utils.settings.autoupdate) {utils.updatevp(false)}
+ if (utils.settings.autoupdate) {
+ utils.updatevp(false)
+ } else {
+ utils.handleNorthstarUpdating();
+ }
autoUpdater.on("update-downloaded", () => {
win.webContents.send("updateavailable")
diff --git a/src/lang/en.json b/src/lang/en.json
index 45bf7c0..47e18c0 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -73,6 +73,19 @@
"gui.browser.loading": "Loading mods...",
"gui.browser.endoflist": "Maximum packages has been loaded.<br>Use the search for finding other packages!",
+ "gui.settings.save": "Save",
+ "gui.settings.discard": "Discard",
+ "gui.settings.title.ns": "Northstar",
+ "gui.settings.title.updates": "Updates",
+ "gui.settings.nsargs.title": "Launch options",
+ "gui.settings.nsargs.desc": "Here you can add launch options for Northstar/Titanfall.",
+ "gui.settings.autoupdate.title": "Viper Auto-Updates",
+ "gui.settings.autoupdate.desc": "Viper will automatically keep itself up-to-date.",
+ "gui.settings.nsupdate.title": "Northstar Auto-Updates",
+ "gui.settings.nsupdate.desc": "Viper will automatically keep Northstar up-to-date, however it can still manually be updated through the Northstar page.",
+ "gui.settings.excludes.title": "Retain files on update",
+ "gui.settings.excludes.desc": "When Northstar is updated, files specified here will not be overwritten by files from the new Northstar update, unless you know what you're changing, you should probably not change anything here. Each file is separated with a space.",
+
"gui.update.downloading": "Downloading...",
"gui.update.extracting": "Extracting update...",
"gui.update.finished": "Done! Ready to play!",
diff --git a/src/lang/es.json b/src/lang/es.json
index 450d559..b570a8d 100644
--- a/src/lang/es.json
+++ b/src/lang/es.json
@@ -73,6 +73,19 @@
"gui.browser.loading": "Cargando modificaciones...",
"gui.browser.endoflist": "Se ha cargado el máximo de paquetes.<br>¡Usa la búsqueda para encontrar otros paquetes!",
+ "gui.settings.save": "Guardar",
+ "gui.settings.discard": "Descartar",
+ "gui.settings.title.ns": "Northstar",
+ "gui.settings.title.updates": "Actualizaciones",
+ "gui.settings.nsargs.title": "Opciones de lanzamiento",
+ "gui.settings.nsargs.desc": "Aqui puedes añadir opciones de lanzamiento para Northstar/Titanfall.",
+ "gui.settings.autoupdate.title": "Actualizaciones automáticas de Viper",
+ "gui.settings.autoupdate.desc": "Viper se mantendrá automáticamente actualizado.",
+ "gui.settings.nsupdate.title": "Actualizaciones automáticas de Northstar",
+ "gui.settings.nsupdate.desc": "Viper mantendrá Northstar actualizado automáticamente, sin embargo, todavía se puede actualizar manualmente a través de la sección de Northstar.",
+ "gui.settings.excludes.title": "Conservar archivos en la actualización",
+ "gui.settings.excludes.desc": "Cuando se actualice Northstar, los archivos especificados aquí no se sobrescribirán con archivos de la nueva actualización de Northstar. A menos que sepa lo que está cambiando, probablemente no debería cambiar nada aquí. Cada archivo se debe separar con un espacio.",
+
"gui.update.downloading": "Descargando...",
"gui.update.extracting": "Extrayendo actualización...",
"gui.update.finished": "¡Hecho! ¡Está listo para jugar!",
diff --git a/src/lang/fr.json b/src/lang/fr.json
index 14bae37..86ec763 100644
--- a/src/lang/fr.json
+++ b/src/lang/fr.json
@@ -73,6 +73,19 @@
"gui.browser.loading": "Chargement des mods...",
"gui.browser.endoflist": "Fin de la liste de mods.<br>Utilisez la barre de recherche pour en trouver davantage !",
+ "gui.settings.save": "Sauvegarder",
+ "gui.settings.discard": "Annuler",
+ "gui.settings.title.ns": "Northstar",
+ "gui.settings.title.updates": "Mises à jour",
+ "gui.settings.nsargs.title": "Options de lancement",
+ "gui.settings.nsargs.desc": "Vous pouvez ajouter ici des options de démarrage pour Northstar/Titanfall.",
+ "gui.settings.autoupdate.title": "Mises à jour pour Viper",
+ "gui.settings.autoupdate.desc": "Viper se tient automatiquement à jour.",
+ "gui.settings.nsupdate.title": "Mises à jour pour Northstar",
+ "gui.settings.nsupdate.desc": "Viper tient automatiquement Northstar à jour (n'empêche pas de le mettre à jour manuellement via sa page dédiée).",
+ "gui.settings.excludes.title": "Fichiers à conserver",
+ "gui.settings.excludes.desc": "Lorsque Northstar est mis à jour, ces fichiers ne seront pas écrasés par ceux provenant de la mise à jour; les noms de fichiers sont séparés par un espace.",
+
"gui.update.downloading": "Téléchargement de la mise à jour...",
"gui.update.extracting": "Extraction des fichiers...",
"gui.update.finished": "Terminé, vous pouvez jouer !",
diff --git a/src/utils.js b/src/utils.js
index 336017f..6069a46 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -22,11 +22,13 @@ process.chdir(app.getPath("appData"));
var settings = {
gamepath: "",
lang: "en-US",
+ nsupdate: true,
autoupdate: true,
+ nsargs: "-multiple",
zip: "/northstar.zip",
// These files won't be overwritten when installing/updating
- // Northstar, useful for config file.
+ // Northstar, useful for config files
excludes: [
"ns_startup_args.txt",
"ns_startup_args_dedi.txt"
@@ -73,8 +75,8 @@ async function isGameRunning() {
//
// It uses isGameRunning() to ensure it doesn't run while the game is
// running, as that may have all kinds of issues.
-function handleNorthstarAutoUpdating() {
- if (!settings.autoupdate || !fs.existsSync("viper.json") || settings.gamepath.length === 0) {
+function handleNorthstarUpdating() {
+ if (! settings.nsupdate || ! fs.existsSync("viper.json") || settings.gamepath.length === 0) {
return;
}
@@ -170,8 +172,13 @@ async function setpath(win, forcedialog) {
// As to not have to do the same one liner a million times, this
// function exists, as the name suggests, it simply writes the current
// settings to the disk.
-function saveSettings() {
- fs.writeFileSync(app.getPath("appData") + "/viper.json", JSON.stringify(settings));
+//
+// You can also pass a settings object to the function and it'll try and
+// merge it together with the already existing settings
+function saveSettings(obj = {}) {
+ settings = {...settings, ...obj};
+ fs.writeFileSync(path.join(settings.gamepath, "ns_startup_args.txt"), settings.nsargs);
+ fs.writeFileSync(app.getPath("appData") + "/viper.json", JSON.stringify({...settings, ...obj}));
}
// Returns the current Northstar version
@@ -299,6 +306,13 @@ async function update() {
function updatevp(autoinstall) {
const { autoUpdater } = require("electron-updater");
+ if (! autoUpdater.isUpdaterActive()) {
+ if (settings.nsupdate) {
+ handleNorthstarUpdating();
+ }
+ return cli.exit();
+ }
+
if (autoinstall) {
autoUpdater.on("update-downloaded", (info) => {
autoUpdater.quitAndInstall();
@@ -307,8 +321,11 @@ function updatevp(autoinstall) {
autoUpdater.on("error", (info) => {cli.exit(1)});
autoUpdater.on("update-not-available", (info) => {
- // only check for N* updates if Viper itself has no updates
- handleNorthstarAutoUpdating();
+ // only check for NS updates if Viper itself has no updates and
+ // if NS auto updates is enabled.
+ if (settings.nsupdate || cli.hasArgs()) {
+ handleNorthstarUpdating();
+ }
cli.exit();
});
@@ -756,9 +773,11 @@ module.exports = {
setpath,
updatevp,
settings,
+ saveSettings,
getNSVersion,
getTF2Version,
isGameRunning,
+ handleNorthstarUpdating,
setlang: (lang) => {
settings.lang = lang;
saveSettings();