diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/app/browser.js | 259 | ||||
-rw-r--r-- | src/app/css/dragui.css | 58 | ||||
-rw-r--r-- | src/app/css/launcher.css | 328 | ||||
-rw-r--r-- | src/app/css/popups.css | 371 | ||||
-rw-r--r-- | src/app/css/theming.css | 62 | ||||
-rw-r--r-- | src/app/css/toasts.css | 52 | ||||
-rw-r--r-- | src/app/css/webview.css | 16 | ||||
-rw-r--r-- | src/app/icons/external.png | bin | 0 -> 9764 bytes | |||
-rw-r--r-- | src/app/icons/game.png | bin | 0 -> 1454 bytes | |||
-rw-r--r-- | src/app/icons/language.png | bin | 0 -> 7132 bytes | |||
-rw-r--r-- | src/app/icons/updates.png | bin | 0 -> 10300 bytes | |||
-rw-r--r-- | src/app/index.html | 47 | ||||
-rw-r--r-- | src/app/lang.js | 2 | ||||
-rw-r--r-- | src/app/launcher.js | 99 | ||||
-rw-r--r-- | src/app/main.css | 742 | ||||
-rw-r--r-- | src/app/main.js | 126 | ||||
-rw-r--r-- | src/app/settings.js | 14 | ||||
-rw-r--r-- | src/cli.js | 22 | ||||
-rw-r--r-- | src/extras/findgame.js | 85 | ||||
-rw-r--r-- | src/extras/requests.js | 36 | ||||
-rw-r--r-- | src/index.js | 181 | ||||
-rw-r--r-- | src/lang.js | 24 | ||||
-rw-r--r-- | src/lang/de.json | 167 | ||||
-rw-r--r-- | src/lang/en.json | 23 | ||||
-rw-r--r-- | src/lang/es.json | 24 | ||||
-rw-r--r-- | src/lang/fr.json | 21 | ||||
-rw-r--r-- | src/lang/maintainers.json | 5 | ||||
-rw-r--r-- | src/utils.js | 438 |
28 files changed, 2138 insertions, 1064 deletions
diff --git a/src/app/browser.js b/src/app/browser.js index 40670bc..db68d31 100644 --- a/src/app/browser.js +++ b/src/app/browser.js @@ -2,9 +2,24 @@ 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 = []; @@ -27,6 +42,19 @@ var Browser = { 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])) { @@ -53,14 +81,14 @@ var Browser = { let spacing = parseInt(getComputedStyle(filters).getPropertyValue("--spacing")); filters.style.top = filterRect.bottom - spacing; - filters.style.right = filterRect.right - filterRect.left + filterRect.width; + 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") + overlay.classList.add("shown"); + browser.classList.add("shown"); if (browserEntries.querySelectorAll(".el").length == 0) { Browser.loadfront(); @@ -69,18 +97,21 @@ var Browser = { } else if (! state) { if (state != undefined) { Browser.filters.toggle(false); - overlay.classList.remove("shown") - browser.classList.remove("shown") + 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") + 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(); @@ -90,8 +121,15 @@ var Browser = { }) } - for (let i in packages) { - new BrowserElFromObj(packages[i]); + let pkgs = Browser.filters.getpkgs(); + for (let i in pkgs) { + if (packagecount >= Browser.maxentries) { + Browser.endoflist(); + break + } + + new BrowserElFromObj(pkgs[i]); + packagecount++; } }, loading: (string) => { @@ -107,21 +145,53 @@ var Browser = { browserEntries.innerHTML = `<div class="loading">${lang('gui.browser.loading')}</div>`; } }, - endoflist: () => { - if (browserEntries.querySelector(".message")) {return} - browserEntries.innerHTML += `<div class="message">${lang('gui.browser.endoflist')}</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")) + 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) => { @@ -157,6 +227,58 @@ var Browser = { } }, 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}")`); } } @@ -171,16 +293,14 @@ function BrowserElFromObj(obj) { download: pkg.download_url, version: pkg.version_number, categories: pkg.categories, - description: pkg.description + description: pkg.description, + dependencies: pkg.dependencies, }) } function BrowserEl(properties) { if (Browser.filters.isfiltered(properties.categories)) {return} - let entries = browser.querySelectorAll(".el").length; - if (entries == Browser.maxentries) {Browser.endoflist();return} - properties = { title: "No name", version: "1.0.0", @@ -230,25 +350,30 @@ function BrowserEl(properties) { } } } - - browserEntries.innerHTML += ` - <div class="el" id="mod-${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> + + 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='installFromURL("${properties.download}", ${JSON.stringify(properties.dependencies)}, true)'>${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> ` + + browserEntries.appendChild(entry); } -ipcRenderer.on("removedmod", (event, mod) => { +ipcRenderer.on("removed-mod", (event, mod) => { setButtons(true); Browser.setbutton(mod.name, lang("gui.browser.install")); if (mod.manifestname) { @@ -256,7 +381,7 @@ ipcRenderer.on("removedmod", (event, mod) => { } }) -ipcRenderer.on("failedmod", (event, modname) => { +ipcRenderer.on("failed-mod", (event, modname) => { setButtons(true); new Toast({ timeout: 10000, @@ -266,7 +391,17 @@ ipcRenderer.on("failedmod", (event, modname) => { }) }) -ipcRenderer.on("installedmod", (event, mod) => { +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) => { setButtons(true); Browser.setbutton(mod.name, lang("gui.browser.reinstall")); @@ -284,14 +419,21 @@ ipcRenderer.on("installedmod", (event, mod) => { 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]); + installqueue.shift(); + } }) function normalize(items) { let main = (string) => { - return string.replaceAll(" ", "").replaceAll(".", "").replaceAll("-", "").replaceAll("_", "").toLowerCase() + return string.replaceAll(" ", "") + .replaceAll(".", "").replaceAll("-", "") + .replaceAll("_", "").toLowerCase(); } if (typeof items == "string") { - return main(items) + return main(items); } else { let newArray = []; for (let i = 0; i < items.length; i++) { @@ -322,11 +464,50 @@ search.addEventListener("keyup", () => { } }) -browser.addEventListener("scroll", () => { - Browser.filters.toggle(false); +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 = ''") + checks[i].setAttribute("onclick", + "this.classList.toggle('checked');Browser.loadfront();search.value = ''" + ) } diff --git a/src/app/css/dragui.css b/src/app/css/dragui.css new file mode 100644 index 0000000..fb5b1a7 --- /dev/null +++ b/src/app/css/dragui.css @@ -0,0 +1,58 @@ +@import "theming.css"; + +/* + This stylesheet is meant for the DragUI, i.e the UI that pops up when dragging + a modfile over the window. +*/ + +#dragUI { + top: 0; + left: 0; + right: 0; + bottom: 0; + color: white; + opacity: 0.0; + position: fixed; + z-index: 1000000; + pointer-events: none; + background: var(--bg); + backdrop-filter: blur(15px); + transition: 0.1s ease-in-out; +} + +#dragUI.shown { + opacity: 1.0; + pointer-events: all; +} + +#dragUI #dragitems { + --size: 25vw; + top: 50%; + left: 50%; + opacity: 0.6; + position: absolute; + text-align: center; + width: var(--size); + height: var(--size); + margin-top: calc(var(--size) / 2 * -1); + margin-left: calc(var(--size) / 2 * -1); +} + +#dragUI #dragitems #icon { + width: 100%; + height: 100%; + filter: invert(1); + transform: scale(0.45); + background-size: cover; + background-image: url("../icons/download.png"); + transition: 0.1s ease-in-out; +} + +#dragUI.shown #dragitems #icon { + transform: scale(0.5); +} + +#dragUI #dragitems #text { + top: -5vw; + position: relative; +} diff --git a/src/app/css/launcher.css b/src/app/css/launcher.css new file mode 100644 index 0000000..ce54ddf --- /dev/null +++ b/src/app/css/launcher.css @@ -0,0 +1,328 @@ +@import "theming.css"; + +/* + This stylesheet is meant for various elements around the launcher, + notably the navbar, launch buttons, and sidebar. +*/ + +.gamesContainer { + width: 10%; + height: 100%; + min-width: 95px; + max-width: 120px; + + float: left; + display: flex; + flex-wrap: wrap; + align-content: center; +} + +.mainContainer { + height: 100%; + flex-grow: 1; + display: flex; + position: relative; +} + +/* nav bar buttons */ +.gamesContainer button { + background-size: 90%; + background-position: center; + background-repeat: no-repeat; + + border: none; + transition: 0.3s ease-in-out; + background-color: transparent; + + + margin: 20px; + position: relative; + box-sizing: border-box; + flex-basis: calc(100% - 10px); +} + +.gamesContainer button.inactive { + opacity: 0.5; + transform: scale(0.9); +} + +.gamesContainer button::before { + content: ""; + display: block; + padding-top: 100%; +} + +.gamesContainer button .content { + width: 100%; + height: 100%; + top: 0; left: 0; + position: absolute; +} + +#vpBtn {background-image: url("../icons/viper.png")} +#nsBtn {background-image: url("../icons/northstar.png")} +#tfBtn { + background-image: url("../icons/titanfall2.png"); + background-size: 69%; /* nice */ +} + +.contentContainer { + width: 85%; + color: white; + flex-grow: 1; + opacity: 1.0; + margin-left: 5%; + position: absolute; + transition: 0.15s ease-in-out; +} + +.contentContainer.hidden { + opacity: 0.0; + pointer-events: none; +} + +.contentMenu { + padding: 0; + flex-grow: 1; + display: flex; + font-size: 20px; + list-style: none; + margin-bottom: 0; + align-items: center; + justify-content: center; + margin-top: var(--padding); +} + +/* some sections do not need space between them and menu */ +#nsMain, #nsRelease, #vpReleaseNotes, .playBtnContainer { + margin-top: 0 !important; +} + +.contentMenu li { + opacity: 0.6; + margin: 0 26px; + cursor: pointer; + font-weight: 700; + text-align: center; + height: fit-content; + transition: opacity 0.3s ease-in-out; +} + +.contentMenu li:last-child {margin-right: 0px} +.contentMenu li:first-child {margin-left: 0px} + +.contentMenu li:hover {opacity: 0.7} + +.contentMenu li[active] { + opacity: 1.0; + pointer-events: none; +} + +.contentMenu li::after { + top: 10px; + width: 30px; + height: 5px; + opacity: 0.0; + content: " "; + display: block; + text-align: center; + position: relative; + border-radius: 50px; + left: calc(50% - 15px); + background: rgb(var(--red)); + transition: 0.2s ease-in-out; +} + +.contentMenu li[active]::after { + top: 5px; + opacity: 1.0; +} + +.section { + opacity: 1.0; + position: fixed; + right: calc(var(--padding) * 2); + left: calc(100px + var(--padding)); + transition: opacity 0.15s ease-in-out; +} + +.section.hidden { + opacity: 0.0; + pointer-events: none; +} + +.contentBody img {max-width: 100%} +.contentBody .img {text-align: center} + +.contentBody .section > :first-child:not(.img) { + margin-top: 35px; +} + +.contentContainer .playBtnContainer { + text-align: center; +} + +.contentContainer .playBtn { + width: 27%; + height: 11%; + border: none; + color: white; + padding: 20px; + font-size: 24px; + font-weight: bold; + margin-top: 100px; + margin-bottom: 10px; + border-radius: 10px; + background: var(--redbg); + transition: 0.2s ease-in-out; + filter: drop-shadow(0px 8px 5px rgba(0, 0, 0, 0.1)); +} + +.contentContainer .playBtn:hover { + transform: scale(1.05); + filter: drop-shadow(0px 5px 15px rgba(0, 0, 0, 0.3)) brightness(110%); +} + +.contentContainer .playBtn:active { + opacity: 0.7;transform: scale(0.98); + filter: drop-shadow(0px 5px 10px rgba(0, 0, 0, 0.4)); +} + +.contentContainer #nsMain .playBtn { + background: var(--bluebg); +} + +#nsContent .contentMenu { + margin-bottom: 0; +} + +.contentBody .img img { + transform: scale(0.85); +} + +.contentBody .img { + width: 100%; + text-align: center; +} + +#nsRelease, #vpReleaseNotes { + height: 80vh; + overflow-y: scroll; + flex-direction: column; + margin-top: 10px !important; +} + +.inline * { + display: inline-block; +} + +#vpMain { + margin-top: 140px; + text-align: center; +} + +#vpMain img { + width: 20%; +} + +#vpVersion { + font-size: 16px; +} + +.simplebar-scrollbar:before { + background: rgb(var(--red)) !important; +} + +#installmod {background: rgb(var(--blue))} +#findmod {background: rgb(var(--blue2))} + +#togglemod {background: rgb(var(--orange))} +#toggleall {background: rgb(var(--orange2))} + +#removemod {background: rgb(var(--red))} +#removeall {background: rgb(var(--red2))} +button:disabled { + opacity: 0.5; + pointer-events: none; +} + +button.visual { + opacity: 1.0; + padding-right: 0px; + pointer-events: none; + background: transparent !important; +} + +code { + font-size: 16px; + padding: 2px 5px; + border-radius: 3px; + background-color: #00000070; +} + +#nsMods .line { + display: flex; + align-items: center; + margin-top: calc(var(--padding) / 2); +} + +#modsdiv { + height: 50vh; + overflow-y: scroll; + border-radius: 5px; + background: var(--bg); + backdrop-filter: blur(15px); + padding: calc(var(--padding) / 4); +} + +#modsdiv .mod { + display: flex; + border-radius: 5px; + transition: 0.1s ease-in-out; + margin: calc(var(--padding) / 3); + padding: calc(var(--padding) / 3); +} + +#modsdiv .mod.selected { + background: var(--selbg); +} + +#modsdiv .mod .disabled, .modbtns { + margin-left: auto; +} + +.modbtns button { + margin-left: var(--spacing); + --spacing: calc(var(--padding) / 3); + margin-top: calc(var(--spacing) / 2); + margin-bottom: calc(var(--spacing) / 2); +} + +#serverstatus { + --spacing: calc(var(--padding) / 5); + + transition-duration: 0.2s; + transition-timing-function: ease-in-out; + transition-property: background, opacity; + + opacity: 0.0; + display: block; + margin: 0 auto; + font-weight: 700; + width: fit-content; + color: transparent; + border-radius: 50px; + flex-basis: max-content; + background: transparent; + margin-top: calc(var(--spacing) * 2); + padding: var(--spacing) calc(var(--spacing) * 3); +} + +#serverstatus.up, +#serverstatus.down { + color: white; + opacity: 1.0; +} + +#serverstatus.up {background: rgb(var(--blue));} +#serverstatus.down {background: rgb(var(--red));} diff --git a/src/app/css/popups.css b/src/app/css/popups.css new file mode 100644 index 0000000..826955b --- /dev/null +++ b/src/app/css/popups.css @@ -0,0 +1,371 @@ +@import "theming.css"; + +/* + This stylesheet is meant for the various popups we use, whether it be the + previewer, the browser, the settings menu, or anything alike. +*/ + +.popup { + --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-duration: 0.15s; + transition-property: opacity, transform; + transition-timing-function: ease-in-out; +} + +.popup.shown { + opacity: 1.0; + pointer-events: all; + transform: scale(1.0); +} + +.popup.small { + left: 20vw; + right: 20vw; + top: calc(var(--padding) * 2); + bottom: calc(var(--padding) * 2); +} + +#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; +} + +/* browser/preview popup { */ +@keyframes fadein { + 0% {opacity: 0.0} + 100% {opacity: 1.0} +} + +#browserEntries { + display: flex; + flex-wrap: wrap; +} + +.popup webview { + width: 78%; + margin: 0 auto; + filter: opacity(1.0); + transition: filter 0.15s ease-in-out; + margin-top: calc(var(--spacing) / 2); + height: calc(100% - calc(var(--spacing) / 2)); +} + +.popup webview.loading { + filter: opacity(0.0); + pointer-events: none; +} + +.popup .el, .popup .misc, .popup .loading { + --spacing: calc(var(--padding) / 2); + --height: calc(var(--padding) * 3.5); + --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; +} + +.popup .el, .popup #search, +.popup #close, .popup .misc button, +.option .actions select, .option .actions input { + 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(50% - var(--spacing) * 4); +} + +.popup .misc, .popup #search, .option .actions input { + --height: var(--mischeight); +} + +.popup .misc { + display: flex; +} + +.popup .misc.vertical { + display: block; +} + +.popup .misc.fixed { + width: 100%; + position: fixed; +} + +.popup #search, +.option .actions input, +.option .actions select { + border: none; + outline: none; + transition: filter 0.15s ease-in-out; + width: calc(100% - var(--spacing) * 2); +} + +.popup #search:focus, +.option .actions input:focus, +.option .actions button:active { + filter: brightness(1.5); +} + +.popup .misc button { + --height: calc(var(--padding) * 1.5); + + padding: 0px; + margin-left: 0px; + padding: 0px !important; + width: var(--height) !important; +} + +.popup .misc button img { + opacity: 0.6; + width: var(--height); + transform: scale(0.5); + height: var(--height) !important; +} + +.popup .misc button:last-child { + margin-left: 0px !important; +} + +.popup#preview #close, +.popup .misc.vertical button { + margin: var(--spacing) var(--spacing) 0 auto !important; +} + +.popup .loading { + width: 100%; + color: white; + display: flex; + position: absolute; + text-align: center; + align-items: center; + justify-content: center; + height: calc(100% - var(--mischeight) - var(--height)); +} + +.popup .message { + color: white; + text-align: center; + margin: var(--padding); + width: calc(100% - var(--padding)); +} + +.popup .el .image, .popup .el .image img { + width: var(--height); + height: var(--height); + margin-right: var(--spacing); + border-radius: var(--spacing); +} + +.popup .el .image img.blur { + z-index: -1; + position: relative; + filter: blur(10px); + top: calc(var(--height) * -1 + 5px); +} + +.popup .el .text { + overflow: hidden; +} + +.popup .el .title, .popup .el .description { + height: 1.2em; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.popup .el .title { + font-size: 1.2em; + font-weight: 700; +} + +.popup .message #loadmore { + background: rgb(var(--blue2)); +} + +.popup .el .description {font-size: 0.8em} +.popup .el button { + background: rgb(var(--blue)); + margin-top: var(--spacing); +} + +.popup .el button.info { + background: rgb(var(--blue2)); +} +/* } */ + +/* settings popup { */ + +.popup .options { + color: white; + margin: calc(var(--padding) / 2); +} + +.popup .options .option { + width: 100%; + display: flex; + margin-bottom: var(--padding); + justify-content: space-between; +} + +.popup .overlay { + z-index: 1; + color: white; + opacity: 0.0; + position: fixed; + pointer-events: none; + transform: scale(0.9); + background: var(--selbg); + backdrop-filter: blur(15px); + transition: 0.15s ease-in-out; + padding: calc(var(--spacing) / 2); + border-radius: calc(var(--spacing) / 2); +} + +.popup .overlay.shown { + opacity: 1.0; + pointer-events: all; + transform: scale(1.0); +} + +#options.popup .misc button { + margin-left: 0px; + width: auto !important; + padding-right: calc(var(--padding) / 2) !important; +} + +.check { + display:flex; + cursor: pointer; +} + +.check::before { + width: 1em; + height: 1em; + content: " "; + background-size: 75%; + filter: brightness(1.3); + background-position: center; + background-repeat: no-repeat; + transition: 0.15s ease-in-out; + background-color: var(--selbg); + margin-right: calc(var(--spacing) / 3); + border-radius: calc(var(--spacing) / 4); +} + +.check.checked::before { + background-color: rgb(var(--red)); + background-image: url(../icons/check.png); +} + +.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, .option .actions select { + 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)); +} + +.switch.on:hover::after { + transform: scale(1.2); +} + +.title { + display: flex; +} + +.title img { + width: 30px; + height: 30px; + margin: auto 0; +} + +.title h2 { + margin-left: calc(var(--padding) / 3); +} + +/* } */ diff --git a/src/app/css/theming.css b/src/app/css/theming.css new file mode 100644 index 0000000..2d45b1d --- /dev/null +++ b/src/app/css/theming.css @@ -0,0 +1,62 @@ +/* + the only reason some of these properties have an !important property + is for it to overwrite Thunderstore's CSS in the preview <webview> +*/ + +:root { + --red: 199, 119, 127 !important; + --red2: 181 97 105; + + --blue: 129, 161, 193; + --blue2: 139, 143, 185; + + --orange: 213, 151, 131; + --orange2: 197 129 107; + + + --padding: 25px; + + --bg: rgba(0, 0, 0, 0.5); + --selbg: rgba(80, 80, 80, 0.5); + --redbg: linear-gradient(45deg, rgb(var(--red)), #FA4343); + --bluebg: linear-gradient(45deg, rgb(var(--blue)), #7380ED); +} + +body, button, input { + font-family: "Roboto", sans-serif !important; +} + +body, button, img, a { + user-select: none; + -webkit-user-drag: none; +} + +a { + text-decoration: none !important; + color: rgb(var(--red)) !important; + transition: filter 0.2s ease-in !important; +} + +a:hover { + filter: brightness(80%) !important; +} + + +::-webkit-scrollbar { + width: 8px !important; +} + +::-webkit-scrollbar-track { + border-radius: 10px !important; + background: transparent !important; +} + +::-webkit-scrollbar-thumb { + border-radius: 10px !important; + background: rgb(var(--red)) !important; +} + +::selection { + color: black !important; + background: rgb(var(--red)) !important; +} diff --git a/src/app/css/toasts.css b/src/app/css/toasts.css new file mode 100644 index 0000000..57ba055 --- /dev/null +++ b/src/app/css/toasts.css @@ -0,0 +1,52 @@ +@import "theming.css"; + +#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 .description { + opacity: 0.8; + font-size: 0.8em; + font-weight: 600; +} diff --git a/src/app/css/webview.css b/src/app/css/webview.css new file mode 100644 index 0000000..982ebfc --- /dev/null +++ b/src/app/css/webview.css @@ -0,0 +1,16 @@ +body { + overflow-x: hidden; + background: transparent !important; + background-color: transparent !important; +} + +#ncmp_tool, +.navbar, .bottom-padding, +.card-header, .breadcrumb, +.list-group, .mb-4, .my-2, .mt-2, +#thunderstore-mod-manager-ad-alert { + display: none !important; +} + +.mt-2.mb-2 {display: block !important} +.card {transform: translateY(-1.0rem)} diff --git a/src/app/icons/external.png b/src/app/icons/external.png Binary files differnew file mode 100644 index 0000000..0b4f99e --- /dev/null +++ b/src/app/icons/external.png diff --git a/src/app/icons/game.png b/src/app/icons/game.png Binary files differnew file mode 100644 index 0000000..7db058f --- /dev/null +++ b/src/app/icons/game.png diff --git a/src/app/icons/language.png b/src/app/icons/language.png Binary files differnew file mode 100644 index 0000000..011e1f9 --- /dev/null +++ b/src/app/icons/language.png diff --git a/src/app/icons/updates.png b/src/app/icons/updates.png Binary files differnew file mode 100644 index 0000000..4b505ee --- /dev/null +++ b/src/app/icons/updates.png diff --git a/src/app/index.html b/src/app/index.html index 794e209..5143520 100644 --- a/src/app/index.html +++ b/src/app/index.html @@ -36,7 +36,10 @@ </button> </div> <div class="options"> - <h2>%%gui.settings.title.ns%%</h2> + <div class="title"> + <img src="icons/game.png"> + <h2>%%gui.settings.title.ns%%</h2> + </div> <div class="option" name="nsargs"> <div class="text"> %%gui.settings.nsargs.title%% @@ -72,6 +75,10 @@ </div> </div> <h2>%%gui.settings.title.language%%</h2> + <div class="title"> + <img src="icons/language.png"> + <h2>%%gui.settings.title.language%%</h2> + </div> <div class="option" name="autolang"> <div class="text"> %%gui.settings.autolang.title%% @@ -96,7 +103,10 @@ </select> </div> </div> - <h2>%%gui.settings.title.updates%%</h2> + <div class="title"> + <img src="icons/updates.png"> + <h2>%%gui.settings.title.updates%%</h2> + </div> <div class="option" name="autoupdate"> <div class="text"> %%gui.settings.autoupdate.title%% @@ -130,6 +140,21 @@ <input type="text"> </div> </div> + <div class="title"> + <img src="icons/settings.png"> + <h2>%%gui.settings.title.misc%%</h2> + </div> + <div class="option" name="originkill"> + <div class="text"> + %%gui.settings.originkill.title%% + <div class="desc"> + %%gui.settings.originkill.desc%% + </div> + </div> + <div class="actions"> + <button class="switch off"></button> + </div> + </div> </div> </div> @@ -156,6 +181,17 @@ <div class="loading">%%gui.browser.loading%%</div> </div> </div> + <div class="popup small" id="preview"> + <div class="misc fixed vertical"> + <button id="close" onclick="Preview.hide()"> + <img src="icons/close.png"> + </button> + <button id="external" onclick=""> + <img src="icons/external.png"> + </button> + </div> + <webview></webview> + </div> <nav class="gamesContainer"> <button id="vpBtn" onclick="page(0)"></button> @@ -208,7 +244,8 @@ <button id="playNsBtn" class="playBtn" onclick="launch()">%%gui.launch%%</button> <div class="inline"> <div id="nsversion"></div> - <a id="update" href="#" onclick="update()">(%%gui.update.check%%)</a> + <a id="update" href="#" onclick="updateNorthstar()">(%%gui.update.check%%)</a> + <div id="serverstatus" class="checking"></div> </div> </div> </div> @@ -223,7 +260,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> + <button id="findmod" onclick="Browser.toggle(true)">%%gui.mods.find%%</button> </div> </div> </div> @@ -252,4 +289,4 @@ <script src="settings.js"></script> <script src="launcher.js"></script> </body> -</html> +< diff --git a/src/app/lang.js b/src/app/lang.js index 6fdcd8d..f1c31d3 100644 --- a/src/app/lang.js +++ b/src/app/lang.js @@ -10,7 +10,7 @@ function setlang() { if (html[i][0] != " " && html[i][html[i].length - 1] != " ") { // Replaces it with it's string - html[i] = lang(html[i]) + html[i] = lang(html[i]); } } diff --git a/src/app/launcher.js b/src/app/launcher.js index e0d7619..e1dbbe0 100644 --- a/src/app/launcher.js +++ b/src/app/launcher.js @@ -1,10 +1,14 @@ 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 pages = document.querySelectorAll(".mainContainer .contentContainer") - let btns = document.querySelectorAll(".gamesContainer button") + 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"); @@ -19,36 +23,39 @@ function page(page) { bgHolder.setAttribute("bg", page); }; page(1) - -// Updates the Viper release notes -ipcRenderer.on("vp-notes", (event, response) => { +function formatRelease(notes) { let content = ""; - for (const release of response) { - content += "# " + release.name + "\n\n" - + release.body.replaceAll("\r\n", "\n") + "\n\n\n"; + if (notes.length === 1) { + content = notes[0]; + } else { + for (let release of notes) { + if (release.prerelease) {continue} + content += "# " + release.name + "\n\n" + release.body + "\n\n\n"; + } + + content = content.replaceAll(/\@(\S+)/g, `<a href="https://github.com/$1">@$1</a>`); } - vpReleaseNotes.innerHTML = markdown(content); -}); - -async function loadVpReleases() { - ipcRenderer.send("get-vp-notes"); -}; loadVpReleases(); + 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) => { - let content = ""; - - for (let release of response) { - content += "# " + release.name + "\n\n" - + release.body.replaceAll("\r\n", "\nhtmlbreak") + "\n\n\n"; - } - - nsRelease.innerHTML = markdown(content).replaceAll("htmlbreak", "<br>"); + nsRelease.innerHTML = formatRelease(response); }); +async function loadVpReleases() { + ipcRenderer.send("get-vp-notes"); +}; loadVpReleases(); + async function loadNsReleases() { ipcRenderer.send("get-ns-notes"); }; loadNsReleases(); @@ -83,7 +90,10 @@ function showVpSection(section) { } function showNsSection(section) { - if (!["main", "release", "mods"].includes(section)) throw new Error("unknown ns section"); + if (!["main", "release", "mods"].includes(section)) { + throw new Error("unknown ns section"); + } + nsMainBtn.removeAttribute("active"); nsModsBtn.removeAttribute("active"); nsReleaseBtn.removeAttribute("active"); @@ -108,3 +118,46 @@ function showNsSection(section) { 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/main.css b/src/app/main.css index 87d89e0..f39c5a1 100644 --- a/src/app/main.css +++ b/src/app/main.css @@ -1,52 +1,14 @@ -:root { - --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, rgb(var(--red)), #FA4343); - --bluebg: linear-gradient(45deg, rgb(var(--blue)), #7380ED); -} - -.popup, #modsdiv { - outline: 1px solid #444444; - border: 3px solid var(--bg); -} - -::-webkit-scrollbar { - width: 8px; -} - -::-webkit-scrollbar-track { - border-radius: 10px; - background: transparent; -} - -::-webkit-scrollbar-thumb { - border-radius: 10px; - background: rgb(var(--red)); -} - -::selection { - color: black; - background: rgb(var(--red)); -} +@import "css/dragui.css"; +@import "css/toasts.css"; +@import "css/popups.css"; +@import "css/theming.css"; +@import "css/launcher.css"; body { margin: 0; overflow: hidden; } -body, button, input {font-family: "Roboto", sans-serif} - -body, button, img, a { - -webkit-user-drag: none; - user-select: none; -} - button {outline: none} b, strong {font-weight: 700} body, input, button {font-weight: 500} @@ -62,281 +24,19 @@ button { transition: 0.2s ease-in-out; } -.playBtn, .gamesContainer button, #winbtns div { - cursor: pointer; -} - -.popup { - --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; -} - -.popup.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} -} - -.popup .el, .popup .misc, .popup .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; -} - -.popup .el, .popup #search, .option .actions select, .option .actions input, .popup #close, .popup .misc button { - 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); -} - -.popup .misc, .popup #search, .option .actions input { - --height: var(--mischeight); -} - -.popup .misc { - display: flex; -} - -.popup #search, .option .actions input, .option .actions select { - border: none; - outline: none; - transition: filter 0.15s ease-in-out; - width: calc(100% - var(--spacing) * 2); -} - -.popup #search:focus, .option .actions input:focus, .option .actions button:active { - filter: brightness(1.5); -} - -.popup .misc button { - --height: calc(var(--padding) * 1.5); - - padding: 0px; - margin-left: 0px; - padding: 0px !important; - width: var(--height) !important; -} - -.popup .misc button img { - opacity: 0.6; - width: 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; -} - -.popup .loading { - width: 100%; - color: white; - display: flex; - text-align: center; - align-items: center; - justify-content: center; - height: calc(100% - var(--mischeight) - var(--height)); -} - -.popup .message { - color: white; - text-align: center; - margin: var(--padding); - width: calc(100% - var(--padding)); -} - -.popup .el .image, .popup .el .image img { - width: var(--height); - height: var(--height); - margin-right: var(--spacing); - border-radius: var(--spacing); -} - -.popup .el .text { - overflow: hidden; -} - -.popup .el .title, .popup .el .description { - height: 1.2em; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - -.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; -} - -.popup .overlay { - z-index: 1; - color: white; - opacity: 0.0; - position: fixed; - pointer-events: none; - transform: scale(0.9); - background: var(--selbg); - backdrop-filter: blur(15px); - transition: 0.15s ease-in-out; - padding: calc(var(--spacing) / 2); - border-radius: calc(var(--spacing) / 2); -} - -.popup .overlay.shown { - opacity: 1.0; - pointer-events: all; - transform: scale(1.0); -} +button:hover {filter: brightness(110%)} +button:active {filter: brightness(90%)} -.checks { +.popup, #modsdiv { + outline: 1px solid #444444; + border: 3px solid var(--bg); } -.check { - display:flex; +.playBtn, .gamesContainer button, #winbtns div { cursor: pointer; } -.check::before { - width: 1em; - height: 1em; - content: " "; - background-size: 75%; - filter: brightness(1.3); - background-position: center; - background-repeat: no-repeat; - transition: 0.15s ease-in-out; - background-color: var(--selbg); - margin-right: calc(var(--spacing) / 3); - border-radius: calc(var(--spacing) / 4); -} - -.check.checked::before { - background-color: rgb(var(--red)); - background-image: url(icons/check.png); -} - -.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, .option .actions select { - 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)); -} - +/* window buttons { */ #winbtns { z-index: 2; display: flex; @@ -359,11 +59,13 @@ button { #winbtns #minimize {background-image: url("icons/minimize.png")} #winbtns #settings {background-image: url("icons/settings.png")} +#winbtns #settings:hover { + transform: rotate(90deg); +} + #winbtns div:hover {opacity: 1.0} #winbtns div:active {transform: scale(0.95)} - -button:hover {filter: brightness(110%)} -button:active {filter: brightness(90%)} +/* } */ img {pointer-events: none} @@ -377,7 +79,7 @@ img {pointer-events: none} background-size: cover; background-position: center; background-repeat: no-repeat; - transition: background-image 0.5s ease-in-out; + transition: background-image 0.1s ease-in-out; filter: brightness(0.4) blur(2px) grayscale(0.6); } @@ -385,412 +87,7 @@ img {pointer-events: none} #bgHolder[bg="1"] {background-image: url("../assets/bg/northstar.jpg")} #bgHolder[bg="2"] {background-image: url("../assets/bg/tf2.jpg")} -.gamesContainer { - width: 10%; - height: 100%; - min-width: 95px; - max-width: 120px; - - float: left; - display: flex; - flex-wrap: wrap; - align-content: center; -} - -.mainContainer { - height: 100%; - flex-grow: 1; - display: flex; - position: relative; -} - - -/* nav bar buttons */ -.gamesContainer button { - background-size: 90%; - background-position: center; - background-repeat: no-repeat; - - border: none; - transition: 0.3s ease-in-out; - background-color: transparent; - - - margin: 20px; - position: relative; - box-sizing: border-box; - flex-basis: calc(100% - 10px); -} - -.gamesContainer button.inactive { - opacity: 0.5; - transform: scale(0.9); -} - -.gamesContainer button::before { - content: ""; - display: block; - padding-top: 100%; -} - -.gamesContainer button .content { - width: 100%; - height: 100%; - top: 0; left: 0; - position: absolute; -} - -#vpBtn {background-image: url("icons/viper.png")} -#nsBtn {background-image: url("icons/northstar.png")} -#tfBtn { - background-image: url("icons/titanfall2.png"); - background-size: 69%; /* nice */ -} - -.contentContainer { - width: 85%; - color: white; - flex-grow: 1; - opacity: 1.0; - margin-left: 5%; - position: absolute; - transition: 0.15s ease-in-out; -} - -.contentContainer.hidden { - opacity: 0.0; - pointer-events: none; -} - -.contentMenu { - padding: 0; - flex-grow: 1; - display: flex; - font-size: 20px; - list-style: none; - margin-bottom: 0; - align-items: center; - justify-content: center; - margin-top: var(--padding); -} - -/* some sections do not need space between them and menu */ -#nsMain, #nsRelease, #vpReleaseNotes, .playBtnContainer { - margin-top: 0 !important; -} - -.contentMenu li { - opacity: 0.6; - margin: 0 26px; - cursor: pointer; - font-weight: 700; - text-align: center; - height: fit-content; - transition: opacity 0.3s ease-in-out; -} - -.contentMenu li:last-child {margin-right: 0px} -.contentMenu li:first-child {margin-left: 0px} - -.contentMenu li:hover {opacity: 0.7} - -.contentMenu li[active] { - opacity: 1.0; - pointer-events: none; -} - -.contentMenu li::after { - top: 10px; - width: 30px; - height: 5px; - opacity: 0.0; - content: " "; - display: block; - text-align: center; - position: relative; - border-radius: 50px; - background: rgb(var(--red)); - left: calc(50% - 15px); - transition: 0.2s ease-in-out; -} - -.contentMenu li[active]::after { - top: 5px; - opacity: 1.0; -} - -.section { - opacity: 1.0; - position: fixed; - right: calc(var(--padding) * 2); - left: calc(100px + var(--padding)); - transition: opacity 0.15s ease-in-out; -} - -.section.hidden { - opacity: 0.0; - pointer-events: none; -} - -.contentBody img {max-width: 100%} -.contentBody .img {text-align: center} - -.contentBody .section > :first-child:not(.img) { - margin-top: 35px; -} - -.contentContainer .playBtnContainer { - text-align: center; -} - -.contentContainer .playBtn { - width: 27%; - height: 11%; - border: none; - color: white; - padding: 20px; - font-size: 24px; - font-weight: bold; - margin-top: 100px; - margin-bottom: 10px; - border-radius: 10px; - background: var(--redbg); - transition: 0.3s ease-in-out; - filter: drop-shadow(0px 8px 5px rgba(0, 0, 0, 0.1)); -} - -.contentContainer .playBtn:hover { - transform: scale(1.05); - filter: drop-shadow(0px 5px 15px rgba(0, 0, 0, 0.3)) brightness(110%); -} - -.contentContainer .playBtn:active { - opacity: 0.7;transform: scale(0.98); - filter: drop-shadow(0px 5px 10px rgba(0, 0, 0, 0.4)); -} - -.contentContainer #nsMain .playBtn { - background: var(--bluebg); -} - -a { - color: rgb(var(--red)); - text-decoration: none; - transition: filter 0.2s ease-in; -} - -a:hover { - filter: brightness(80%); -} - -#nsContent .contentMenu { - margin-bottom: 0; -} - -.contentBody .img img { - transform: scale(0.85); -} - -.contentBody .img { - width: 100%; - text-align: center; -} - -#nsRelease, #vpReleaseNotes { - height: 80vh; - overflow-y: scroll; - flex-direction: column; - margin-top: 10px !important; -} - -.inline * { - display: inline-block; -} - -#vpMain { - margin-top: 140px; - text-align: center; -} - -#vpMain img { - width: 20%; -} - -#vpVersion { - font-size: 16px; -} - -.simplebar-scrollbar:before { - background: rgb(var(--red)) !important; -} - -#installmod {background: rgb(var(--blue))} -#togglemod, #toggleall {background: rgb(var(--orange))} -#removeall, #removemod {background: rgb(var(--red))} -button:disabled { - opacity: 0.5; - pointer-events: none; -} - -button.visual { - opacity: 1.0; - pointer-events: none; - background: transparent !important; -} - -code { - font-size: 16px; - padding: 2px 5px; - border-radius: 3px; - background-color: #00000070; -} - -#nsMods .line { - display: flex; - align-items: center; - margin-top: calc(var(--padding) / 2); -} - -#modsdiv { - height: 50vh; - overflow-y: scroll; - border-radius: 5px; - background: var(--bg); - backdrop-filter: blur(15px); - padding: calc(var(--padding) / 4); -} - -#modsdiv .mod { - display: flex; - border-radius: 5px; - transition: 0.1s ease-in-out; - margin: calc(var(--padding) / 3); - padding: calc(var(--padding) / 3); -} - -#modsdiv .mod.selected { - background: var(--selbg); -} - -#modsdiv .mod .disabled, .modbtns { - margin-left: auto; -} - -.modbtns button { - 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; -} - -#dragUI { - top: 0; - left: 0; - right: 0; - bottom: 0; - color: white; - opacity: 0.0; - position: fixed; - z-index: 1000000; - pointer-events: none; - background: var(--bg); - backdrop-filter: blur(15px); - transition: 0.1s ease-in-out; -} - -#dragUI.shown { - opacity: 1.0; - pointer-events: all; -} - -#dragUI #dragitems { - --size: 25vw; - top: 50%; - left: 50%; - opacity: 0.6; - position: absolute; - text-align: center; - width: var(--size); - height: var(--size); - margin-top: calc(var(--size) / 2 * -1); - margin-left: calc(var(--size) / 2 * -1); -} - -#dragUI #dragitems #icon { - width: 100%; - height: 100%; - filter: invert(1); - transform: scale(0.45); - background-size: cover; - background-image: url("icons/download.png"); - transition: 0.1s ease-in-out; -} - -#dragUI.shown #dragitems #icon { - transform: scale(0.5); -} - -#dragUI #dragitems #text { - top: -5vw; - position: relative; -} - /* drag control */ - #bgHolder, .contentContainer, .gamesContainer { @@ -804,6 +101,7 @@ code { -webkit-app-region: no-drag; } -a, button, #close, #nsRelease, #vpReleaseNotes, .mod, #overlay, #modsdiv, #winbtns, .contentMenu { +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 4918777..ad1a44f 100644 --- a/src/app/main.js +++ b/src/app/main.js @@ -1,6 +1,6 @@ const fs = require("fs"); const path = require("path"); -const { ipcRenderer, shell } = require("electron"); +const { ipcRenderer, shell, ipcMain } = require("electron"); const lang = require("../lang"); var modsobj = {}; @@ -15,6 +15,7 @@ var settings = { autolang: true, forcedlang: "en", autoupdate: true, + originkill: false, zip: "/northstar.zip", lang: navigator.language, winebin: "/usr/bin/wine64", @@ -29,7 +30,24 @@ ipcRenderer.send("setlang", settings.lang); // Loads the settings if (fs.existsSync("viper.json")) { - settings = {...settings, ...JSON.parse(fs.readFileSync("viper.json", "utf8"))}; + let conf = fs.readFileSync("viper.json", "utf8"); + let json = {}; + + // Validates viper.json + try { + json = JSON.parse(conf); + }catch (e) { + let reset = confirm(lang("general.invalidconfig", navigator.language) + e); + if (! reset) { + ipcRenderer.send("exit"); + } else { + fs.writeFileSync("viper.json", "{}"); + ipcRenderer.send("relaunch"); + } + + } + + settings = {...settings, ...json}; settings.zip = path.join(settings.gamepath + "/northstar.zip"); if (settings.gamepath.length === 0) { @@ -46,8 +64,14 @@ if (fs.existsSync("viper.json")) { setpath(); } + +// Show a toast message if no Internet connection has been detected. +if (!navigator.onLine) { + ipcRenderer.send("no-internet"); +} + function exit() {ipcRenderer.send("exit")} -function update() {ipcRenderer.send("update")} +function updateNorthstar() {ipcRenderer.send("update-northstar")} // Reports to the main process about game path status. // @param {boolean} value is game path loaded @@ -58,15 +82,15 @@ function setpath(value = false) { // Tells the main process to launch or install Northstar function launch() { if (shouldInstallNorthstar) { - update(); + updateNorthstar(); shouldInstallNorthstar = false; } else { - ipcRenderer.send("launch"); + ipcRenderer.send("launch-ns"); } } // Tells the main process to launch the vanilla game -function launchVanilla() {ipcRenderer.send("launchVanilla")} +function launchVanilla() {ipcRenderer.send("launch-vanilla")} // In conjunction with utils.js' winLog(), it'll send log messages in // the devTools from utils.js @@ -85,13 +109,11 @@ function setButtons(state) { } } - disablearray(document.querySelectorAll(".playBtnContainer .playBtn")) - disablearray(document.querySelectorAll("#nsMods .buttons.modbtns button")) - disablearray(document.querySelectorAll("#browser #browserEntries .text button")) + disablearray(document.querySelectorAll(".playBtnContainer .playBtn")); + disablearray(document.querySelectorAll("#nsMods .buttons.modbtns button")); + disablearray(document.querySelectorAll("#browser #browserEntries .text button")); } -ipcRenderer.on("setbuttons", (event, state) => {setButtons(state)}) - ipcRenderer.on("gamestate", (event, state) => { setButtons(! state); @@ -105,7 +127,11 @@ ipcRenderer.on("gamestate", (event, state) => { btns[1].innerHTML = string; }) -ipcRenderer.on("gamepathlost", (event, state) => { +ipcRenderer.on("set-buttons", (event, state) => { + setButtons(state); +}) + +ipcRenderer.on("gamepath-lost", (event, state) => { page(0); setButtons(false); alert(lang("gui.gamepath.lost")); @@ -117,6 +143,7 @@ ipcRenderer.on("ns-update-event", (event, key) => { console.log(lang(key)); switch(key) { case "cli.update.uptodate.short": + case "cli.update.noInternet": setButtons(true); playNsBtn.innerText = lang("gui.launch"); break; @@ -171,7 +198,7 @@ function selected(all) { } } - ipcRenderer.send("removemod", selected) + ipcRenderer.send("remove-mod", selected); }, toggle: () => { if (selected.match(/^Northstar\./)) { @@ -184,33 +211,82 @@ function selected(all) { } } - ipcRenderer.send("togglemod", selected) + ipcRenderer.send("toggle-mod", selected); } } } +let installqueue = []; + // Tells the main process to install a mod through the file selector function installmod() { setButtons(false); - ipcRenderer.send("installmod") + ipcRenderer.send("install-mod"); } // Tells the main process to directly install a mod from this path function installFromPath(path) { setButtons(false); - ipcRenderer.send("installfrompath", path) + ipcRenderer.send("install-from-path", path); } // Tells the main process to install a mod from a URL -function installFromURL(url) { +function installFromURL(url, dependencies, clearqueue) { + if (clearqueue) {installqueue = []}; + + let prettydepends = []; + + if (dependencies) { + let newdepends = []; + for (let i = 0; i < dependencies.length; i++) { + let depend = dependencies[i].toLowerCase(); + if (! depend.match(/northstar-northstar-.*/)) { + depend = dependencies[i].replaceAll("-", "/"); + let pkg = depend.split("/"); + if (! isModInstalled(pkg[1])) { + newdepends.push(depend); + prettydepends.push(`${pkg[1]} v${pkg[2]} - ${lang("gui.browser.madeby")} ${pkg[0]}`); + } + } + } + + dependencies = newdepends; + } + + if (dependencies && dependencies.length != 0) { + let confirminstall = confirm(lang("gui.mods.confirmdependencies") + prettydepends.join("\n")); + if (! confirminstall) { + return + } + } + setButtons(false); - ipcRenderer.send("installfromurl", url) + ipcRenderer.send("install-from-url", url, dependencies); + + if (dependencies) { + installqueue = dependencies; + } +} + +function isModInstalled(modname) { + for (let i = 0; i < modsobj.all.length; i++) { + let mod = modsobj.all[i]; + if (mod.ManifestName) { + if (mod.ManifestName.match(modname)) { + return true; + } + } else if (mod.Name.match(modname)) { + return true; + } + } + + return false; } // Frontend part of settings a new game path ipcRenderer.on("newpath", (event, newpath) => { settings.gamepath = newpath; - ipcRenderer.send("guigetmods"); + ipcRenderer.send("gui-getmods"); }) // Continuation of log() @@ -258,23 +334,23 @@ ipcRenderer.on("version", (event, versions) => { shouldInstallNorthstar = true; playNsBtn.innerText = lang("gui.installnorthstar"); } -}); ipcRenderer.send("getversion"); +}); ipcRenderer.send("get-version"); // When an update is available it'll ask the user about it -ipcRenderer.on("updateavailable", () => { +ipcRenderer.on("update-available", () => { if (confirm(lang("gui.update.available"))) { - ipcRenderer.send("updatenow"); + ipcRenderer.send("update-now"); } }) // Error out when no game path is set -ipcRenderer.on("nopathselected", () => { +ipcRenderer.on("no-path-selected", () => { alert(lang("gui.gamepath.must")); exit(); }); // Error out when game path is wrong -ipcRenderer.on("wrongpath", () => { +ipcRenderer.on("wrong-path", () => { alert(lang("gui.gamepath.wrong")); setpath(false); }); @@ -303,7 +379,7 @@ document.addEventListener("drop", (e) => { event.stopPropagation(); dragUI.classList.remove("shown"); - installFromPath(event.dataTransfer.files[0].path) + installFromPath(event.dataTransfer.files[0].path); }); document.body.addEventListener("keyup", (e) => { diff --git a/src/app/settings.js b/src/app/settings.js index 96d9379..34975fe 100644 --- a/src/app/settings.js +++ b/src/app/settings.js @@ -3,26 +3,26 @@ var Settings = { if (state) { Settings.load(); options.scrollTo(0, 0); - overlay.classList.add("shown") - options.classList.add("shown") + overlay.classList.add("shown"); + options.classList.add("shown"); return } else if (! state) { if (state != undefined) { - overlay.classList.remove("shown") - options.classList.remove("shown") + overlay.classList.remove("shown"); + options.classList.remove("shown"); return } } Settings.load(); options.scrollTo(0, 0); - overlay.classList.toggle("shown") - options.classList.toggle("shown") + overlay.classList.toggle("shown"); + options.classList.toggle("shown"); }, apply: () => { settings = {...settings, ...Settings.get()}; - ipcRenderer.send("savesettings", Settings.get()); + ipcRenderer.send("save-settings", Settings.get()); }, reloadSwitches: () => { let switches = document.querySelectorAll(".switch"); @@ -18,11 +18,11 @@ function hasArgs() { cli.hasSwitch("launch") || cli.hasSwitch("setpath") || cli.hasSwitch("version") || - cli.hasSwitch("updatevp") || cli.hasSwitch("gamepath") || cli.hasSwitch("togglemod") || cli.hasSwitch("removemod") || - cli.hasSwitch("installmod")) { + cli.hasSwitch("installmod") || + cli.hasSwitch("update-viper")) { return true; } else {return false} } @@ -36,7 +36,7 @@ function exit(code) { // Ensures the gamepath exists, it's called by options that require the // gamepath to be able to work. -function gamepath() { +function gamepathExists() { if (fs.existsSync("viper.json")) { gamepath = JSON.parse(fs.readFileSync("viper.json", "utf8")).gamepath; @@ -64,7 +64,7 @@ async function init() { --cli ${lang("cli.help.cli")} --update ${lang("cli.help.update")} - --updatevp ${lang("cli.help.updatevp")} + --update-viper ${lang("cli.help.updatevp")} --setpath ${lang("cli.help.setpath")} --no-vp-updates ${lang("cli.help.novpupdates")} @@ -77,9 +77,9 @@ async function init() { } // --update - if (gamepath() && cli.hasSwitch("update")) {ipcMain.emit("update")} + if (cli.hasSwitch("update") && gamepathExists()) {ipcMain.emit("update")} // --version - if (gamepath() && cli.hasSwitch("version")) {ipcMain.emit("versioncli")} + if (cli.hasSwitch("version") && gamepathExists()) {ipcMain.emit("versioncli")} // --setpath if (cli.hasSwitch("setpath")) { @@ -93,7 +93,7 @@ async function init() { } // --launch - if (gamepath() && cli.hasSwitch("launch")) { + if (gamepathExists() && cli.hasSwitch("launch")) { switch(cli.getSwitchValue("launch")) { case "vanilla": ipcMain.emit("launchVanilla"); @@ -105,12 +105,12 @@ async function init() { } // Mod related args, --installmod, --removemod, --togglemod - if (gamepath() && cli.hasSwitch("installmod")) {ipcMain.emit("installmod")} - if (gamepath() && cli.hasSwitch("removemod")) {ipcMain.emit("removemod", "", cli.getSwitchValue("removemod"))} - if (gamepath() && cli.hasSwitch("togglemod")) {ipcMain.emit("togglemod", "", cli.getSwitchValue("togglemod"))} + if (cli.hasSwitch("installmod") && gamepathExists()) {ipcMain.emit("installmod")} + if (cli.hasSwitch("removemod") && gamepathExists()) {ipcMain.emit("removemod", "", cli.getSwitchValue("removemod"))} + if (cli.hasSwitch("togglemod") && gamepathExists()) {ipcMain.emit("togglemod", "", cli.getSwitchValue("togglemod"))} // Prints out the list of mods - if (gamepath() && cli.hasSwitch("mods")) {ipcMain.emit("getmods")} + if (cli.hasSwitch("mods") && gamepathExists()) {ipcMain.emit("getmods")} } module.exports = { diff --git a/src/extras/findgame.js b/src/extras/findgame.js new file mode 100644 index 0000000..615c5b4 --- /dev/null +++ b/src/extras/findgame.js @@ -0,0 +1,85 @@ +const fs = require("fs"); +const path = require("path"); +const vdf = require("simple-vdf"); +const { app } = require("electron"); + +const util = require("util"); +const exec = util.promisify(require("child_process").exec); + +module.exports = async () => { + let gamepath = ""; + + // Autodetect path + // Windows only using powershell and windows registery + // Get-Item -Path Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Respawn\Titanfall2\ + if (process.platform == "win32") { + try { + const {stdout} = await exec("Get-ItemProperty -Path Registry::HKEY_LOCAL_MACHINE\\SOFTWARE\\Respawn\\Titanfall2\\ -Name \"Install Dir\"", {"shell":"powershell.exe"}); + + const gamepath = stdout.split('\n') + .filter(r => r.indexOf("Install Dir") !== -1)[0] + .replace(/\s+/g,' ') + .trim() + .replace("Install Dir : ",""); + + if (gamepath) {return gamepath} + } catch (err) {} + } + + // Detect using Steam VDF + function readvdf(data) { + // Parse read_data + data = vdf.parse(data); + + let values = Object.values(data["libraryfolders"]); + if (typeof values[values.length - 1] != "object") { + values.pop(1); + } + + // `.length - 1` This is because the last value is `contentstatsid` + for (let i = 0; i < values.length; i++) { + let data_array = Object.values(values[i]); + + if (fs.existsSync(data_array[0] + "/steamapps/common/Titanfall2/Titanfall2.exe")) { + console.log("Found game in:", data_array[0]); + return data_array[0] + "/steamapps/common/Titanfall2"; + } else { + console.log("Game not in:", data_array[0]); + } + } + } + + let folders = []; + switch (process.platform) { + case "win32": + folders = ["C:\\Program Files (x86)\\Steam\\steamapps\\libraryfolders.vdf"]; + break + case "linux": + case "openbsd": + case "freebsd": + let home = app.getPath("home"); + folders = [ + path.join(home, "/.steam/steam/steamapps/libraryfolders.vdf"), + path.join(home, ".var/app/com.valvesoftware.Steam/.steam/steam/steamapps/libraryfolders.vdf"), + path.join(home, ".var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/libraryfolders.vdf") + ] + break + } + + if (folders.length > 0) { + for (let i = 0; i < folders.length; i++) { + if (! fs.existsSync(folders[i])) {continue} + console.log("Searching VDF file at:", folders[i]); + + let data = fs.readFileSync(folders[i]); + let read_vdf = readvdf(data.toString()); + if (read_vdf) {return read_vdf} + } + } + + if (gamepath) { + return gamepath; + } else { + return false; + } +} diff --git a/src/extras/requests.js b/src/extras/requests.js index 16b1330..a5f3121 100644 --- a/src/extras/requests.js +++ b/src/extras/requests.js @@ -2,10 +2,11 @@ const { app } = require("electron"); const path = require("path"); const fs = require("fs"); const { https } = require("follow-redirects"); +const lang = require("../lang"); // all requests results are stored in this file -const cachePath = path.join(app.getPath("cache"), "requests.json"); +const cachePath = path.join(app.getPath("cache"), "viper-requests.json"); const NORTHSTAR_LATEST_RELEASE_KEY = "nsLatestRelease"; const NORTHSTAR_RELEASE_NOTES_KEY = "nsReleaseNotes"; const VIPER_RELEASE_NOTES_KEY = "vpReleaseNotes"; @@ -58,6 +59,11 @@ async function getLatestNsVersion() { _saveCache(cache); resolve( cache[NORTHSTAR_LATEST_RELEASE_KEY]["body"]["tag_name"] ); }); + }) + + .on('error', () => { + console.error('Failed to get latest Northstar version.'); + resolve( false ); }); } }); @@ -103,6 +109,20 @@ async function getNsReleaseNotes() { _saveCache(cache); resolve( cache[NORTHSTAR_RELEASE_NOTES_KEY]["body"] ); }); + }) + + // When GitHub cannot be reached (when user doesn't have Internet + // access for instance), we return latest cache content even if + // it's not up-to-date, or display an error message if cache + // is empty. + .on('error', () => { + if ( cache[NORTHSTAR_RELEASE_NOTES_KEY] ) { + console.warn("Couldn't fetch Northstar release notes, returning data from cache."); + resolve( cache[NORTHSTAR_RELEASE_NOTES_KEY]["body"] ); + } else { + console.error("Couldn't fetch Northstar release notes, cache is empty."); + resolve( [lang("request.northstar.noReleaseNotes")] ); + } }); } }); @@ -140,6 +160,20 @@ async function getVpReleaseNotes() { _saveCache(cache); resolve( cache[VIPER_RELEASE_NOTES_KEY]["body"] ); }); + }) + + // When GitHub cannot be reached (when user doesn't have Internet + // access for instance), we return latest cache content even if + // it's not up-to-date, or display an error message if cache + // is empty. + .on('error', () => { + if ( cache[VIPER_RELEASE_NOTES_KEY] ) { + console.warn("Couldn't fetch Viper release notes, returning data from cache."); + resolve( cache[VIPER_RELEASE_NOTES_KEY]["body"] ); + } else { + console.error("Couldn't fetch Viper release notes, cache is empty."); + resolve( [lang("request.viper.noReleaseNotes")] ); + } }); } }); diff --git a/src/index.js b/src/index.js index d854ce8..879dbcf 100644 --- a/src/index.js +++ b/src/index.js @@ -3,125 +3,155 @@ const path = require("path"); const { autoUpdater } = require("electron-updater"); const { app, ipcMain, BrowserWindow, dialog } = require("electron"); -const Emitter = require("events"); -const events = new Emitter(); - const utils = require("./utils"); const cli = require("./cli"); const requests = require("./extras/requests"); +var log = console.log; + // Starts the actual BrowserWindow, which is only run when using the // GUI, for the CLI this function is never called. function start() { win = new BrowserWindow({ width: 1000, height: 600, + title: "Viper", // Hides the window initially, it'll be shown when the DOM is // loaded, as to not cause visual issues. show: false, - title: "Viper", // In the future we may want to allow the user to resize the window, // as it's fairly responsive, but for now we won't allow that. resizable: false, - titleBarStyle: "hidden", + frame: false, + titleBarStyle: "hidden", icon: path.join(__dirname, "assets/icons/512x512.png"), webPreferences: { + webviewTag: true, nodeIntegration: true, contextIsolation: false, }, - }); + }); - // When --debug is added it'll open the dev tools + // when --debug is added it'll open the dev tools if (cli.hasParam("debug")) {win.openDevTools()} - // General setup + // general setup win.removeMenu(); win.loadFile(__dirname + "/app/index.html"); - ipcMain.on("exit", () => {process.exit(0)}) - ipcMain.on("minimize", () => {win.minimize()}) - ipcMain.on("installfrompath", (event, path) => {utils.mods.install(path)}) - ipcMain.on("installfromurl", (event, url) => {utils.mods.installFromURL(url)}) - ipcMain.on("winLog", (event, ...args) => {win.webContents.send("log", ...args)}); - ipcMain.on("winAlert", (event, ...args) => {win.webContents.send("alert", ...args)}); - ipcMain.on("ns-update-event", (event) => win.webContents.send("ns-update-event", event)); - ipcMain.on("failedmod", (event, modname) => {win.webContents.send("failedmod", modname)}); - ipcMain.on("removedmod", (event, modname) => {win.webContents.send("removedmod", modname)}); - ipcMain.on("installedmod", (event, modname) => {win.webContents.send("installedmod", modname)}); - ipcMain.on("guigetmods", (event, ...args) => {win.webContents.send("mods", utils.mods.list())}); + win.send = (channel, data) => { + win.webContents.send(channel, data); + }; send = win.send; + + ipcMain.on("exit", () => { + if (utils.settings.originkill) { + utils.isOriginRunning().then((running) => { + if (running) { + utils.killOrigin().then(process.exit(0)) + } else { + process.exit(0) + } + }) + } else { + process.exit(0) + } + }); ipcMain.on("gamestarted", (event) => {win.webContents.send("gamestate", true)}); ipcMain.on("gamestopped", (event) => {win.webContents.send("gamestate", false)}); + ipcMain.on("minimize", () => {win.minimize()}); + ipcMain.on("relaunch", () => {app.relaunch(); app.exit()}); + + // passthrough to renderer from main + ipcMain.on("win-log", (event, ...args) => {send("log", ...args)}); + ipcMain.on("win-alert", (event, ...args) => {send("alert", ...args)}); + + // mod states + ipcMain.on("failed-mod", (event, modname) => {send("failed-mod", modname)}); + ipcMain.on("removed-mod", (event, modname) => {send("removed-mod", modname)}); + ipcMain.on("gui-getmods", (event, ...args) => {send("mods", utils.mods.list())}); + ipcMain.on("installed-mod", (event, modname) => {send("installed-mod", modname)}); + ipcMain.on("no-internet", () => {send("no-internet")}); + + // install calls + ipcMain.on("install-from-path", (event, path) => {utils.mods.install(path)}); + ipcMain.on("install-from-url", (event, url) => {utils.mods.installFromURL(url)}); + + win.webContents.on("dom-ready", () => { + send("mods", utils.mods.list()); + }); + + // ensures gamepath still exists and is valid on startup let gamepathlost = false; - ipcMain.on("gamepathlost", (event, ...args) => { + ipcMain.on("gamepath-lost", (event, ...args) => { if (! gamepathlost) { gamepathlost = true; - win.webContents.send("gamepathlost"); + send("gamepath-lost"); } }); - ipcMain.on("savesettings", (event, obj) => {utils.saveSettings(obj)}) + ipcMain.on("save-settings", (event, obj) => {utils.saveSettings(obj)}); - ipcMain.on("can-autoupdate", (event) => { - if (! require("electron-updater").autoUpdater.isUpdaterActive() || cli.hasParam("no-vp-updates")) { - win.webContents.send("cant-autoupdate") + // allows renderer to check for updates + ipcMain.on("ns-update-event", (event) => {send("ns-update-event", event)}); + ipcMain.on("can-autoupdate", () => { + if (! autoUpdater.isUpdaterActive() || cli.hasParam("no-vp-updates")) { + send("cant-autoupdate"); } }) - win.webContents.on("dom-ready", () => { - win.webContents.send("mods", utils.mods.list()); - }); - + // start auto-update process if (utils.settings.autoupdate) { if (cli.hasParam("no-vp-updates")) { utils.handleNorthstarUpdating(); } else { - utils.updatevp(false) + utils.updateViper(false) } } else { utils.handleNorthstarUpdating(); } autoUpdater.on("update-downloaded", () => { - win.webContents.send("updateavailable") + send("update-available"); }); - // Updates and restarts Viper, if user says yes to do so. - // Otherwise it'll do it on the next start up. - ipcMain.on("updatenow", () => { + // updates and restarts Viper, if user says yes to do so. + // otherwise it'll do it on the next start up. + ipcMain.on("update-now", () => { autoUpdater.quitAndInstall(); }) } // General events used to handle utils.js stuff without requiring the // module inside the file that sent the event. { -ipcMain.on("installmod", () => { +ipcMain.on("install-mod", () => { if (cli.hasArgs()) { - utils.mods.install(cli.param("installmod")) + utils.mods.install(cli.param("installmod")); } else { dialog.showOpenDialog({properties: ["openFile"]}).then(res => { if (res.filePaths.length != 0) { utils.mods.install(res.filePaths[0]); } else { - win.webContents.send("setbuttons", true); + send("set-buttons", true); } - }).catch(err => {console.error(err)}) + }).catch(err => {error(err)}); } }) -ipcMain.on("removemod", (event, mod) => {utils.mods.remove(mod)}) -ipcMain.on("togglemod", (event, mod) => {utils.mods.toggle(mod)}) +ipcMain.on("remove-mod", (event, mod) => {utils.mods.remove(mod)}); +ipcMain.on("toggle-mod", (event, mod) => {utils.mods.toggle(mod)}); + +ipcMain.on("launch-ns", () => {utils.launch()}); +ipcMain.on("launch-vanilla", () => {utils.launch("vanilla")}); -ipcMain.on("launch", (event) => {utils.launch()}) -ipcMain.on("setlang", (event, lang) => {utils.setlang(lang)}) -ipcMain.on("launchVanilla", (event) => {utils.launch("vanilla")}) +ipcMain.on("setlang", (event, lang) => {utils.setlang(lang)}); -ipcMain.on("update", (event) => {utils.update()}) -ipcMain.on("setpathcli", (event) => {utils.setpath()}); +ipcMain.on("update-northstar", () => {utils.updateNorthstar()}) +ipcMain.on("setpath-cli", () => {utils.setpath()}); ipcMain.on("setpath", (event, value) => { if (! value) { if (! win.isVisible()) { @@ -134,69 +164,72 @@ ipcMain.on("setpath", (event, value) => { } }); -function _sendVersionsInfo() { - win.webContents.send("version", { +// retrieves various local version numbers +function sendVersionsInfo() { + send("version", { ns: utils.getNSVersion(), tf2: utils.getTF2Version(), vp: "v" + require("../package.json").version }); } -// Sends the version info back to the renderer -ipcMain.on("getversion", () => {_sendVersionsInfo()}); +// sends the version info back to the renderer +ipcMain.on("get-version", () => {sendVersionsInfo()}); -// Prints out version info for the CLI -ipcMain.on("versioncli", () => { - console.log("Viper: v" + require("../package.json").version); - console.log("Northstar: " + utils.getNSVersion()); - console.log("Node: " + process.version); - console.log("Electron: v" + process.versions.electron); +// prints out version info for the CLI +ipcMain.on("version-cli", () => { + log("Viper: v" + require("../package.json").version); + log("Northstar: " + utils.getNSVersion()); + log("Node: " + process.version); + log("Electron: v" + process.versions.electron); cli.exit(); }) -ipcMain.on("getmods", (event) => { +// sends installed mods info to renderer +ipcMain.on("getmods", () => { let mods = utils.mods.list(); if (mods.all.length > 0) { - console.log(`${utils.lang("general.mods.installed")} ${mods.all.length}`) - console.log(`${utils.lang("general.mods.enabled")} ${mods.enabled.length}`) + log(`${utils.lang("general.mods.installed")} ${mods.all.length}`); + log(`${utils.lang("general.mods.enabled")} ${mods.enabled.length}`); for (let i = 0; i < mods.enabled.length; i++) { - console.log(` ${mods.enabled[i].Name} ${mods.enabled[i].Version}`) + log(` ${mods.enabled[i].Name} ${mods.enabled[i].Version}`); } if (mods.disabled.length > 0) { - console.log(`${utils.lang("general.mods.disabled")} ${mods.disabled.length}`) + log(`${utils.lang("general.mods.disabled")} ${mods.disabled.length}`); for (let i = 0; i < mods.disabled.length; i++) { - console.log(` ${mods.disabled[i].Name} ${mods.disabled[i].Version}`) + log(` ${mods.disabled[i].Name} ${mods.disabled[i].Version}`); } } cli.exit(0); } else { - console.log("No mods installed"); + log("No mods installed"); cli.exit(0); } }) // } +// allows renderer to set a new renderer ipcMain.on("newpath", (event, newpath) => { - if (newpath === false && !win.isVisible()) { - win.webContents.send("nopathselected"); + if (newpath === false && ! win.isVisible()) { + win.send("no-path-selected"); } else { - _sendVersionsInfo(); + sendVersionsInfo(); if (!win.isVisible()) { win.show(); } } -}); ipcMain.on("wrongpath", (event) => { - win.webContents.send("wrongpath"); +}); ipcMain.on("wrong-path", () => { + win.send("wrong-path"); }); -// Ensures ./ is the config folder where viper.json is located. +// ensures PWD/CWD is the config folder where viper.json is located process.chdir(app.getPath("appData")); -// Starts the GUI or CLI +// starts the GUI or CLI if (cli.hasArgs()) { - if (cli.hasParam("updatevp")) { - utils.updatevp(true); + if (cli.hasParam("update-viper")) { + utils.updateViper(true); } else { cli.init(); } @@ -207,11 +240,11 @@ if (cli.hasArgs()) { }) } -// Returns cached requests +// returns cached requests ipcMain.on("get-ns-notes", async () => { - win.webContents.send("ns-notes", await requests.getNsReleaseNotes()); + win.send("ns-notes", await requests.getNsReleaseNotes()); }); ipcMain.on("get-vp-notes", async () => { - win.webContents.send("vp-notes", await requests.getVpReleaseNotes()); + win.send("vp-notes", await requests.getVpReleaseNotes()); }); diff --git a/src/lang.js b/src/lang.js index 041ef35..f2fab3a 100644 --- a/src/lang.js +++ b/src/lang.js @@ -5,13 +5,24 @@ let lang = ""; var langObj = {}; -function _loadTranslation() { +function _loadTranslation(forcedlang) { if (fs.existsSync("viper.json")) { - opts = JSON.parse(fs.readFileSync("viper.json", "utf8")); + // Validate viper.json + let opts = { + lang: "en", + autolang: true, + } + + try { + opts = JSON.parse(fs.readFileSync("viper.json", "utf8")); + }catch (e) {} + lang = opts.lang; if (! lang) {lang = "en"} + if (forcedlang) {lang = forcedlang} + if (opts.autolang == false) { lang = opts.forcedlang; if (! lang) {lang = "en"} @@ -32,9 +43,14 @@ function _loadTranslation() { } -module.exports = (string) => { - if (lang === "") +module.exports = (string, forcedlang) => { + if (lang === "") { _loadTranslation(); + } + + if (forcedlang) { + _loadTranslation(forcedlang); + } if (langObj[string]) { return langObj[string]; diff --git a/src/lang/de.json b/src/lang/de.json new file mode 100644 index 0000000..f8f2d63 --- /dev/null +++ b/src/lang/de.json @@ -0,0 +1,167 @@ +{ + "lang.title": "German - Deutsch", + + "cli.help.help": "Zeigt die Hilfe Nachricht an.", + "cli.help.debug": "Öffnet Entwickler/Debug Werkzeuge.", + "cli.help.version": "Gibt die Versions Informationen aus.", + "cli.help.cli": "Zwingt die CLI Einstellung auf \"an\".", + "cli.help.update": "Aktualisiert den Installationspfad von Northstar, durch den gegeben Installationspfad von Titanfall 2.", + "cli.help.setpath": "Setzt den Installationspfad von Titanfall 2.", + "cli.help.updatevp": "Aktualisiert Viper, falls dies unterstützt wird.", + "cli.help.novpupdates": "Überschreibt viper.json und deaktiviert das Aktualisieren von Viper.", + "cli.help.installmod": "Installiert einen Mod, eine ZIP-Datei oder einen Ordner.", + "cli.help.removemod": "Entfernt einen Mod.", + "cli.help.togglemod": "Aktiviert/Deaktiviert einen Mod.", + + "cli.setpath.noarg": "Keine Argument für --setpath wurden angegeben!", + + "cli.update.current": "Jetzige Version:", + "cli.update.downloading": "Wird heruntergeladen...", + "cli.update.checking": "Überprüfe auf Updates...", + "cli.update.downloaddone": "Herunterladen abgeschlossen! Extrahiere...", + "cli.update.finished": "Installation/Aktualisierung abeschlossen!", + "cli.update.uptodate": "Installation ist bereits auf dem neusten Stand (%s), aktualisieren wird übersprungen.", + "cli.update.uptodate.short": "Auf dem neusten stand", + "cli.update.noInternet": "Keine Internetverbindung", + + "cli.autoupdates.checking": "Überprüfe Northstar auf Updates...", + "cli.autoupdates.available": "Ein Update für Northstar wurde gefunden!", + "cli.autoupdates.gamerunning": "Spiel wird ausgeführt, Northstar wird nicht aktualisiert.", + "cli.autoupdates.updatingns": "Updateprozess wird gestartet...", + "cli.autoupdates.noupdate": "Kein Update für Northstar vorhanden.", + + "cli.launch.linuxerror": "Das Spiel starten ist derzeit nicht auf Linux unterstützt", + + "cli.gamepath.lost": "Installationspfad wurde nicht gefunden, bitte stelle sicher das er gemountet ist!", + + "cli.mods.failed": "Mod konnte nicht installiert werden!", + "cli.mods.removed": "Mod wurde erfolgreich entfernt!", + "cli.mods.toggled": "Mod wurde erfolgreich aktiviert/deaktiviert!", + "cli.mods.installed": "Mod wurde erfolgreich installiert!", + "cli.mods.cantfind": "Ein Mod mit diesem Namen konnte nicht gefunden werden!", + "cli.mods.notamod": "Die angegebene Datei/der angegebene Ordner ist kein Mod!", + "cli.mods.toggledall": "Alle Mods wurde erfolgreich aktiviert/deaktiviert!", + "cli.mods.improperjson": "Die mod.json vom Mod %s ist fehlerhaft!", + + "gui.welcome": "Willkommen zu Viper!", + "gui.versions.viper": "Viper Version", + "gui.versions.northstar": "Northstar Version", + "gui.exit": "Schließen", + "gui.update": "Aktualisieren", + "gui.setpath": "Installationspfad aktualisieren.", + + "gui.update.check": "Auf Update überprüfen", + "gui.mods": "Mods", + "gui.mods.count": "Installierte Mods:", + "gui.mods.disabledtag": "Deaktiviert", + "gui.mods.install": "Installiere den Mod", + "gui.mods.find": "Suche nach Mods", + "gui.mods.toggle": "Aktiviere/Deaktiviere den Mod", + "gui.mods.toggleall": "Aktiviere/Deaktiviere alle Mods", + "gui.mods.remove": "Entferne den Mod", + "gui.mods.removeall": "Entferne alle Mods", + "gui.mods.nothingselected": "Es wurde kein Mod ausgewählt.", + "gui.mods.toggleall.confirm": "Das Deaktivieren aller Mods kann zum Deaktiveren von Mods führen die von Northstar benötigt werden. Bist du dir sicher das du diese Aktion durchführen willst?", + "gui.mods.removeall.confirm": "Das Entfernen aller Mods führt meist dazu das eine Neuinstallation von Northstar nötig ist. Bist du dir sicher das du diese Aktion durchführen willst?", + "gui.mods.required.confirm": "Du hast einen von Northstar benötigten Mod ausgewählt, bist du dir sicher das du diese Aktion durchführen willst", + "gui.mods.notamod": "Kein Mod!", + "gui.mods.extracting": "Extrahiere den Mod...", + "gui.mods.installing": "Installiere den Mod...", + "gui.mods.installedmod": "Mod installiert!", + "gui.mods.dragdrop": "Drag and drop den Mod um ihn zu installieren!", + "gui.mods.confirmdependencies": "Dieser Mod benötigt weitere Mods, diese werden unter dieser Nachricht angezeigt. Beim drücken auf \"Ok\" stimmst du zu da diese Installiert werden.\n\n", + + "gui.browser.info": "Info", + "gui.browser.view": "Anschauen", + "gui.browser.madeby": "von", + "gui.browser.search": "Suchen...", + "gui.browser.update": "Aktualisieren", + "gui.browser.install": "Installieren", + "gui.browser.reinstall": "Neuinstallieren", + "gui.browser.loading": "Lade Mods...", + "gui.browser.loadmore": "Lade mehr...", + "gui.browser.endoflist": "Alle Packete wurden geladen.", + "gui.browser.noresults": "Keine Ergebnisse...", + "gui.browser.filter.mods": "Mods", + "gui.browser.filter.skins": "Skins", + "gui.browser.filter.client": "Client-seitig", + "gui.browser.filter.server": "Server-seitig", + + "gui.settings.save": "Speichern", + "gui.settings.discard": "Verwerfen", + "gui.settings.title.ns": "Northstar", + "gui.settings.title.language": "Sprache", + "gui.settings.title.updates": "Updates", + "gui.settings.title.misc": "Sonstiges", + "gui.settings.nsargs.title": "Startoptionen", + "gui.settings.nsargs.desc": "Hier kannst du Startoptionen für Northstar/Titanfall setzen.", + "gui.settings.autolang.title": "Automatische Spracherkennung", + "gui.settings.autolang.desc": "Beim Aktiveren versucht Viper die richtige Sprache durch deine Systemsprache zu erkennen, durch das deaktivieren ist die manuelle Auswahl aktiviert.", + "gui.settings.forcedlang.title": "Sprache", + "gui.settings.forcedlang.desc": "Wenn \"Automatische Spracherkennung\" deaktiviert ist, wird diese Option genutzt um die Sprachen zu ändern. Oft ist ein Neustart nötig!", + "gui.settings.autoupdate.title": "Viper Auto-Updates", + "gui.settings.autoupdate.desc": "Viper wird sich automatisch selbst aktualisieren!", + "gui.settings.nsupdate.title": "Northstar Auto-Updates", + "gui.settings.nsupdate.desc": "Viper wird Northstar automatisch aktualisieren, eine manuelle Aktualisierung ist trotzdem möglich.", + "gui.settings.excludes.title": "Behalte Datein beim aktualisieren.", + "gui.settings.excludes.desc": "Beim Aktualisieren von Northstar werden diese Datein nicht überschrieben. Solang du nicht weißt was du verändert solltest du diese Datein auch nicht berabeiten. Dateinamen sollte durch eine Lücke getrennt werden.", + "gui.settings.originkill.title": "Automatisch Origin schließen", + "gui.settings.originkill.desc": "When Viper sich schließt soll Origin sich auch schließen.", + + "gui.update.downloading": "Herunterladen...", + "gui.update.extracting": "Extrahiere update...", + "gui.update.finished": "Fertig! Spielbereit!", + "gui.update.uptodate": "Bereits auf dem neusten Stand!", + "gui.update.available": "Ein neues update für Viper ist verfügbar willst du Viper neustarten und diese installieren?", + + "gui.nsupdate.gaming.title": "Northstar Update verfübar!", + "gui.nsupdate.gaming.body": "Ein Update für Northstar ist verfügbar! Du kannst die Aktualisierung nach dem schließen erzwingen!", + "gui.server.player": "Spieler", + "gui.server.players": "Spieler", + "gui.server.servers": "Server", + "gui.server.offline": "Masterserver ist offline", + + "gui.launch": "Starten", + "gui.launchvanilla": "Vanilla", + "gui.launchnorthstar": "Northstar", + "gui.installnorthstar": "Installieren", + + "gui.selectpath": "Bitte wähle einen Installationspfad!", + "gui.gamepath.must": "Der Installationspfad muss gesetzt worden sein um Viper zu nutzen.", + "gui.gamepath.wrong": "Der angegebene Installationspfad ist nicht gültig!", + "gui.gamepath.lost": "Der angegeben Installationspfad ist nicht mehr gültig!\n\nBitte stelle sicher das die Festplatte mounted ist, oder falls sich der Installationspfad geändert hat, das du diesen auch in Viper aktualisierst!\n\nViper wird möglicherweiße bis zum nächsten Neustart nicht funktionieren!", + + "gui.toast.title.installed": "Mod installiert!", + "gui.toast.title.failed": "Fehler beim Installieren des Mods!", + "gui.toast.title.malformed": "Fehlerhafte Ordnerstruktur!", + "gui.toast.desc.installed": "wurde installiert!", + "gui.toast.desc.malformed": "hat eine fehlerhafte Ordnerstruktur, falls du der Entwickler bist, solltest du dies beheben.", + "gui.toast.desc.failed": "Ein unbekannter Fehler ist aufgetaucht beim Installieren, die Schuld kann beim Autor liegen oder bei Viper selbst!", + + "gui.toast.noInternet.title": "Kein Internet", + "gui.toast.noInternet.desc": "Viper funktioniert möglicherweise nicht korrekt", + + "viper.menu.main": "Viper", + "viper.menu.release": "Release Notes", + "viper.menu.info": "Extras", + "viper.menu.info.links": "Links", + "viper.menu.info.credits": "Credits", + + "viper.info.discord": "Tritt dem Discord bei:", + "viper.info.issues": "Report ein Problem mit Viper:", + + "ns.menu.main": "Northstar Launcher", + "ns.menu.mods": "Mods", + "ns.menu.release": "Release Notes", + + "general.mods.enabled": "Aktivierte Mods:", + "general.mods.disabled": "Deaktivierte Mods:", + "general.mods.installed": "Installierte Mods:", + "general.missingpath": "Installationspfad konnte nicht automatisch gefunden werden, bitte setze diesen manuell!", + "general.notinstalled": "Northstar ist nicht installiert!", + "general.launching": "Starte!", + "general.invalidconfig": "Deine Konfigurationsdatei ist nicht richtig formatiert, falls diese manuell verändert wurde bitte stelle sicher das alles korrekt ist.\n\nFalls du diese nicht manuell verändert hast, ist es empfohlen diese zurückzusetzen!\n\nUm sie zurückzusetzen drücke einfach auf \"Ok\".\n\nWeitere Informationen:\n", + + "request.viper.noReleaseNotes": "Viper Release Notes konnten nicht geladen werden.\nVersuche es erneut später!", + "request.northstar.noReleaseNotes": "Northstar Release Notes konnten nicht geladen werden.\nVersuche es erneut später!" +} diff --git a/src/lang/en.json b/src/lang/en.json index 9f2ac40..7ea0d39 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -22,6 +22,7 @@ "cli.update.finished": "Installation/Update finished!", "cli.update.uptodate": "Latest version (%s) is already installed, skipping update.", "cli.update.uptodate.short": "Up-to-date", + "cli.update.noInternet": "No Internet connection", "cli.autoupdates.checking": "Checking for Northstar updates...", "cli.autoupdates.available": "Northstar update available!", @@ -68,15 +69,18 @@ "gui.mods.installing": "Installing mod...", "gui.mods.installedmod": "Installed mod!", "gui.mods.dragdrop": "Drag and drop a mod to install", + "gui.mods.confirmdependencies": "This package has dependencies, shown below, clicking \"Ok\" will install the package and the dependencies.\n\n", "gui.browser.info": "Info", + "gui.browser.view": "View", "gui.browser.madeby": "by", "gui.browser.search": "Search...", "gui.browser.update": "Update", "gui.browser.install": "Install", "gui.browser.reinstall": "Re-Install", "gui.browser.loading": "Loading mods...", - "gui.browser.endoflist": "Maximum packages has been loaded.<br>Use the search for finding other packages!", + "gui.browser.loadmore": "Load more...", + "gui.browser.endoflist": "All packages have been loaded.", "gui.browser.noresults": "No results...", "gui.browser.filter.mods": "Mods", "gui.browser.filter.skins": "Skins", @@ -89,6 +93,7 @@ "gui.settings.title.ns": "Northstar", "gui.settings.title.language": "Language", "gui.settings.title.updates": "Updates", + "gui.settings.title.misc": "Miscellaneous", "gui.settings.nsargs.title": "Launch options", "gui.settings.nsargs.desc": "Here you can add launch options for Northstar/Titanfall.", "gui.settings.wineprefix.title": "Wine Prefix", @@ -105,6 +110,8 @@ "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.settings.originkill.title": "Automatically quit Origin", + "gui.settings.originkill.desc": "When Viper exits, automatically quit Origin as well.", "gui.update.downloading": "Downloading...", "gui.update.extracting": "Extracting update...", @@ -115,6 +122,11 @@ "gui.nsupdate.gaming.title": "Northstar update available!", "gui.nsupdate.gaming.body": "An update for Northstar is available.\nYou can force its installation after closing the game.", + "gui.server.player": "player", + "gui.server.players": "players", + "gui.server.servers": "servers", + "gui.server.offline": "Masterserver is Offline", + "gui.launch": "Launch", "gui.running": "Running", "gui.launchvanilla": "Vanilla", @@ -137,6 +149,9 @@ "wine.originnotfound": "Origin can't be found in the selected Wine prefix.", "wine.cantfindprefix": "Viper was unable to automatically find and set your Wine prefix.", + "gui.toast.noInternet.title": "No Internet", + "gui.toast.noInternet.desc": "Viper may not work properly.", + "viper.menu.main": "Viper", "viper.menu.release": "Release Notes", "viper.menu.info": "Extras", @@ -155,5 +170,9 @@ "general.mods.installed": "Installed mods:", "general.missingpath": "Game location could not be found automatically! Please select it manually!", "general.notinstalled": "Northstar is not installed!", - "general.launching": "Launching" + "general.launching": "Launching", + "general.invalidconfig": "Your config file is improperly formatted, if it's been manually edited, please validate that everything is typed correctly.\n\nIf you did not manually edit the config file, it is recommended to simply reset the config.\n\nTo reset your config file simply click \"Ok\" below.\n\nMore details:\n", + + "request.viper.noReleaseNotes": "Couldn't fetch Viper release notes.\nTry again later!", + "request.northstar.noReleaseNotes": "Couldn't fetch Northstar release notes.\nTry again later!" } diff --git a/src/lang/es.json b/src/lang/es.json index b1d34e1..f80c9db 100644 --- a/src/lang/es.json +++ b/src/lang/es.json @@ -24,6 +24,7 @@ "cli.update.finished": "Instalación/Actualización completada!", "cli.update.uptodate": "La ultima versión (%s) ya está instalada, omitiendo actualización.", "cli.update.uptodate.short": "Está actualizado", + "cli.update.noInternet": "Sin conexión a internet", "cli.autoupdates.checking": "Buscando actualizaciones de Northstar...", "cli.autoupdates.available": "¡Actualización de Northsar disponible!", @@ -68,6 +69,7 @@ "gui.mods.installing": "Instalando modificación...", "gui.mods.installedmod": "¡Modificación instalada!", "gui.mods.dragdrop": "Arrastra y suelta una modificación para instalarla", + "gui.mods.confirmdependencies": "Este paquete tiene dependencias, se muestran abajo. Presionar \"Ok\" instalará el paquete y las dependencias.\n\n", "gui.browser.info": "Información", "gui.browser.madeby": "por", @@ -76,18 +78,21 @@ "gui.browser.install": "Instalar", "gui.browser.reinstall": "Re-Instalar", "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.browser.endoflist": "Todos los paquetes han sido cargados", "gui.browser.noresults": "Sin resultados...", "gui.browser.filter.mods": "Modificaciones", "gui.browser.filter.skins": "Skins", "gui.browser.filter.client": "Del lado del cliente", "gui.browser.filter.server": "Del lado del servidor", + "gui.browser.view": "Ver", + "gui.browser.loadmore": "Cargar más...", "gui.settings.save": "Guardar", "gui.settings.discard": "Descartar", "gui.settings.title.ns": "Northstar", "gui.settings.title.linux": "Linux", "gui.settings.title.updates": "Actualizaciones", + "gui.settings.title.misc": "Misceláneos", "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", @@ -96,6 +101,8 @@ "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.settings.originkill.title": "Cerrar Origin automáticamente", + "gui.settings.originkill.desc": "Cuando Viper se cierra, cerrar automáticamente Origin también.", "gui.settings.title.language": "Idioma", "gui.settings.autolang.title": "Detectar automáticamente el idioma", "gui.settings.autolang.desc": "Cuando está habilitado, Viper intenta detectar automáticamente el idioma de su sistema, cuando está deshabilitado, puede cambiar manualmente el idioma a continuación.", @@ -133,6 +140,13 @@ "gui.toast.desc.failed": "Se produjo un error desconocido al intentar instalar la modificación. Esto puede ser culpa del autor de la modificación, y también puede ser culpa de Viper.", "gui.running": "Ejecutándose", + "gui.server.player": "jugador", + "gui.server.players": "jugadores", + "gui.server.servers": "servidores", + "gui.server.offline": "El servidor Master está desconectado", + + "gui.toast.noInternet.title": "Sin Internet", + "gui.toast.noInternet.desc": "Viper puede funcionar de forma incorrecta.", "viper.menu.main": "Viper", "viper.menu.release": "Notas de la versión", @@ -152,9 +166,15 @@ "general.mods.installed": "Modificiaciones intaladas:", "general.missingpath": "¡La ruta del jueno no se ha podido encontrar automáticamente! ¡Por favor, elige la ruta manualmente!", "general.notinstalled": "¡Northstar no se ha instalado!", - "general.launching": "Ejecutando" + "general.launching": "Ejecutando", "wine.invalidprefix": "El prefijo seleccionado de Wine no existe, y por ende es inválido.", "wine.originnotfound": "Origin no pudo ser encontrado en el prefijo de Wine seleccionado.", "wine.cantfindprefix": "Viper no logró encontrar y establecer automáticamente el prefijo de Wine", + + "general.launching": "Ejecutando", + "general.invalidconfig": "Su archivo de configuración está formateado de forma incorrecta, si ha sido editado manualmente, por favor valide que ha sido escrito correctamente. \n\nSi no ha editado manualmente el archivo de configuración, es recomendado que simplemente restaure la configuración. \n\nPara restaurar tu configuración simplemente de click en \"OK\" a continuación. \n\nMas detalles:\n", + + "request.viper.noReleaseNotes": "No se pudo encontrar las notas de lanzamiento de Viper.\n¡Intenta mas tarde!", + "request.northstar.noReleaseNotes": "No se pudo encontrar las notas de lanzamiento de Northstar.\n¡Intenta mas tarde!" } diff --git a/src/lang/fr.json b/src/lang/fr.json index 434fce0..b3468f8 100644 --- a/src/lang/fr.json +++ b/src/lang/fr.json @@ -22,6 +22,7 @@ "cli.update.finished": "Mise à jour terminée !", "cli.update.uptodate": "La dernière version (%s) est déjà installée.", "cli.update.uptodate.short": "Votre client est à jour", + "cli.update.noInternet": "Pas de connexion Internet", "cli.autoupdates.checking": "Vérifications des mises à jour de Northstar...", "cli.autoupdates.available": "Une mise à jour de Northstar est disponible !", @@ -68,14 +69,17 @@ "gui.mods.installing": "Installation du mod...", "gui.mods.installedmod": "Mod installé !", "gui.mods.dragdrop": "Glissez/déposez un mod pour l'installer", + "gui.mods.confirmdependencies": "Ce mod a des dépendances (affichées ci-dessous), cliquer \"Ok\" les installera en même temps que le mod.\n\n", "gui.browser.info": "Info", + "gui.browser.view": "Voir", "gui.browser.madeby": "par", "gui.browser.search": "Rechercher", "gui.browser.update": "Mise à jour", "gui.browser.install": "Installer", "gui.browser.reinstall": "Réinstaller", "gui.browser.loading": "Chargement des mods...", + "gui.browser.loadmore": "Charger plus de mods...", "gui.browser.endoflist": "Fin de la liste de mods.<br>Utilisez la barre de recherche pour en trouver davantage !", "gui.browser.noresults": "Pas de résultat", "gui.browser.filter.mods": "Mods", @@ -89,6 +93,7 @@ "gui.settings.title.linux": "Linux", "gui.settings.title.language": "Langue", "gui.settings.title.updates": "Mises à jour", + "gui.settings.title.misc": "Divers", "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.wineprefix.title": "Préfixe Wine", @@ -105,6 +110,8 @@ "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.settings.originkill.title": "Quitter automatiquement Origin", + "gui.settings.originkill.desc": "Lorsque Viper est fermé, Origin sera également automatiquement fermé.", "gui.update.downloading": "Téléchargement de la mise à jour...", "gui.update.extracting": "Extraction des fichiers...", @@ -115,6 +122,11 @@ "gui.nsupdate.gaming.title": "Mise à jour Northstar disponible !", "gui.nsupdate.gaming.body": "Une mise à jour pour Northstar est disponible.\nVous pourrez l'installer après avoir fermé le jeu.", + "gui.server.player": "joueur", + "gui.server.players": "joueurs", + "gui.server.servers": "serveurs", + "gui.server.offline": "Le serveur maître est hors-ligne", + "gui.launch": "Jouer", "gui.running": "En cours d'exécution", "gui.launchvanilla": "Vanilla", @@ -137,6 +149,9 @@ "wine.originnotfound": "Origin ne peut être trouvé dans le préfixe Wine sélectionné.", "wine.cantfindprefix": "Viper n'a pas pu trouver et configurer votre préfixe Wine.", + "gui.toast.noInternet.title": "Pas de connexion Internet", + "gui.toast.noInternet.desc": "Viper ne fonctionnera pas correctement tant que la connexion n'est pas rétablie.", + "viper.menu.main": "Viper", "viper.menu.release": "Notes de mises à jour", "viper.menu.info": "Informations", @@ -155,5 +170,9 @@ "general.mods.installed": "Mods installés :", "general.missingpath": "Le chemin du client n'a pu être trouvé automatiquement, merci de le sélectionner manuellement.", "general.notinstalled": "Northstar n'est pas installé !", - "general.launching": "Lancement" + "general.launching": "Lancement", + "general.invalidconfig": "Votre fichier de configuration n'est pas formaté correctement ; si vous l'avez manuellement édité, veuillez vérifier son contenu.\n\nSinon, il est recommandé de remettre la configuration à zéro.\n\nPour cela, cliquez sur le bouton \"Ok\" en-dessous de ce message.\n\nPlus d'informations :\n", + + "request.viper.noReleaseNotes": "Impossible de récupérer les notes de mises à jour de Viper.\nVeuillez réessayer plus tard.", + "request.northstar.noReleaseNotes": "Impossible de récupérer les notes de mises à jour de Northstar.\nVeuillez réessayer plus tard." } diff --git a/src/lang/maintainers.json b/src/lang/maintainers.json index 96ad685..41ab968 100644 --- a/src/lang/maintainers.json +++ b/src/lang/maintainers.json @@ -10,6 +10,11 @@ "fr": [ "https://github.com/Alystrasz" + ], + + "de": [ + "https://github.com/DxsSucuk", + "https://twitter.com/memerinoto" ] } } diff --git a/src/utils.js b/src/utils.js index 2c1ed98..e6a3245 100644 --- a/src/utils.js +++ b/src/utils.js @@ -12,12 +12,14 @@ const find = require("./extras/find"); const requests = require("./extras/requests"); const unzip = require("unzipper"); -const run = require("child_process").spawn; +const repair = require("jsonrepair"); const exec = require("child_process").exec; const { https } = require("follow-redirects"); process.chdir(app.getPath("appData")); +var invalidsettings = false; + // Base settings var settings = { gamepath: "", @@ -27,6 +29,7 @@ var settings = { wineprefix: "", forcedlang: "en", autoupdate: true, + originkill: false, nsargs: "-multiple", zip: "/northstar.zip", winebin: "/usr/bin/wine64", @@ -39,9 +42,29 @@ var settings = { ] } +// Logs into the dev tools of the renderer +function winLog(msg) { + ipcMain.emit("win-log", msg, msg); +} + +// Sends an alert to the renderer +function winAlert(msg) { + ipcMain.emit("win-alert", msg, msg); +} + // Creates the settings file with the base settings if it doesn't exist. if (fs.existsSync("viper.json")) { - settings = {...settings, ...JSON.parse(fs.readFileSync("viper.json", "utf8"))}; + let conf = fs.readFileSync("viper.json", "utf8"); + let json = "{}"; + + // Validates viper.json + try { + json = JSON.parse(conf); + }catch (e) { + invalidsettings = true; + } + + settings = {...settings, ...json}; settings.zip = path.join(settings.gamepath + "/northstar.zip"); let args = path.join(settings.gamepath, "ns_startup_args.txt"); @@ -80,6 +103,47 @@ async function isGameRunning() { }); } +// checks if any origin processes are running +async function isOriginRunning() { + return new Promise(resolve => { + let procs = ["Origin.exe", "OriginClientService.exe"]; + let cmd = (() => { + switch (process.platform) { + case "linux": return "ps -A"; + case "win32": return "tasklist"; + } + })(); + + exec(cmd, (err, stdout) => { + procs.forEach(proc => { + if (stdout.includes(proc)) { + resolve(true); + return; + } + resolve(false); + }); + }); + }); +} + +// kill origin processes +async function killOrigin() { + return new Promise(resolve => { + let proc = "Origin.exe"; //I'm pretty sure we only have to kill this one + let cmd = (() => { + switch (process.platform) { + case "linux": return "killall " + proc; + case "win32": return "taskkill /IM " + proc + " /F"; + } + })(); + + exec(cmd, (err, stdout) => { + // just try and fail silently if we don't find it w/e + resolve(true); + }); + }); +} + // Handles auto updating Northstar. // // It uses isGameRunning() to ensure it doesn't run while the game is @@ -92,6 +156,7 @@ function handleNorthstarUpdating() { async function _checkForUpdates() { let localVersion = getNSVersion(); let distantVersion = await requests.getLatestNsVersion(); + if (distantVersion == false) { return; } console.log(lang("cli.autoupdates.checking")); // Checks if NS is outdated @@ -100,19 +165,19 @@ function handleNorthstarUpdating() { if (await isGameRunning()) { console.log(lang("cli.autoupdates.gamerunning")); new Notification({ - title: lang("gui.nsupdate.gaming.title"), + title: lang("gui.nsupdate.gaming.title"), body: lang("gui.nsupdate.gaming.body") }).show(); } else { console.log(lang("cli.autoupdates.updatingns")); - update(); + updateNorthstar(); } } else { - console.log(lang("cli.autoupdates.noupdate")) + console.log(lang("cli.autoupdates.noupdate")); } setTimeout( - _checkForUpdates, + _checkForUpdates, 15 * 60 * 1000 // interval in between each update check // by default 15 minutes. @@ -166,11 +231,11 @@ async function setpath(win, forcedialog) { return; } if (! fs.existsSync(path.join(res.filePaths[0], "Titanfall2.exe"))) { - ipcMain.emit("wrongpath"); + ipcMain.emit("wrong-path"); return; } - setGamepath(res.filePaths[0]) + setGamepath(res.filePaths[0]); cli.exit(); return; @@ -185,6 +250,8 @@ async function setpath(win, forcedialog) { // 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 = {}) { + if (invalidsettings) {return false} + settings = {...settings, ...obj}; if (fs.existsSync(settings.gamepath)) { @@ -197,17 +264,57 @@ function saveSettings(obj = {}) { // Returns the current Northstar version // If not installed it'll return "unknown" function getNSVersion() { - var versionFilePath = path.join(settings.gamepath, "ns_version.txt"); + // if NorthstarLauncher.exe doesn't exist, always return "unknown" + if (! fs.existsSync(path.join(settings.gamepath, "NorthstarLauncher.exe"))) { + return "unknown"; + } - if (fs.existsSync(versionFilePath)) { - return fs.readFileSync(versionFilePath, "utf8"); - } else { - if (gamepathExists()) { - fs.writeFileSync(versionFilePath, "unknown"); + // mods to check version of + var versionFiles = [ + "Northstar.Client", + "Northstar.Custom", + "Northstar.CustomServers" + ] + + var versions = []; + + + let add = (version) => { + versions.push(version) + } + + // checks version of mods + for (let i = 0; i < versionFiles.length; i++) { + var versionFile = path.join(settings.gamepath, "R2Northstar/mods/", versionFiles[i],"/mod.json"); + if (fs.existsSync(versionFile)) { + if (! fs.statSync(versionFile).isFile()) { + add("unknown"); + } + + try { + add("v" + JSON.parse(fs.readFileSync(versionFile, "utf8")).Version); + }catch(err) { + add("unknown"); + } + } else { + add("unknown"); } + } - return "unknown"; + if (versions.includes("unknown")) {return "unknown"} + + // verifies all mods have the same version number + let mismatch = false; + let baseVersion = versions[0]; + for (let i = 0; i < versions.length; i++) { + if (versions[i] != baseVersion) { + mismatch = true; + break + } } + + if (mismatch) {return "unknown"} + return baseVersion; } @@ -231,7 +338,7 @@ function restoreExcludedFiles() { for (let i = 0; i < settings.excludes.length; i++) { let exclude = path.join(settings.gamepath + "/" + settings.excludes[i]); if (fs.existsSync(exclude + ".excluded")) { - fs.renameSync(exclude + ".excluded", exclude) + fs.renameSync(exclude + ".excluded", exclude); } } } @@ -247,7 +354,7 @@ restoreExcludedFiles(); // As to handle not overwriting files we rename certain files to // <file>.excluded, then rename them back after the extraction. The // unzip module does not support excluding files directly. -async function update() { +async function updateNorthstar() { if (! gamepathExists()) {return} ipcMain.emit("ns-update-event", "cli.update.checking"); @@ -255,6 +362,11 @@ async function update() { var version = getNSVersion(); const latestAvailableVersion = await requests.getLatestNsVersion(); + console.log(latestAvailableVersion) + if (latestAvailableVersion == false) { + ipcMain.emit("ns-update-event", "cli.update.noInternet"); + return; + } // Makes sure it is not already the latest version if (version === latestAvailableVersion) { @@ -267,7 +379,7 @@ async function update() { } else { if (version != "unknown") { console.log(lang("cli.update.current"), version); - }; + }; console.log(lang("cli.update.downloading") + ":", latestAvailableVersion); ipcMain.emit("ns-update-event", "cli.update.downloading"); } @@ -276,7 +388,7 @@ async function update() { for (let i = 0; i < settings.excludes.length; i++) { let exclude = path.join(settings.gamepath + "/" + settings.excludes[i]); if (fs.existsSync(exclude)) { - fs.renameSync(exclude, exclude + ".excluded") + fs.renameSync(exclude, exclude + ".excluded"); } } @@ -295,19 +407,32 @@ async function update() { stream.on("finish", () => { stream.close(); + let extract = fs.createReadStream(settings.zip); + winLog(lang("gui.update.extracting")); ipcMain.emit("ns-update-event", "gui.update.extracting"); console.log(lang("cli.update.downloaddone")); // Extracts the zip, this is the part where we're actually // installing Northstar. - fs.createReadStream(settings.zip).pipe(unzip.Extract({path: settings.gamepath})) - .on("finish", () => { - fs.writeFileSync(path.join(settings.gamepath, "ns_version.txt"), latestAvailableVersion); + extract.pipe(unzip.Extract({path: settings.gamepath})) + + let max = received; + received = 0; + + extract.on("data", (chunk) => { + received += chunk.length; + let percent = Math.floor(received / max * 100); + ipcMain.emit("ns-update-event", lang("gui.update.extracting") + " " + percent + "%"); + }) + + extract.on("end", () => { + extract.close(); ipcMain.emit("getversion"); restoreExcludedFiles(); - ipcMain.emit("guigetmods"); + ipcMain.emit("gui-getmods"); + ipcMain.emit("get-version"); ipcMain.emit("ns-update-event", "cli.update.uptodate.short"); winLog(lang("gui.update.finished")); console.log(lang("cli.update.finished")); @@ -322,7 +447,7 @@ async function update() { // This uses electron updater to easily update and publish releases, it // simply fetches it from GitHub and updates if it's outdated, very // useful. Not much we have to do on our side. -function updatevp(autoinstall) { +function updateViper(autoinstall) { const { autoUpdater } = require("electron-updater"); if (! autoUpdater.isUpdaterActive()) { @@ -399,7 +524,7 @@ function launch(version) { if (process.platform != "win32") { run(winebin, [path.join(settings.gamepath + "/Titanfall2.exe")]) } else { - run(path.join(settings.gamepath + "/Titanfall2.exe")) + exec("Titanfall2.exe", {cwd: settings.gamepath}); } break; @@ -415,25 +540,14 @@ function launch(version) { run(winebin, [prefix.origin]) run(winebin, [path.join(settings.gamepath + "/NorthstarLauncher.exe")]) } else { - run(path.join(settings.gamepath + "/Titanfall2.exe"), ["-northstar"]) + exec("NorthstarLauncher.exe", {cwd: settings.gamepath}); } - break; } process.chdir(cwd); } -// Logs into the dev tools of the renderer -function winLog(msg) { - ipcMain.emit("winLog", msg, msg); -} - -// Sends an alert to the renderer -function winAlert(msg) { - ipcMain.emit("winAlert", msg, msg); -} - // Returns true/false depending on if the gamepath currently exists/is // mounted, used to avoid issues... function gamepathExists() { @@ -455,17 +569,17 @@ const mods = { let modpath = path.join(settings.gamepath, "R2Northstar/mods"); if (getNSVersion() == "unknown") { - winLog(lang("general.notinstalled")) - console.log("error: " + lang("general.notinstalled")) + winLog(lang("general.notinstalled")); + console.log("error: " + lang("general.notinstalled")); cli.exit(1); return false; } - let mods = []; + let enabled = []; let disabled = []; if (! fs.existsSync(modpath)) { - fs.mkdirSync(path.join(modpath, "disabled"), {recursive: true}) + fs.mkdirSync(path.join(modpath), {recursive: true}); return { enabled: [], disabled: [], @@ -473,53 +587,44 @@ const mods = { }; } - files = fs.readdirSync(modpath) + files = fs.readdirSync(modpath); files.forEach((file) => { if (fs.statSync(path.join(modpath, file)).isDirectory()) { - if (fs.existsSync(path.join(modpath, file, "mod.json"))) { - try { - mods.push({...require(path.join(modpath, file, "mod.json")), FolderName: file, Disabled: false}) - }catch(err) { - if (cli.hasArgs()) {console.log("error: " + lang("cli.mods.improperjson"), file)} - mods.push({Name: file, FolderName: file, Version: "unknown", Disabled: false}) - } + let modjson = path.join(modpath, file, "mod.json"); + if (fs.existsSync(modjson)) { + let mod = JSON.parse(repair(fs.readFileSync(modjson, "utf8"))); - let manifest = path.join(modpath, file, "manifest.json"); - if (fs.existsSync(manifest)) { - try {mods[mods.length - 1].ManifestName = require(manifest).name}catch(err){} - } - } - } - }) + let obj = { + Version: "unknown", + Name: "unknown", + FolderName: file, + ...mod} - let disabledPath = path.join(modpath, "disabled") - if (! fs.existsSync(disabledPath)) { - fs.mkdirSync(disabledPath) - } + obj.Disabled = ! mods.modfile().get(obj.Name); - files = fs.readdirSync(disabledPath) - files.forEach((file) => { - if (fs.statSync(path.join(disabledPath, file)).isDirectory()) { - if (fs.existsSync(path.join(disabledPath, file, "mod.json"))) { - try { - disabled.push({...require(path.join(disabledPath, file, "mod.json")), FolderName: file, Disabled: true}) - }catch(err) { - if (cli.hasArgs()) {console.log("error: " + lang("cli.mods.improperjson"), file)} - disabled.push({Name: file, FolderName: file, Version: "unknown", Disabled: true}) + let manifestfile = path.join(modpath, file, "manifest.json"); + if (fs.existsSync(manifestfile)) { + let manifest = JSON.parse(repair(fs.readFileSync(manifestfile, "utf8"))); + + obj.ManifestName = manifest.name; + if (obj.Version == "unknown") { + obj.Version = manifest.version_number; + } } - } - let manifest = path.join(modpath, file, "manifest.json"); - if (fs.existsSync(manifest)) { - try {mods[mods.length - 1].ManifestName = require(manifest).name}catch(err){} + if (obj.Disabled) { + disabled.push(obj); + } else { + enabled.push(obj); + } } } }) return { - enabled: mods, + enabled: enabled, disabled: disabled, - all: [...mods, ...disabled] + all: [...enabled, ...disabled] }; }, @@ -533,8 +638,8 @@ const mods = { let modpath = path.join(settings.gamepath, "R2Northstar/mods"); if (getNSVersion() == "unknown") { - winLog(lang("general.notinstalled")) - console.log("error: " + lang("general.notinstalled")) + winLog(lang("general.notinstalled")); + console.log("error: " + lang("general.notinstalled")); cli.exit(1); return false; } @@ -550,6 +655,67 @@ const mods = { return false; }, + // Manages the enabledmods.json file + // + // It can both return info about the file, but also toggle mods in + // it, generate the file itself, and so on. + modfile: () => { + let modpath = path.join(settings.gamepath, "R2Northstar/mods"); + let file = path.join(modpath, "..", "enabledmods.json"); + + if (! fs.existsSync(modpath)) { + fs.mkdirSync(path.join(modpath), {recursive: true}); + } + + if (! fs.existsSync(file)) { + fs.writeFileSync(file, "{}"); + } + + return { + gen: () => { + let names = {}; + let list = mods.list().all; + for (let i = 0; i < list.length; i++) { + names[list[i].Name] = true + } + + fs.writeFileSync(file, JSON.stringify(names)); + }, + disable: (mod) => { + let data = JSON.parse(repair(fs.readFileSync(file, "utf8"))); + data[mod] = false; + fs.writeFileSync(file, JSON.stringify(data)); + }, + enable: (mod) => { + let data = JSON.parse(repair(fs.readFileSync(file, "utf8"))); + data[mod] = true; + fs.writeFileSync(file, JSON.stringify(data)); + }, + toggle: (mod) => { + let data = JSON.parse(repair(fs.readFileSync(file, "utf8"))); + if (data[mod] != undefined) { + data[mod] = ! data[mod]; + } else { + data[mod] = false; + } + + fs.writeFileSync(file, JSON.stringify(data)); + }, + get: (mod) => { + let data = JSON.parse(repair(fs.readFileSync(file, "utf8"))); + let names = Object.keys(data); + + if (data[mod]) { + return true; + } else if (data[mod] === false) { + return false; + } else { + return true; + } + } + }; + }, + // Installs mods from a file path // // Either a zip or folder is supported, we'll also try to search @@ -559,15 +725,15 @@ const mods = { let modname = mod.replace(/^.*(\\|\/|\:)/, ""); if (getNSVersion() == "unknown") { - winLog(lang("general.notinstalled")) - console.log("error: " + lang("general.notinstalled")) + winLog(lang("general.notinstalled")); + console.log("error: " + lang("general.notinstalled")); cli.exit(1); return false; } let notamod = () => { - winLog(lang("gui.mods.notamod")) - console.log("error: " + lang("cli.mods.notamod")) + winLog(lang("gui.mods.notamod")); + console.log("error: " + lang("cli.mods.notamod")); cli.exit(1); return false; } @@ -576,30 +742,30 @@ const mods = { console.log(lang("cli.mods.installed")); cli.exit(); - winLog(lang("gui.mods.installedmod")) + winLog(lang("gui.mods.installedmod")); if (modname == "mods") { - let manifest = path.join(app.getPath("userData"), "Archives/manifest.json") + let manifest = path.join(app.getPath("userData"), "Archives/manifest.json"); if (fs.existsSync(manifest)) { modname = require(manifest).name; } } - ipcMain.emit("installedmod", "", { + ipcMain.emit("installed-mod", "", { name: modname, malformed: malformed, }); - ipcMain.emit("guigetmods"); + ipcMain.emit("gui-getmods"); return true; } if (! fs.existsSync(mod)) {return notamod()} if (fs.statSync(mod).isDirectory()) { - winLog(lang("gui.mods.installing")) + winLog(lang("gui.mods.installing")); files = fs.readdirSync(mod); - if (fs.existsSync(path.join(mod, "mod.json")) && + if (fs.existsSync(path.join(mod, "mod.json")) && fs.statSync(path.join(mod, "mod.json")).isFile()) { if (fs.existsSync(path.join(modpath, modname))) { @@ -607,8 +773,8 @@ const mods = { } let copydest = path.join(modpath, modname); if (typeof destname == "string") {copydest = path.join(modpath, destname)} - copy(mod, copydest) - copy(manifestfile, path.join(copydest, "manifest.json")) + copy(mod, copydest); + copy(manifestfile, path.join(copydest, "manifest.json")); return installed(); } else { @@ -619,7 +785,7 @@ const mods = { if (fs.existsSync(path.join(mod, files[i], "mod.json")) && fs.statSync(path.join(mod, files[i], "mod.json")).isFile()) { - mods.install(path.join(mod, files[i])) + mods.install(path.join(mod, files[i])); if (mods.install(path.join(mod, files[i]))) {return true}; } } @@ -630,7 +796,7 @@ const mods = { return notamod(); } else { - winLog(lang("gui.mods.extracting")) + winLog(lang("gui.mods.extracting")); let cache = path.join(app.getPath("userData"), "Archives"); if (fs.existsSync(cache)) { fs.rmSync(cache, {recursive: true}); @@ -662,7 +828,7 @@ const mods = { } if (files.length == 0) { - ipcMain.emit("failedmod"); + ipcMain.emit("failed-mod"); return notamod(); } } @@ -693,26 +859,18 @@ const mods = { if (fs.existsSync(tmp)) { if (! fs.statSync(tmp).isDirectory()) { - fs.rmSync(tmp) + fs.rmSync(tmp); } } else { - fs.mkdirSync(tmp) + fs.mkdirSync(tmp); if (fs.existsSync(modlocation)) { - fs.rmSync(modlocation) + fs.rmSync(modlocation); } } let stream = fs.createWriteStream(modlocation); res.pipe(stream); - // let received = 0; - // // Progress messages, we should probably switch this to - // // percentage instead of how much is downloaded. - // res.on("data", (chunk) => { - // received += chunk.length; - // ipcMain.emit("ns-update-event", lang("gui.update.downloading") + " " + (received / 1024 / 1024).toFixed(1) + "mb"); - // }) - stream.on("finish", () => { stream.close(); mods.install(modlocation); @@ -728,8 +886,8 @@ const mods = { let modpath = path.join(settings.gamepath, "R2Northstar/mods"); if (getNSVersion() == "unknown") { - winLog(lang("general.notinstalled")) - console.log("error: " + lang("general.notinstalled")) + winLog(lang("general.notinstalled")); + console.log("error: " + lang("general.notinstalled")); cli.exit(1); return false; } @@ -737,19 +895,19 @@ const mods = { if (mod == "allmods") { let modlist = mods.list().all; for (let i = 0; i < modlist.length; i++) { - mods.remove(modlist[i].Name) + mods.remove(modlist[i].Name); } return } let disabled = path.join(modpath, "disabled"); if (! fs.existsSync(disabled)) { - fs.mkdirSync(disabled) + fs.mkdirSync(disabled); } let modName = mods.get(mod).FolderName; if (! modName) { - console.log("error: " + lang("cli.mods.cantfind")) + console.log("error: " + lang("cli.mods.cantfind")); cli.exit(1); return; } @@ -769,8 +927,8 @@ const mods = { fs.rmSync(modPath, {recursive: true}); console.log(lang("cli.mods.removed")); cli.exit(); - ipcMain.emit("guigetmods"); - ipcMain.emit("removedmod", "", { + ipcMain.emit("gui-getmods"); + ipcMain.emit("removed-mod", "", { name: mod.replace(/^.*(\\|\/|\:)/, ""), manifestname: manifestname }); @@ -786,11 +944,9 @@ const mods = { // you checked for if a mod is already disable and if not run the // function. However we currently have no need for that. toggle: (mod, fork) => { - let modpath = path.join(settings.gamepath, "R2Northstar/mods"); - if (getNSVersion() == "unknown") { - winLog(lang("general.notinstalled")) - console.log("error: " + lang("general.notinstalled")) + winLog(lang("general.notinstalled")); + console.log("error: " + lang("general.notinstalled")); cli.exit(1); return false; } @@ -798,7 +954,7 @@ const mods = { if (mod == "allmods") { let modlist = mods.list().all; for (let i = 0; i < modlist.length; i++) { - mods.toggle(modlist[i].Name, true) + mods.toggle(modlist[i].Name, true); } console.log(lang("cli.mods.toggledall")); @@ -806,42 +962,22 @@ const mods = { return } - let disabled = path.join(modpath, "disabled"); - if (! fs.existsSync(disabled)) { - fs.mkdirSync(disabled) - } - - let modName = mods.get(mod).FolderName; - if (! modName) { - console.log("error: " + lang("cli.mods.cantfind")) - cli.exit(1); - return; - } - - let modPath = path.join(modpath, modName); - let dest = path.join(disabled, modName); - - if (mods.get(mod).Disabled) { - modPath = path.join(disabled, modName); - dest = path.join(modpath, modName); - } - - fs.moveSync(modPath, dest) + mods.modfile().toggle(mod); if (! fork) { console.log(lang("cli.mods.toggled")); cli.exit(); } - ipcMain.emit("guigetmods"); + ipcMain.emit("gui-getmods"); } }; setInterval(async () => { if (gamepathExists()) { - ipcMain.emit("guigetmods"); + ipcMain.emit("gui-getmods"); } else { if (fs.existsSync("viper.json")) { if (settings.gamepath != "") { - ipcMain.emit("gamepathlost"); + ipcMain.emit("gamepath-lost"); } } } @@ -855,19 +991,27 @@ setInterval(async () => { module.exports = { mods, - lang, winLog, - launch, - update, - setpath, - updatevp, - settings, - saveSettings, + + updateViper, getNSVersion, getTF2Version, + updateNorthstar, + handleNorthstarUpdating, + + launch, + killOrigin, isGameRunning, + isOriginRunning, + + + settings, + saveSettings, + + setpath, gamepathExists, - handleNorthstarUpdating, + + lang, setlang: (lang) => { settings.lang = lang; saveSettings(); |