diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/app/css/grid.css | 148 | ||||
-rw-r--r-- | src/app/css/launcher.css | 21 | ||||
-rw-r--r-- | src/app/css/popups.css | 160 | ||||
-rw-r--r-- | src/app/css/theming.css | 9 | ||||
-rw-r--r-- | src/app/index.html | 29 | ||||
-rw-r--r-- | src/app/js/browser.js (renamed from src/app/browser.js) | 65 | ||||
-rw-r--r-- | src/app/js/launcher.js (renamed from src/app/launcher.js) | 6 | ||||
-rw-r--r-- | src/app/js/misc.js | 44 | ||||
-rw-r--r-- | src/app/js/mods.js | 81 | ||||
-rw-r--r-- | src/app/js/settings.js (renamed from src/app/settings.js) | 0 | ||||
-rw-r--r-- | src/app/js/toast.js (renamed from src/app/toast.js) | 6 | ||||
-rw-r--r-- | src/app/main.css | 11 | ||||
-rw-r--r-- | src/app/main.js | 110 | ||||
-rw-r--r-- | src/cli.js | 5 | ||||
-rw-r--r-- | src/extras/findgame.js | 85 | ||||
-rw-r--r-- | src/index.js | 51 | ||||
-rw-r--r-- | src/lang.js | 8 | ||||
-rw-r--r-- | src/lang/de.json | 8 | ||||
-rw-r--r-- | src/lang/en.json | 8 | ||||
-rw-r--r-- | src/lang/es.json | 8 | ||||
-rw-r--r-- | src/lang/fr.json | 10 | ||||
-rw-r--r-- | src/modules/find.js (renamed from src/extras/find.js) | 0 | ||||
-rw-r--r-- | src/modules/json.js | 35 | ||||
-rw-r--r-- | src/modules/mods.js | 552 | ||||
-rw-r--r-- | src/modules/requests.js (renamed from src/extras/requests.js) | 10 | ||||
-rw-r--r-- | src/modules/settings.js | 70 | ||||
-rw-r--r-- | src/utils.js | 508 |
27 files changed, 1150 insertions, 898 deletions
diff --git a/src/app/css/grid.css b/src/app/css/grid.css new file mode 100644 index 0000000..5c4019c --- /dev/null +++ b/src/app/css/grid.css @@ -0,0 +1,148 @@ +.grid .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; +} + +.grid .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)); +} + +.grid .el .image, .grid .el .image img { + width: var(--height); + height: var(--height); + margin-right: var(--spacing); + border-radius: var(--spacing); +} + +.grid .el .image img.blur { + z-index: -1; + position: relative; + filter: blur(10px); + top: calc(var(--height) * -1 + 5px); +} + +.grid .el .text { + overflow: hidden; +} + +.grid .el .title, .grid .el .description { + height: 1.2em; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.grid .el .title { + font-size: 1.2em; + font-weight: 700; +} + +.popup .message #loadmore { + background: rgb(var(--blue2)); +} + +.grid .el .description {font-size: 0.8em} +.grid .el button { + background: rgb(var(--blue)); + margin-top: var(--spacing); +} + +.grid .el button.info { + background: rgb(var(--blue2)); +} + diff --git a/src/app/css/launcher.css b/src/app/css/launcher.css index ce54ddf..5698179 100644 --- a/src/app/css/launcher.css +++ b/src/app/css/launcher.css @@ -151,6 +151,15 @@ pointer-events: none; } +.section .release-block { + margin-top: 0px; + border-radius: 5px; + background: var(--bg); + padding: var(--padding); + backdrop-filter: blur(15px); + margin-bottom: var(--padding); +} + .contentBody img {max-width: 100%} .contentBody .img {text-align: center} @@ -233,14 +242,6 @@ 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; @@ -261,13 +262,15 @@ code { } #nsMods .line { + width: 100%; display: flex; align-items: center; + margin: calc(var(--padding) / 2); margin-top: calc(var(--padding) / 2); } #modsdiv { - height: 50vh; + height: 80vh; overflow-y: scroll; border-radius: 5px; background: var(--bg); diff --git a/src/app/css/popups.css b/src/app/css/popups.css index 826955b..8f47d0a 100644 --- a/src/app/css/popups.css +++ b/src/app/css/popups.css @@ -64,11 +64,6 @@ 100% {opacity: 1.0} } -#browserEntries { - display: flex; - flex-wrap: wrap; -} - .popup webview { width: 78%; margin: 0 auto; @@ -82,154 +77,6 @@ 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 { */ @@ -356,6 +203,12 @@ .title { display: flex; + margin-top: calc(var(--padding) * 2); + margin-bottom: calc(var(--padding) / 2); +} + +.title:first-child { + margin-top: 0px; } .title img { @@ -365,6 +218,7 @@ } .title h2 { + margin: 0px; margin-left: calc(var(--padding) / 3); } diff --git a/src/app/css/theming.css b/src/app/css/theming.css index 2d45b1d..6d7e223 100644 --- a/src/app/css/theming.css +++ b/src/app/css/theming.css @@ -60,3 +60,12 @@ a:hover { color: black !important; background: rgb(var(--red)) !important; } + +.blue {background: rgb(var(--blue)) !important} +.blue2 {background: rgb(var(--blue2)) !important} + +.orange {background: rgb(var(--orange)) !important} +.orange2 {background: rgb(var(--orange2)) !important} + +.red {background: rgb(var(--red)) !important} +.red2 {background: rgb(var(--red2)) !important} diff --git a/src/app/index.html b/src/app/index.html index c32d18f..f073cac 100644 --- a/src/app/index.html +++ b/src/app/index.html @@ -179,7 +179,7 @@ <img src="icons/close.png"> </button> </div> - <div id="browserEntries"> + <div id="browserEntries" class="grid"> <div class="loading">%%gui.browser.loading%%</div> </div> </div> @@ -252,19 +252,18 @@ </div> </div> <div id="nsMods" class="hidden section"> - <div id="modsdiv"> - </div> + <div id="modsdiv" class="grid"> <div class="line"> <div class="text" id="modcount">%%gui.mods%%</div> <div class="buttons modbtns"> - <button id="removemod" onclick="selected().remove()">%%gui.mods.remove%%</button> - <button id="removeall" onclick="selected(true).remove()">%%gui.mods.removeall%%</button> - <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="findmod" onclick="Browser.toggle(true)">%%gui.mods.find%%</button> + <button id="removeall" class="red2" onclick="mods.remove('allmods')">%%gui.mods.removeall%%</button> + <button id="toggleall" class="orange2" onclick="selected(true).toggle(true)">%%gui.mods.toggleall%%</button> + <button id="installmod" class="blue" onclick="installmod()">%%gui.mods.install%%</button> + <button id="findmod" class="blue2" onclick="Browser.toggle(true)">%%gui.mods.find%%</button> </div> </div> + + </div> </div> <div id="nsRelease" class="hidden section"></div> </div> @@ -286,9 +285,11 @@ <script src="lang.js"></script> <script src="main.js"></script> - <script src="toast.js"></script> - <script src="browser.js"></script> - <script src="settings.js"></script> - <script src="launcher.js"></script> + <script src="js/misc.js"></script> + <script src="js/mods.js"></script> + <script src="js/toast.js"></script> + <script src="js/browser.js"></script> + <script src="js/settings.js"></script> + <script src="js/launcher.js"></script> </body> -< +</html> diff --git a/src/app/browser.js b/src/app/js/browser.js index db68d31..fcb79a2 100644 --- a/src/app/browser.js +++ b/src/app/js/browser.js @@ -319,16 +319,17 @@ function BrowserEl(properties) { } let installstr = lang("gui.browser.install"); + let normalized_mods = []; - if (normalize(modsdiv.innerText.split("\n")).includes(normalize(properties.title))) { + for (let i = 0; i < modsobj.all; i++) { + normalized_mods.push(normalize(mods_list[i].Name)); + } + + if (normalized_mods.includes(normalize(properties.title))) { installstr = lang("gui.browser.reinstall"); - for (let i = 0; i < modsobj.all.length; i++) { - if (normalize(modsobj.all[i].Name) == normalize(properties.title) - && "v" + modsobj.all[i].Version != properties.version) { - - installstr = lang("gui.browser.update"); - } + if (version.is_newer(properties.version, modsobj.all[i].Version)) { + installstr = lang("gui.browser.update"); } } else { for (let i = 0; i < modsobj.all.length; i++) { @@ -342,9 +343,7 @@ function BrowserEl(properties) { if (title.includes(folder) || title.includes(manifestname)) { installstr = lang("gui.browser.reinstall"); - if (folder == title - && "v" + modsobj.all[i].Version != properties.version) { - + if (version.is_newer(properties.version, modsobj.all[i].Version)) { installstr = lang("gui.browser.update"); } } @@ -363,16 +362,35 @@ function BrowserEl(properties) { <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="install" onclick=''>${installstr}</button> <button class="info" onclick="Preview.set('${properties.url}')">${lang('gui.browser.view')}</button> <button class="visual">${properties.version}</button> <button class="visual">${lang("gui.browser.madeby")} ${properties.author}</button> </div> ` + entry.querySelector("button.install").addEventListener("click", () => { + installFromURL( + properties.download, + JSON.stringify(properties.dependencies), + true, properties.author + ) + }) + browserEntries.appendChild(entry); } +let recent_toasts = {}; +function add_recent_toast(name, timeout = 3000) { + if (recent_toasts[name]) {return} + + recent_toasts[name] = true; + + setTimeout(() => { + delete recent_toasts[name]; + }, timeout) +} + ipcRenderer.on("removed-mod", (event, mod) => { setButtons(true); Browser.setbutton(mod.name, lang("gui.browser.install")); @@ -382,6 +400,9 @@ ipcRenderer.on("removed-mod", (event, mod) => { }) ipcRenderer.on("failed-mod", (event, modname) => { + if (recent_toasts["failed" + modname]) {return} + add_recent_toast("failed" + modname); + setButtons(true); new Toast({ timeout: 10000, @@ -391,6 +412,19 @@ ipcRenderer.on("failed-mod", (event, modname) => { }) }) +ipcRenderer.on("duped-mod", (event, modname) => { + if (recent_toasts["duped" + modname]) {return} + add_recent_toast("duped" + modname); + + setButtons(true); + new Toast({ + timeout: 10000, + scheme: "warning", + title: lang("gui.toast.title.duped"), + description: modname + " " + lang("gui.toast.desc.duped") + }) +}) + ipcRenderer.on("no-internet", (event, modname) => { setButtons(true); new Toast({ @@ -402,6 +436,9 @@ ipcRenderer.on("no-internet", (event, modname) => { }) ipcRenderer.on("installed-mod", (event, mod) => { + if (recent_toasts["installed" + mod.name]) {return} + add_recent_toast("installed" + mod.name); + setButtons(true); Browser.setbutton(mod.name, lang("gui.browser.reinstall")); @@ -421,7 +458,11 @@ ipcRenderer.on("installed-mod", (event, mod) => { }) if (installqueue.length != 0) { - installFromURL("https://thunderstore.io/package/download/" + installqueue[0]); + installFromURL( + "https://thunderstore.io/package/download/" + installqueue[0].pkg, + false, false, installqueue[0].author + ) + installqueue.shift(); } }) diff --git a/src/app/launcher.js b/src/app/js/launcher.js index e1dbbe0..5330b7a 100644 --- a/src/app/launcher.js +++ b/src/app/js/launcher.js @@ -31,7 +31,11 @@ function formatRelease(notes) { } else { for (let release of notes) { if (release.prerelease) {continue} - content += "# " + release.name + "\n\n" + release.body + "\n\n\n"; + let new_content = "# " + release.name + "\n\n" + release.body + "\n\n\n"; + content += + "<div class='release-block'>\n" + + markdown(new_content, {breaks: true}) + "\n" + + "</div>"; } content = content.replaceAll(/\@(\S+)/g, `<a href="https://github.com/$1">@$1</a>`); diff --git a/src/app/js/misc.js b/src/app/js/misc.js new file mode 100644 index 0000000..b35f239 --- /dev/null +++ b/src/app/js/misc.js @@ -0,0 +1,44 @@ +version = { + is_newer: (version1, version2) => { + version1 = version.format(version1, true).split("."); + version2 = version.format(version2, true).split("."); + + for (let i = 0; i < version1.length; i++) { + + + let nums = [ + parseInt(version1[i]) || 0, + parseInt(version2[i]) || 0 + ]; + if (nums[0] > nums[1]) { + return true; + } else if (nums[0] < nums[1]) { + return false; + } + } + + return false; + }, + format: (version_number, no_leading_v) => { + version_number = version_number.trim(); + + if (no_leading_v) { + if (version_number[0] == "v") { + return version_number.slice(1, version_number.length); + } + + return version_number; + if (no_leading_v) { + return version_number + } + + return "v" + version_number; + } else { + if (version_number[0] != "v") { + return "v" + version_number; + } + } + + return version_number; + } +} diff --git a/src/app/js/mods.js b/src/app/js/mods.js new file mode 100644 index 0000000..39e904e --- /dev/null +++ b/src/app/js/mods.js @@ -0,0 +1,81 @@ +var mods = {}; + +mods.load = (mods_obj) => { + modcount.innerHTML = `${lang("gui.mods.count")} ${mods_obj.all.length}`; + + let normalized_names = []; + + let set_mod = (mod) => { + let image_url = ""; + let normalized_name = "mod-list-" + normalize(mod.Name); + + normalized_names.push(normalized_name); + + if (document.getElementById(normalized_name)) { + return; + } + + let div = document.createElement("div"); + div.classList.add("el"); + div.id = normalized_name; + + div.innerHTML += ` + <div class="image"> + <img src="${image_url}"> + <img class="blur" src="${image_url}"> + </div> + <div class="text"> + <div class="title">${mod.Name}</div> + <div class="description">${mod.Description}</div> + <button class="red" onclick="mods.remove('${mod.Name}')">Remove</button> + <button class="visual">${mod.Version}</button> + <button class="visual">by ${mod.Author || "Unknown"}</button> + </div> + `; + + if (! image_url) { + div.querySelector(".image").remove(); + } + + modsdiv.append(div); + } + + for (let i = 0; i < mods_obj.all.length; i++) { + set_mod(mods_obj.all[i]); + } + + let mod_els = document.querySelectorAll("#modsdiv .el"); + for (let i = 0; i < mod_els.length; i++) { + if (! normalized_names.includes(mod_els[i].id)) { + mod_els[i].remove(); + } + } +} + +mods.remove = (mod) => { + if (mod.match(/^northstar\./)) { + if (! confirm(lang("gui.mods.required.confirm"))) { + return; + } + } else if (mod == "allmods") { + if (! confirm(lang("gui.mods.removeall.confirm"))) { + return; + } + } + + ipcRenderer.send("remove-mod", mod); +} + +mods.toggle = (mod) => { + if (mod.match(/^Northstar\./)) { + if (! confirm(lang("gui.mods.required.confirm"))) { + return; + } + } else if (mod == "allmods") { + if (! confirm(lang("gui.mods.toggleall.confirm"))) { + return; + } + } + + ipcRenderer.send("toggle-mod", mod); +} diff --git a/src/app/settings.js b/src/app/js/settings.js index 34975fe..34975fe 100644 --- a/src/app/settings.js +++ b/src/app/js/settings.js diff --git a/src/app/toast.js b/src/app/js/toast.js index 9cb8996..3bc1745 100644 --- a/src/app/toast.js +++ b/src/app/js/toast.js @@ -3,6 +3,7 @@ function Toast(properties) { fg: "#000000", bg: "#FFFFFF", timeout: 3000, + callback: () => {}, title: "Untitled Toast", description: "No description provided for toast", ...properties @@ -34,7 +35,10 @@ function Toast(properties) { el.style.background = toast.bg; el.id = id; - el.setAttribute("onclick", `dismissToast(${id})`); + el.addEventListener("click", () => { + dismissToast(id); + toast.callback(); + }) el.innerHTML = ` <div class="title">${toast.title}</div> diff --git a/src/app/main.css b/src/app/main.css index f39c5a1..17ed3ee 100644 --- a/src/app/main.css +++ b/src/app/main.css @@ -1,3 +1,4 @@ +@import "css/grid.css"; @import "css/dragui.css"; @import "css/toasts.css"; @import "css/popups.css"; @@ -27,7 +28,7 @@ button { button:hover {filter: brightness(110%)} button:active {filter: brightness(90%)} -.popup, #modsdiv { +.popup, #modsdiv, .release-block { outline: 1px solid #444444; border: 3px solid var(--bg); } @@ -105,3 +106,11 @@ a, button, #close, #nsRelease, #vpReleaseNotes, .mod, #overlay, #modsdiv, #winbtns, .contentMenu { -webkit-app-region: no-drag; } + +/* grids */ + +.grid { + display: flex; + flex-wrap: wrap; +} + diff --git a/src/app/main.js b/src/app/main.js index ad1a44f..6120428 100644 --- a/src/app/main.js +++ b/src/app/main.js @@ -153,68 +153,24 @@ ipcRenderer.on("ns-update-event", (event, key) => { } }); -let lastselected = ""; -function select(entry) { - let entries = document.querySelectorAll("#modsdiv .mod .modtext"); - - for (let i = 0; i < entries.length; i++) { - if (entries[i].innerHTML == entry) { - lastselected = entry; - entries[i].parentElement.classList.add("selected"); - } else { - entries[i].parentElement.classList.remove("selected"); - } - } -} - -// Mod selection -function selected(all) { - let selected = ""; - if (all) { - selected = "allmods" - } else { - selected = document.querySelector(".mod.selected .modtext"); - if (selected != null) { - selected = selected.innerHTML; - } else { - alert(lang("gui.mods.nothingselected")); - return { - remove: () => {}, - toggle: () => {}, - } +ipcRenderer.on("unknown-error", (event, err) => { + new Toast({ + timeout: 10000, + scheme: "error", + title: lang("gui.toast.title.unknown_error"), + description: lang("gui.toast.desc.unknown_error"), + callback: () => { + new Toast({ + timeout: 15000, + scheme: "error", + title: "", + description: err.stack.replaceAll("\n", "<br>") + }) } - } - - return { - remove: () => { - - if (selected.match(/^Northstar\./)) { - if (! confirm(lang("gui.mods.required.confirm"))) { - return; - } - } else if (selected == "allmods") { - if (! confirm(lang("gui.mods.removeall.confirm"))) { - return; - } - } + }) - ipcRenderer.send("remove-mod", selected); - }, - toggle: () => { - if (selected.match(/^Northstar\./)) { - if (! confirm(lang("gui.mods.required.confirm"))) { - return; - } - } else if (selected == "allmods") { - if (! confirm(lang("gui.mods.toggleall.confirm"))) { - return; - } - } - - ipcRenderer.send("toggle-mod", selected); - } - } -} + console.error(err.stack) +}) let installqueue = []; @@ -231,7 +187,7 @@ function installFromPath(path) { } // Tells the main process to install a mod from a URL -function installFromURL(url, dependencies, clearqueue) { +function installFromURL(url, dependencies, clearqueue, author) { if (clearqueue) {installqueue = []}; let prettydepends = []; @@ -244,7 +200,11 @@ function installFromURL(url, dependencies, clearqueue) { depend = dependencies[i].replaceAll("-", "/"); let pkg = depend.split("/"); if (! isModInstalled(pkg[1])) { - newdepends.push(depend); + newdepends.push({ + pkg: depend, + author: pkg[0] + }); + prettydepends.push(`${pkg[1]} v${pkg[2]} - ${lang("gui.browser.madeby")} ${pkg[0]}`); } } @@ -261,7 +221,7 @@ function installFromURL(url, dependencies, clearqueue) { } setButtons(false); - ipcRenderer.send("install-from-url", url, dependencies); + ipcRenderer.send("install-from-url", url, author); if (dependencies) { installqueue = dependencies; @@ -294,27 +254,11 @@ ipcRenderer.on("log", (event, msg) => {log(msg)}) ipcRenderer.on("alert", (event, msg) => {alert(msg)}) // Updates the installed mods -ipcRenderer.on("mods", (event, mods) => { - modsobj = mods; - if (! mods) {return} - - modcount.innerHTML = `${lang("gui.mods.count")} ${mods.all.length}`; - modsdiv.innerHTML = ""; - - let newmod = (name, disabled) => { - if (disabled) { - disabled = `<span class="disabled">${lang("gui.mods.disabledtag")}</span>` - } else { - disabled = "" - } - - modsdiv.innerHTML += `<div onclick="select('${name}')" class="mod"><span class="modtext">${name}</span>${disabled}</div>`; - } - - for (let i = 0; i < mods.enabled.length; i++) {newmod(mods.enabled[i].Name)} - for (let i = 0; i < mods.disabled.length; i++) {newmod(mods.disabled[i].Name, " - Disabled")} +ipcRenderer.on("mods", (event, mods_obj) => { + modsobj = mods_obj; + if (! mods_obj) {return} - select(lastselected); + mods.load(mods_obj); }) // Updates version numbers @@ -6,6 +6,7 @@ const events = new Emitter(); const cli = app.commandLine; const lang = require("./lang"); +const json = require("./modules/json"); function hasArgs() { // Makes sure the GUI isn't launched. @@ -38,7 +39,7 @@ function exit(code) { // gamepath to be able to work. function gamepathExists() { if (fs.existsSync("viper.json")) { - gamepath = JSON.parse(fs.readFileSync("viper.json", "utf8")).gamepath; + gamepath = json("viper.json").gamepath; if (! fs.existsSync(gamepath)) { console.error(`error: ${lang("cli.gamepath.lost")}`); @@ -59,8 +60,8 @@ async function init() { if (cli.hasSwitch("help")) { console.log(`options: --help ${lang("cli.help.help")} - --debug ${lang("cli.help.debug")} --version ${lang("cli.help.version")} + --devtools ${lang("cli.help.devtools")} --cli ${lang("cli.help.cli")} --update ${lang("cli.help.update")} diff --git a/src/extras/findgame.js b/src/extras/findgame.js deleted file mode 100644 index 615c5b4..0000000 --- a/src/extras/findgame.js +++ /dev/null @@ -1,85 +0,0 @@ -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/index.js b/src/index.js index 879dbcf..2f4285e 100644 --- a/src/index.js +++ b/src/index.js @@ -3,9 +3,15 @@ const path = require("path"); const { autoUpdater } = require("electron-updater"); const { app, ipcMain, BrowserWindow, dialog } = require("electron"); +// ensures PWD/CWD is the config folder where viper.json is located +process.chdir(app.getPath("appData")); + const utils = require("./utils"); const cli = require("./cli"); -const requests = require("./extras/requests"); +const json = require("./modules/json"); +const mods = require("./modules/mods"); +const settings = require("./modules/settings"); +const requests = require("./modules/requests"); var log = console.log; @@ -25,6 +31,8 @@ function start() { // as it's fairly responsive, but for now we won't allow that. resizable: false, + userAgent: "test", + frame: false, titleBarStyle: "hidden", icon: path.join(__dirname, "assets/icons/512x512.png"), @@ -35,19 +43,21 @@ function start() { }, }); - // when --debug is added it'll open the dev tools - if (cli.hasParam("debug")) {win.openDevTools()} + // when --devtools is added it'll open the dev tools + if (cli.hasParam("devtools")) {win.openDevTools()} // general setup win.removeMenu(); - win.loadFile(__dirname + "/app/index.html"); + win.loadURL("file://" + __dirname + "/app/index.html", { + userAgent: "viper/" + json(path.join(__dirname, "../package.json")).version, + }); win.send = (channel, data) => { win.webContents.send(channel, data); }; send = win.send; ipcMain.on("exit", () => { - if (utils.settings.originkill) { + if (settings.originkill) { utils.isOriginRunning().then((running) => { if (running) { utils.killOrigin().then(process.exit(0)) @@ -71,18 +81,24 @@ function start() { ipcMain.on("win-alert", (event, ...args) => {send("alert", ...args)}); // mod states + ipcMain.on("duped-mod", (event, modname) => {send("duped-mod", modname)}); 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("gui-getmods", (event, ...args) => {send("mods", mods.list())}); ipcMain.on("installed-mod", (event, modname) => {send("installed-mod", modname)}); ipcMain.on("no-internet", () => {send("no-internet")}); + process.on("uncaughtException", (err) => { + send("unknown-error", err); + console.error(err); + }); + // install calls - ipcMain.on("install-from-path", (event, path) => {utils.mods.install(path)}); - ipcMain.on("install-from-url", (event, url) => {utils.mods.installFromURL(url)}); + ipcMain.on("install-from-path", (event, path) => {mods.install(path)}); + ipcMain.on("install-from-url", (event, url, author) => {mods.installFromURL(url, author)}); win.webContents.on("dom-ready", () => { - send("mods", utils.mods.list()); + send("mods", mods.list()); }); // ensures gamepath still exists and is valid on startup @@ -94,7 +110,7 @@ function start() { } }); - ipcMain.on("save-settings", (event, obj) => {utils.saveSettings(obj)}); + ipcMain.on("save-settings", (event, obj) => {settings.save(obj)}); // allows renderer to check for updates ipcMain.on("ns-update-event", (event) => {send("ns-update-event", event)}); @@ -105,7 +121,7 @@ function start() { }) // start auto-update process - if (utils.settings.autoupdate) { + if (settings.autoupdate) { if (cli.hasParam("no-vp-updates")) { utils.handleNorthstarUpdating(); } else { @@ -130,11 +146,11 @@ function start() { // module inside the file that sent the event. { ipcMain.on("install-mod", () => { if (cli.hasArgs()) { - utils.mods.install(cli.param("installmod")); + mods.install(cli.param("installmod")); } else { dialog.showOpenDialog({properties: ["openFile"]}).then(res => { if (res.filePaths.length != 0) { - utils.mods.install(res.filePaths[0]); + mods.install(res.filePaths[0]); } else { send("set-buttons", true); } @@ -142,8 +158,8 @@ ipcMain.on("install-mod", () => { } }) -ipcMain.on("remove-mod", (event, mod) => {utils.mods.remove(mod)}); -ipcMain.on("toggle-mod", (event, mod) => {utils.mods.toggle(mod)}); +ipcMain.on("remove-mod", (event, mod) => {mods.remove(mod)}); +ipcMain.on("toggle-mod", (event, mod) => {mods.toggle(mod)}); ipcMain.on("launch-ns", () => {utils.launch()}); ipcMain.on("launch-vanilla", () => {utils.launch("vanilla")}); @@ -187,7 +203,7 @@ ipcMain.on("version-cli", () => { // sends installed mods info to renderer ipcMain.on("getmods", () => { - let mods = utils.mods.list(); + let mods = mods.list(); if (mods.all.length > 0) { log(`${utils.lang("general.mods.installed")} ${mods.all.length}`); log(`${utils.lang("general.mods.enabled")} ${mods.enabled.length}`); @@ -223,9 +239,6 @@ ipcMain.on("newpath", (event, newpath) => { win.send("wrong-path"); }); -// ensures PWD/CWD is the config folder where viper.json is located -process.chdir(app.getPath("appData")); - // starts the GUI or CLI if (cli.hasArgs()) { if (cli.hasParam("update-viper")) { diff --git a/src/lang.js b/src/lang.js index f2fab3a..85488d8 100644 --- a/src/lang.js +++ b/src/lang.js @@ -1,6 +1,8 @@ const fs = require("fs"); -const enLang = JSON.parse(fs.readFileSync(__dirname + `/lang/en.json`, "utf8")); +const json = require("./modules/json"); + +const enLang = json(__dirname + "/lang/en.json"); let lang = ""; var langObj = {}; @@ -14,7 +16,7 @@ function _loadTranslation(forcedlang) { } try { - opts = JSON.parse(fs.readFileSync("viper.json", "utf8")); + opts = json("viper.json"); }catch (e) {} lang = opts.lang; @@ -39,7 +41,7 @@ function _loadTranslation(forcedlang) { lang = "en"; } - langObj = JSON.parse(fs.readFileSync(__dirname + `/lang/${lang}.json`, "utf8")); + langObj = json(__dirname + `/lang/${lang}.json`); } diff --git a/src/lang/de.json b/src/lang/de.json index f8f2d63..e281ff5 100644 --- a/src/lang/de.json +++ b/src/lang/de.json @@ -2,7 +2,7 @@ "lang.title": "German - Deutsch", "cli.help.help": "Zeigt die Hilfe Nachricht an.", - "cli.help.debug": "Öffnet Entwickler/Debug Werkzeuge.", + "cli.help.devtools": "Ö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.", @@ -56,9 +56,7 @@ "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?", @@ -134,9 +132,13 @@ "gui.toast.title.installed": "Mod installiert!", "gui.toast.title.failed": "Fehler beim Installieren des Mods!", "gui.toast.title.malformed": "Fehlerhafte Ordnerstruktur!", + "gui.toast.title.duped": "Duplizierter Ordner name!", + "gui.toast.title.unknown_error": "Unbekannter Fehler!", "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.desc.duped": "hat mehrere Ordner in sich mit dem selben Namen, wodurch ein duplizierter Ordner ensteht! Falls du der Entwickler bist solltest du dies beheben!", + "gui.toast.desc.unknown_error": "Ein unbekannter Fehler ist aufgetreten für mehr details drücken! Es wird empfohlen einen Screenshot von der detalierten Fehlernachricht zu machen wenn ein Bug-Report erstellt wird!", "gui.toast.noInternet.title": "Kein Internet", "gui.toast.noInternet.desc": "Viper funktioniert möglicherweise nicht korrekt", diff --git a/src/lang/en.json b/src/lang/en.json index 7ea0d39..e2cf29f 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -2,7 +2,7 @@ "lang.title": "English", "cli.help.help": "shows this help message", - "cli.help.debug": "opens the dev/debug tools", + "cli.help.devtools": "opens the dev/debug tools", "cli.help.version": "outputs version info", "cli.help.cli": "forces the CLI to enable", "cli.help.update": "updates Northstar from your set game path", @@ -56,9 +56,7 @@ "gui.mods.disabledtag": "Disabled", "gui.mods.install": "Install Mod", "gui.mods.find": "Find Mods", - "gui.mods.toggle": "Toggle Mod", "gui.mods.toggleall": "Toggle All", - "gui.mods.remove": "Remove Mod", "gui.mods.removeall": "Remove All", "gui.mods.nothingselected": "You've not selected a mod.", "gui.mods.toggleall.confirm": "Toggling all mods could disable mods required for Northstar to function. Are you sure?", @@ -139,11 +137,15 @@ "gui.gamepath.lost": "Gamepath no longer exists/can't be found!\n\nMake sure your drive is mounted properly, or if you moved your game location that you update the game path.\n\nViper may not work properly until next restart!", "gui.toast.title.installed": "Mod installed!", + "gui.toast.title.duped": "Duplicate folder names!", "gui.toast.title.failed": "Failed to install", "gui.toast.title.malformed": "Incorrect folder structure!", + "gui.toast.title.unknown_error": "Unknown Error!", "gui.toast.desc.installed": "has been installed successfully!", "gui.toast.desc.malformed": "has an incorrect folder structure, if you're the developer, you should fix this.", "gui.toast.desc.failed": "An unknown error occurred while trying to install the mod. This may be the author's fault, and it may also be Viper's fault.", + "gui.toast.desc.duped": "has multiple mod folders in it, with the same name, causing duplicate folders, if you're the developer, you should fix this.", + "gui.toast.desc.unknown_error": "An unknown error occurred, click for more details. You may want to take a screenshot of the detailed error when filing a bug report.", "wine.invalidprefix": "The selected Wine prefix doesn't exist, and is therefore invalid.", "wine.originnotfound": "Origin can't be found in the selected Wine prefix.", diff --git a/src/lang/es.json b/src/lang/es.json index f80c9db..aaee7b9 100644 --- a/src/lang/es.json +++ b/src/lang/es.json @@ -2,7 +2,7 @@ "lang.title": "Spanish - Español", "cli.help.help": "muestra este mensaje de ayuda", - "cli.help.debug": "habre las herramientas de desarrollador/depuración ", + "cli.help.devtools": "habre las herramientas de desarrollador/depuración ", "cli.help.version": "muestra la información de la versión", "cli.help.cli": "obliga la linea de comandos a habilitarse", "cli.help.update": "actualiza Northstar desde la ruta de juego establecida", @@ -56,9 +56,7 @@ "gui.mods.disabledtag": "Deshabilitado", "gui.mods.install": "Instalar modificación", "gui.mods.find": "Encontrar modificaciones", - "gui.mods.toggle": "Alternar modificación", "gui.mods.toggleall": "Alternar todo", - "gui.mods.remove": "Remover modificación", "gui.mods.removeall": "Remover todo", "gui.mods.nothingselected": "No has seleccionado una modificación.", "gui.mods.toggleall.confirm": "Alternar todo podría deshabilitar las modificaciones requeridas para que Northstar funcione. ¿Está seguro?", @@ -135,9 +133,13 @@ "gui.toast.title.installed": "¡Modificación instalada!", "gui.toast.title.failed": "¡Falló al instalar!", "gui.toast.title.malformed": "¡Estructura de las carpetas incorrecta!", + "gui.toast.title.duped": "tiene varias carpetas de la modificación con el mismo nombre, lo que genera carpetas duplicadas. Si eres el desarrollador, deberías arreglar esto.", + "gui.toast.title.unknown_error": "¡Error desconocido!", "gui.toast.desc.installed": "¡Ha sido instalado exitosamente!", "gui.toast.desc.malformed": "tiene una estructura de carpetas incorrecta, si usted es el desarrollador, debe corregir esto.", "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.toast.desc.duped": "¡Nombres de las carpetas duplicados!", + "gui.toast.desc.unknown_error": "Ha ocurrido un error desconocido, presiona para más detalles. Recomendamos que tomes una captura de pantalla del error con sus detalles cuando reportes un error.", "gui.running": "Ejecutándose", "gui.server.player": "jugador", diff --git a/src/lang/fr.json b/src/lang/fr.json index b3468f8..48b3a69 100644 --- a/src/lang/fr.json +++ b/src/lang/fr.json @@ -2,7 +2,7 @@ "lang.title": "French - Français", "cli.help.help": "affiche ce message d'aide", - "cli.help.debug": "affiche les outils de développement", + "cli.help.devtools": "affiche les outils de développement", "cli.help.version": "retourne des informations sur la version du logiciel", "cli.help.cli": "force l'activation de la CLI", "cli.help.update": "met à jour Northstar sur le chemin du jeu précisé", @@ -56,9 +56,7 @@ "gui.mods.disabledtag": "Désactivé", "gui.mods.install": "Installer le mod", "gui.mods.find": "Chercher des mods", - "gui.mods.toggle": "Activer/désactiver le mod", "gui.mods.toggleall": "Activer/désactiver tous les mods", - "gui.mods.remove": "Supprimer le mod", "gui.mods.removeall": "Tout supprimer", "gui.mods.nothingselected": "Aucun mod n'est sélectionné.", "gui.mods.toggleall.confirm": "Cette action pourrait désactiver des mods nécessaires au bon fonctionnement de Northstar. Souhaitez-vous faire cela ?", @@ -135,15 +133,19 @@ "gui.selectpath": "Veuillez sélectionner le dossier où se trouve le client Titanfall 2.", "gui.gamepath.must": "Vous devez sélectionner le chemin du dossier du jeu Titanfall 2 pour pouvoir lancer Viper.", - "gui.gamepath.wrong": "Ce dossier ne contient pas le jeu Titanfall 2, et n'est donc pas valide.", + "gui.gamepath.wrong": "Ce dossier ne contient pas le jeu Titanfall 2, et n'est donc pas valide.", "gui.gamepath.lost": "Le chemin du jeu ne peut être trouvé / n'existe plus !\n\nVeuillez vérifier que votre disqué est correctement monté, ou, si vous avez déplacé votre jeu, que vous avez mis à jour le chemin du dossier.\n\nViper ne fonctionnera pas correctement jusqu'au prochain redémarrage.", "gui.toast.title.installed": "Mod installé !", + "gui.toast.title.duped": "Nom de dossier dupliqué !", "gui.toast.title.failed": "L'installation a échoué", "gui.toast.title.malformed": "La structure du dossier du mod est incorrecte.", + "gui.toast.title.unknown_error": "Erreur inconnue", "gui.toast.desc.installed": "a été installé avec succès !", "gui.toast.desc.malformed": "a une structure de dossier incorrecte ; si vous êtes son développeur, vous devriez réparer ça.", "gui.toast.desc.failed": "Une erreur inconnue est survenue lors de l'installation du mod. Cela peut être du ressort de l'auteur du mod ou de Viper.", + "gui.toast.desc.duped": "contient plusieurs dossiers ayant le même nom ; si vous êtes le développer, vous devriez réparer ceci.", + "gui.toast.desc.unknown_error": "Une erreur inconnue est survenue, cliquez pour plus de détails. Vous devriez prendre une capture d'écran de l'erreur si vous comptez créer un ticket.", "wine.invalidprefix": "Le préfixe Wine sélectionné n'existe pas, et n'est donc pas valide.", "wine.originnotfound": "Origin ne peut être trouvé dans le préfixe Wine sélectionné.", diff --git a/src/extras/find.js b/src/modules/find.js index 25c1b38..25c1b38 100644 --- a/src/extras/find.js +++ b/src/modules/find.js diff --git a/src/modules/json.js b/src/modules/json.js new file mode 100644 index 0000000..5c099b1 --- /dev/null +++ b/src/modules/json.js @@ -0,0 +1,35 @@ +const fs = require("fs"); +const repair = require("jsonrepair"); + +function read(file) { + let json = false; + + // make sure the file actually exists + if (! fs.existsSync(file)) { + return false; + } + + // make sure we're actually reading a file + if (! fs.statSync(file).isFile()) { + return false; + } + + // read the file + let file_content = fs.readFileSync(file, "utf8"); + + // attempt to parse it + try { + json = JSON.parse(file_content); + }catch(err) { + // attempt to repair then parse + try { + json = JSON.parse(repair(file_content)); + }catch(repair_err) { + return false; + } + } + + return json; +} + +module.exports = read; diff --git a/src/modules/mods.js b/src/modules/mods.js new file mode 100644 index 0000000..9aa2c81 --- /dev/null +++ b/src/modules/mods.js @@ -0,0 +1,552 @@ +const path = require("path"); +const fs = require("fs-extra"); +const unzip = require("unzipper"); +const copy = require("recursive-copy"); +const { app, ipcMain } = require("electron"); +const { https } = require("follow-redirects"); + +const json = require("./json"); +const settings = require("./settings"); + +const cli = require("../cli"); +const lang = require("../lang"); +const utils = require("../utils"); + +var mods = { + installing: [], + dupe_msg_sent: false, +} + +function update_path() { + mods.path = path.join(settings.gamepath, "R2Northstar/mods"); +}; update_path(); + +// Returns a list of mods +// +// It'll return 3 arrays, all, enabled, disabled. all being a +// combination of the other two, enabled being enabled mods, and you +// guessed it, disabled being disabled mods. +mods.list = () => { + update_path(); + + if (utils.getNSVersion() == "unknown") { + utils.winLog(lang("general.notinstalled")); + console.log("error: " + lang("general.notinstalled")); + cli.exit(1); + return false; + } + + let enabled = []; + let disabled = []; + + if (! fs.existsSync(mods.path)) { + fs.mkdirSync(path.join(mods.path), {recursive: true}); + return { + enabled: [], + disabled: [], + all: [] + }; + } + + let files = fs.readdirSync(mods.path); + files.forEach((file) => { + if (fs.statSync(path.join(mods.path, file)).isDirectory()) { + let modjson = path.join(mods.path, file, "mod.json"); + if (fs.existsSync(modjson)) { + let mod = json(modjson); + if (! mod) {return} + + let obj = { + Author: false, + Version: "unknown", + Name: "unknown", + FolderName: file, + ...mod} + + obj.Disabled = ! mods.modfile.get(obj.Name); + + let manifest_file = path.join(mods.path, file, "manifest.json"); + if (fs.existsSync(manifest_file)) { + let manifest = json(manifest_file); + if (manifest != false) { + obj.ManifestName = manifest.name; + if (obj.Version == "unknown") { + obj.Version = manifest.version_number; + } + } + } + + let author_file = path.join(mods.path, file, "thunderstore_author.txt"); + if (fs.existsSync(author_file)) { + obj.Author = fs.readFileSync(author_file, "utf8"); + } + + if (obj.Disabled) { + disabled.push(obj); + } else { + enabled.push(obj); + } + } + } + }) + + return { + enabled: enabled, + disabled: disabled, + all: [...enabled, ...disabled] + }; +} + +// Gets information about a mod +// +// Folder name, version, name and whatever else is in the mod.json, keep +// in mind if the mod developer didn't format their JSON file the +// absolute basics will be provided and we can't know the version or +// similar. +mods.get = (mod) => { + update_path(); + + if (utils.getNSVersion() == "unknown") { + utils.winLog(lang("general.notinstalled")); + console.log("error: " + lang("general.notinstalled")); + cli.exit(1); + return false; + } + + let list = mods.list().all; + + for (let i = 0; i < list.length; i++) { + if (list[i].Name == mod) { + return list[i]; + } else {continue} + } + + return false; +} + +function modfile_pre() { + mods.modfile.file = path.join(mods.path, "..", "enabledmods.json"); + + if (! fs.existsSync(mods.path)) { + fs.mkdirSync(path.join(mods.path), {recursive: true}); + } + + if (! fs.existsSync(mods.modfile.file)) { + fs.writeFileSync(mods.modfile.file, "{}"); + } +} + +// 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. +mods.modfile = {}; + +mods.modfile.gen = () => { + modfile_pre(); + + let names = {}; + let list = mods.list().all; + for (let i = 0; i < list.length; i++) { + names[list[i].Name] = true + } + + fs.writeFileSync(mods.modfile.file, JSON.stringify(names)); +} + +mods.modfile.disable = (mod) => { + modfile_pre(); + + let data = json(mods.modfile.file); + data[mod] = false; + fs.writeFileSync(mods.modfile.file, JSON.stringify(data)); +} + +mods.modfile.enable = (mod) => { + modfile_pre(); + + let data = json(mods.modfile.file); + data[mod] = true; + fs.writeFileSync(mods.modfile.file, JSON.stringify(data)); +} + +mods.modfile.toggle = (mod) => { + modfile_pre(); + + let data = json(mods.modfile.file); + if (data[mod] != undefined) { + data[mod] = ! data[mod]; + } else { + data[mod] = false; + } + + fs.writeFileSync(mods.modfile.file, JSON.stringify(data)); +} + +mods.modfile.get = (mod) => { + modfile_pre(); + + let data = json(mods.modfile.file); + + 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 inside +// the zip or folder to see if buried in another folder or not, as +// sometimes that's the case. +mods.install = (mod, opts) => { + update_path(); + + let modname = mod.replace(/^.*(\\|\/|\:)/, ""); + + opts = { + forked: false, + author: false, + destname: false, + malformed: false, + manifest_file: false, + ...opts + } + + if (! opts.forked) { + mods.installing = []; + mods.dupe_msg_sent = false; + } + + if (utils.getNSVersion() == "unknown") { + utils.winLog(lang("general.notinstalled")); + console.log("error: " + lang("general.notinstalled")); + cli.exit(1); + return false; + } + + let notamod = () => { + utils.winLog(lang("gui.mods.notamod")); + console.log("error: " + lang("cli.mods.notamod")); + cli.exit(1); + return false; + } + + let installed = () => { + console.log(lang("cli.mods.installed")); + cli.exit(); + + utils.winLog(lang("gui.mods.installedmod")); + + if (modname == "mods") { + let manifest = path.join(app.getPath("userData"), "Archives/manifest.json"); + + if (fs.existsSync(manifest)) { + modname = require(manifest).name; + } + } + + ipcMain.emit("installed-mod", "", { + name: modname, + malformed: opts.malformed, + }); + + ipcMain.emit("gui-getmods"); + return true; + } + + if (! fs.existsSync(mod)) {return notamod()} + + if (fs.statSync(mod).isDirectory()) { + utils.winLog(lang("gui.mods.installing")); + files = fs.readdirSync(mod); + if (fs.existsSync(path.join(mod, "mod.json")) && + fs.statSync(path.join(mod, "mod.json")).isFile()) { + + + if (! json(path.join(mod, "mod.json"))) { + ipcMain.emit("failed-mod"); + return notamod(); + } + + if (fs.existsSync(path.join(mods.path, modname))) { + fs.rmSync(path.join(mods.path, modname), {recursive: true}); + } + + let copydest = path.join(mods.path, modname); + if (typeof opts.destname == "string") { + copydest = path.join(mods.path, opts.destname) + } + + copy(mod, copydest, (err) => { + if (err) { + ipcMain.emit("failed-mod"); + return; + } + + copy(opts.manifest_file, path.join(copydest, "manifest.json"), (err) => { + if (err) { + ipcMain.emit("failed-mod"); + return; + } + + if (opts.author) { + fs.writeFileSync( + path.join(copydest, "thunderstore_author.txt"), + opts.author + ) + } + + return installed(); + }); + }); + + return; + } else { + mod_files = fs.readdirSync(mod); + + for (let i = 0; i < mod_files.length; i++) { + if (fs.statSync(path.join(mod, mod_files[i])).isDirectory()) { + if (fs.existsSync(path.join(mod, mod_files[i], "mod.json")) && + fs.statSync(path.join(mod, mod_files[i], "mod.json")).isFile()) { + + let mod_name = mod_files[i]; + let use_mod_name = false; + + while (mods.installing.includes(mod_name)) { + if (! mods.dupe_msg_sent) { + mods.dupe_msg_sent = true; + ipcMain.emit("duped-mod", "", mod_name); + } + + use_mod_name = true; + mod_name = mod_name + " (dupe)"; + } + + mods.installing.push(mod_name); + + let install = false; + if (use_mod_name) { + install = mods.install(path.join(mod, mod_files[i]), { + ...opts, + forked: true, + destname: mod_name, + }) + } else { + install = mods.install(path.join(mod, mod_files[i]), { + ...opts, + forked: true + }) + } + + if (install) {return true}; + } + } + } + + return notamod(); + } + } else { + utils.winLog(lang("gui.mods.extracting")); + let cache = path.join(app.getPath("userData"), "Archives"); + if (fs.existsSync(cache)) { + fs.rmSync(cache, {recursive: true}); + fs.mkdirSync(path.join(cache, "mods"), {recursive: true}); + } else { + fs.mkdirSync(path.join(cache, "mods"), {recursive: true}); + } + + try { + if (mod.replace(/.*\./, "").toLowerCase() == "zip") { + fs.createReadStream(mod).pipe(unzip.Extract({path: cache})) + .on("finish", () => { + setTimeout(() => { + let manifest = path.join(cache, "manifest.json"); + if (fs.existsSync(manifest)) { + files = fs.readdirSync(path.join(cache, "mods")); + if (fs.existsSync(path.join(cache, "mods/mod.json"))) { + if (mods.install(path.join(cache, "mods"), { + ...opts, + + forked: true, + malformed: true, + manifest_file: manifest, + destname: require(manifest).name + })) { + + return true; + } + } else { + for (let i = 0; i < files.length; i++) { + let mod = path.join(cache, "mods", files[i]); + if (fs.statSync(mod).isDirectory()) { + setTimeout(() => { + if (mods.install(mod, { + ...opts, + forked: true, + destname: false, + manifest_file: manifest + })) { + + return true + } + }, 1000) + } + } + + if (files.length == 0) { + ipcMain.emit("failed-mod"); + return notamod(); + } + } + + return notamod(); + } + + if (mods.install(cache, { + ...opts, + forked: true + })) { + installed(); + } else {return notamod()} + }, 1000) + }); + } else { + return notamod(); + } + }catch(err) {return notamod()} + } +} + +// Installs mods from URL's +// +// This'll simply download the file that the URL points to and then +// install it with mods.install() +mods.installFromURL = (url, author) => { + update_path(); + + https.get(url, (res) => { + let tmp = path.join(app.getPath("cache"), "vipertmp"); + let modlocation = path.join(tmp, "/mod.zip"); + + if (fs.existsSync(tmp)) { + if (! fs.statSync(tmp).isDirectory()) { + fs.rmSync(tmp); + } + } else { + fs.mkdirSync(tmp); + if (fs.existsSync(modlocation)) { + fs.rmSync(modlocation); + } + } + + let stream = fs.createWriteStream(modlocation); + res.pipe(stream); + + stream.on("finish", () => { + stream.close(); + mods.install(modlocation, { + author: author + }) + }) + }) +} + +// Removes mods +// +// Takes in the names of the mod then removes it, no confirmation, +// that'd be up to the GUI. +mods.remove = (mod) => { + update_path(); + + if (utils.getNSVersion() == "unknown") { + utils.winLog(lang("general.notinstalled")); + console.log("error: " + lang("general.notinstalled")); + cli.exit(1); + return false; + } + + if (mod == "allmods") { + let modlist = mods.list().all; + for (let i = 0; i < modlist.length; i++) { + mods.remove(modlist[i].Name); + } + return + } + + let disabled = path.join(mods.path, "disabled"); + if (! fs.existsSync(disabled)) { + fs.mkdirSync(disabled); + } + + let mod_name = mods.get(mod).FolderName; + if (! mod_name) { + console.log("error: " + lang("cli.mods.cantfind")); + cli.exit(1); + return; + } + + let path_to_mod = path.join(mods.path, mod_name); + + if (mods.get(mod).Disabled) { + path_to_mod = path.join(disabled, mod_name); + } + + if (fs.statSync(path_to_mod).isDirectory()) { + let manifestname = null; + if (fs.existsSync(path.join(path_to_mod, "manifest.json"))) { + manifestname = require(path.join(path_to_mod, "manifest.json")).name; + } + + fs.rmSync(path_to_mod, {recursive: true}); + console.log(lang("cli.mods.removed")); + cli.exit(); + ipcMain.emit("gui-getmods"); + ipcMain.emit("removed-mod", "", { + name: mod.replace(/^.*(\\|\/|\:)/, ""), + manifestname: manifestname + }); + } else { + cli.exit(1); + } +} + +// Toggles mods +// +// If a mod is enabled it'll disable it, vice versa it'll enable it if +// it's disabled. You could have a direct .disable() function if you +// checked for if a mod is already disable and if not run the function. +// However we currently have no need for that. +mods.toggle = (mod, fork) => { + update_path(); + + if (utils.getNSVersion() == "unknown") { + utils.winLog(lang("general.notinstalled")); + console.log("error: " + lang("general.notinstalled")); + cli.exit(1); + return false; + } + + if (mod == "allmods") { + let modlist = mods.list().all; + for (let i = 0; i < modlist.length; i++) { + mods.toggle(modlist[i].Name, true); + } + + console.log(lang("cli.mods.toggledall")); + cli.exit(0); + return + } + + mods.modfile.toggle(mod); + if (! fork) { + console.log(lang("cli.mods.toggled")); + cli.exit(); + } + ipcMain.emit("gui-getmods"); +} + +module.exports = mods; diff --git a/src/extras/requests.js b/src/modules/requests.js index a5f3121..6841fe2 100644 --- a/src/extras/requests.js +++ b/src/modules/requests.js @@ -1,9 +1,10 @@ const { app } = require("electron"); const path = require("path"); const fs = require("fs"); -const { https } = require("follow-redirects"); +const { https, http } = require("follow-redirects"); const lang = require("../lang"); +const json = require("./json"); // all requests results are stored in this file const cachePath = path.join(app.getPath("cache"), "viper-requests.json"); @@ -11,6 +12,7 @@ const NORTHSTAR_LATEST_RELEASE_KEY = "nsLatestRelease"; const NORTHSTAR_RELEASE_NOTES_KEY = "nsReleaseNotes"; const VIPER_RELEASE_NOTES_KEY = "vpReleaseNotes"; +const user_agent = "viper/" + json(path.join(__dirname, "../../package.json")).version; function _saveCache(data) { fs.writeFileSync(cachePath, JSON.stringify(data)); @@ -40,7 +42,7 @@ async function getLatestNsVersion() { port: 443, path: "/repos/R2Northstar/Northstar/releases/latest", method: "GET", - headers: { "User-Agent": "viper" } + headers: { "User-Agent": user_agent } }, response => { @@ -90,7 +92,7 @@ async function getNsReleaseNotes() { port: 443, path: "/repos/R2Northstar/Northstar/releases", method: "GET", - headers: { "User-Agent": "viper" } + headers: { "User-Agent": user_agent } }, response => { @@ -141,7 +143,7 @@ async function getVpReleaseNotes() { port: 443, path: "/repos/0negal/viper/releases", method: "GET", - headers: { "User-Agent": "viper" } + headers: { "User-Agent": user_agent } }, response => { diff --git a/src/modules/settings.js b/src/modules/settings.js new file mode 100644 index 0000000..d0a2db1 --- /dev/null +++ b/src/modules/settings.js @@ -0,0 +1,70 @@ +const fs = require("fs"); +const path = require("path"); +const app = require("electron").app; + +const json = require("./json"); +const lang = require("../lang"); + +var invalid_settings = false; + +// Base settings +var settings = { + gamepath: "", + lang: "en-US", + nsupdate: true, + autolang: true, + forcedlang: "en", + autoupdate: true, + originkill: false, + nsargs: "-multiple", + zip: "/northstar.zip", + + // These files won't be overwritten when installing/updating + // Northstar, useful for config files + excludes: [ + "ns_startup_args.txt", + "ns_startup_args_dedi.txt" + ] +} + +// Creates the settings file with the base settings if it doesn't exist. +if (fs.existsSync("viper.json")) { + let conf = json("viper.json"); + + // Validates viper.json + if (! conf) { + invalid_settings = true; + } + + settings = {...settings, ...conf}; + settings.zip = path.join(settings.gamepath + "/northstar.zip"); + + let args = path.join(settings.gamepath, "ns_startup_args.txt"); + if (fs.existsSync(args)) { + settings.nsargs = fs.readFileSync(args, "utf8"); + } +} else { + console.log(lang("general.missingpath")); +} + +// As to not have to do the same one liner a million times, this +// function exists, as the name suggests, it simply writes the current +// settings to the disk. +// +// You can also pass a settings object to the function and it'll try and +// merge it together with the already existing settings +settings.save = (obj = {}) => { + if (invalid_settings) {return false} + + let settings_content = {...settings, ...obj}; + + delete settings_content.save; + + if (fs.existsSync(settings.gamepath)) { + fs.writeFileSync(path.join(settings.gamepath, "ns_startup_args.txt"), settings.nsargs); + } + + fs.writeFileSync(app.getPath("appData") + "/viper.json", JSON.stringify({...settings, ...obj})); +} + +module.exports = settings; diff --git a/src/utils.js b/src/utils.js index e6a3245..615057f 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,47 +1,22 @@ const path = require("path"); const fs = require("fs-extra"); -const copy = require("recursive-copy"); -const { app, dialog, ipcMain, Notification } = require("electron"); +const { dialog, ipcMain, Notification } = require("electron"); const Emitter = require("events"); const events = new Emitter(); const cli = require("./cli"); const lang = require("./lang"); -const find = require("./extras/find"); -const requests = require("./extras/requests"); + +const find = require("./modules/find"); +const json = require("./modules/json"); +const settings = require("./modules/settings"); +const requests = require("./modules/requests"); const unzip = require("unzipper"); -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: "", - lang: "en-US", - nsupdate: true, - autolang: true, - wineprefix: "", - forcedlang: "en", - autoupdate: true, - originkill: false, - nsargs: "-multiple", - zip: "/northstar.zip", - winebin: "/usr/bin/wine64", - - // These files won't be overwritten when installing/updating - // Northstar, useful for config files - excludes: [ - "ns_startup_args.txt", - "ns_startup_args_dedi.txt" - ] -} - // Logs into the dev tools of the renderer function winLog(msg) { ipcMain.emit("win-log", msg, msg); @@ -52,29 +27,6 @@ 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")) { - 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"); - if (fs.existsSync(args)) { - settings.nsargs = fs.readFileSync(args, "utf8"); - } -} else { - console.log(lang("general.missingpath")); -} - // A simple function that checks if the game is running, which we use to // not update Northstar when it is running. async function isGameRunning() { @@ -196,7 +148,7 @@ async function setpath(win, forcedialog) { function setGamepath(folder) { settings.gamepath = folder; settings.zip = path.join(settings.gamepath + "/northstar.zip"); - saveSettings(); + settings.save(); win.webContents.send("newpath", settings.gamepath); ipcMain.emit("newpath", null, settings.gamepath); @@ -210,7 +162,7 @@ async function setpath(win, forcedialog) { function setGamepath(folder, forcedialog) { settings.gamepath = folder; settings.zip = path.join(settings.gamepath + "/northstar.zip"); - saveSettings(); + settings.save(); win.webContents.send("newpath", settings.gamepath); ipcMain.emit("newpath", null, settings.gamepath); } @@ -243,24 +195,6 @@ async function setpath(win, forcedialog) { } } -// As to not have to do the same one liner a million times, this -// function exists, as the name suggests, it simply writes the current -// settings to the disk. -// -// 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)) { - fs.writeFileSync(path.join(settings.gamepath, "ns_startup_args.txt"), settings.nsargs); - } - - fs.writeFileSync(app.getPath("appData") + "/viper.json", JSON.stringify({...settings, ...obj})); -} - // Returns the current Northstar version // If not installed it'll return "unknown" function getNSVersion() { @@ -292,7 +226,7 @@ function getNSVersion() { } try { - add("v" + JSON.parse(fs.readFileSync(versionFile, "utf8")).Version); + add("v" + json(versionFile).Version); }catch(err) { add("unknown"); } @@ -554,423 +488,6 @@ function gamepathExists() { return fs.existsSync(settings.gamepath); } -// Used to manage mods. -// -// We can both get list of disabled mods, remove/install/toggle mods and -// other things akin to that, all kinds of mod related stuff -let modpath = path.join(settings.gamepath, "R2Northstar/mods"); -const mods = { - // Returns a list of mods - // - // It'll return 3 arrays, all, enabled, disabled. all being a - // combination of the other two, enabled being enabled mods, and you - // guessed it, disabled being disabled mods. - list: () => { - let modpath = path.join(settings.gamepath, "R2Northstar/mods"); - - if (getNSVersion() == "unknown") { - winLog(lang("general.notinstalled")); - console.log("error: " + lang("general.notinstalled")); - cli.exit(1); - return false; - } - - let enabled = []; - let disabled = []; - - if (! fs.existsSync(modpath)) { - fs.mkdirSync(path.join(modpath), {recursive: true}); - return { - enabled: [], - disabled: [], - all: [] - }; - } - - files = fs.readdirSync(modpath); - files.forEach((file) => { - if (fs.statSync(path.join(modpath, file)).isDirectory()) { - let modjson = path.join(modpath, file, "mod.json"); - if (fs.existsSync(modjson)) { - let mod = JSON.parse(repair(fs.readFileSync(modjson, "utf8"))); - - let obj = { - Version: "unknown", - Name: "unknown", - FolderName: file, - ...mod} - - obj.Disabled = ! mods.modfile().get(obj.Name); - - 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; - } - } - - if (obj.Disabled) { - disabled.push(obj); - } else { - enabled.push(obj); - } - } - } - }) - - return { - enabled: enabled, - disabled: disabled, - all: [...enabled, ...disabled] - }; - }, - - // Gets information about a mod - // - // Folder name, version, name and whatever else is in the mod.json, - // keep in mind if the mod developer didn't format their JSON file - // the absolute basics will be provided and we can't know the - // version or similar. - get: (mod) => { - let modpath = path.join(settings.gamepath, "R2Northstar/mods"); - - if (getNSVersion() == "unknown") { - winLog(lang("general.notinstalled")); - console.log("error: " + lang("general.notinstalled")); - cli.exit(1); - return false; - } - - let list = mods.list().all; - - for (let i = 0; i < list.length; i++) { - if (list[i].Name == mod) { - return list[i]; - } else {continue} - } - - 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 - // inside the zip or folder to see if buried in another folder or - // not, as sometimes that's the case. - install: (mod, destname, manifestfile, malformed = false) => { - let modname = mod.replace(/^.*(\\|\/|\:)/, ""); - - if (getNSVersion() == "unknown") { - 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")); - cli.exit(1); - return false; - } - - let installed = () => { - console.log(lang("cli.mods.installed")); - cli.exit(); - - winLog(lang("gui.mods.installedmod")); - - if (modname == "mods") { - let manifest = path.join(app.getPath("userData"), "Archives/manifest.json"); - - if (fs.existsSync(manifest)) { - modname = require(manifest).name; - } - } - - ipcMain.emit("installed-mod", "", { - name: modname, - malformed: malformed, - }); - ipcMain.emit("gui-getmods"); - return true; - } - - if (! fs.existsSync(mod)) {return notamod()} - - if (fs.statSync(mod).isDirectory()) { - winLog(lang("gui.mods.installing")); - files = fs.readdirSync(mod); - if (fs.existsSync(path.join(mod, "mod.json")) && - fs.statSync(path.join(mod, "mod.json")).isFile()) { - - if (fs.existsSync(path.join(modpath, modname))) { - fs.rmSync(path.join(modpath, modname), {recursive: true}); - } - 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")); - - return installed(); - } else { - files = fs.readdirSync(mod); - - for (let i = 0; i < files.length; i++) { - if (fs.statSync(path.join(mod, files[i])).isDirectory()) { - 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])); - if (mods.install(path.join(mod, files[i]))) {return true}; - } - } - } - - return notamod(); - } - - return notamod(); - } else { - winLog(lang("gui.mods.extracting")); - let cache = path.join(app.getPath("userData"), "Archives"); - if (fs.existsSync(cache)) { - fs.rmSync(cache, {recursive: true}); - fs.mkdirSync(path.join(cache, "mods"), {recursive: true}); - } else { - fs.mkdirSync(path.join(cache, "mods"), {recursive: true}); - } - - try { - if (mod.replace(/.*\./, "").toLowerCase() == "zip") { - fs.createReadStream(mod).pipe(unzip.Extract({path: cache})) - .on("finish", () => { - setTimeout(() => { - let manifest = path.join(cache, "manifest.json"); - if (fs.existsSync(manifest)) { - files = fs.readdirSync(path.join(cache, "mods")); - if (fs.existsSync(path.join(cache, "mods/mod.json"))) { - if (mods.install(path.join(cache, "mods"), require(manifest).name, manifest, true)) { - return true; - } - } else { - for (let i = 0; i < files.length; i++) { - let mod = path.join(cache, "mods", files[i]); - if (fs.statSync(mod).isDirectory()) { - setTimeout(() => { - if (mods.install(mod, false, manifest)) {return true}; - }, 1000) - } - } - - if (files.length == 0) { - ipcMain.emit("failed-mod"); - return notamod(); - } - } - - return notamod(); - } - - if (mods.install(cache)) { - installed(); - } else {return notamod()} - }, 1000) - }); - } else { - return notamod(); - } - }catch(err) {return notamod()} - } - }, - - // Installs mods from URL's - // - // This'll simply download the file that the URL points to and then - // install it with mods.install() - installFromURL: (url) => { - https.get(url, (res) => { - let tmp = path.join(app.getPath("cache"), "vipertmp"); - let modlocation = path.join(tmp, "/mod.zip"); - - if (fs.existsSync(tmp)) { - if (! fs.statSync(tmp).isDirectory()) { - fs.rmSync(tmp); - } - } else { - fs.mkdirSync(tmp); - if (fs.existsSync(modlocation)) { - fs.rmSync(modlocation); - } - } - - let stream = fs.createWriteStream(modlocation); - res.pipe(stream); - - stream.on("finish", () => { - stream.close(); - mods.install(modlocation); - }) - }) - }, - - // Removes mods - // - // Takes in the names of the mod then removes it, no confirmation, - // that'd be up to the GUI. - remove: (mod) => { - let modpath = path.join(settings.gamepath, "R2Northstar/mods"); - - if (getNSVersion() == "unknown") { - winLog(lang("general.notinstalled")); - console.log("error: " + lang("general.notinstalled")); - cli.exit(1); - return false; - } - - if (mod == "allmods") { - let modlist = mods.list().all; - for (let i = 0; i < modlist.length; i++) { - mods.remove(modlist[i].Name); - } - 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); - - if (mods.get(mod).Disabled) { - modPath = path.join(disabled, modName); - } - - if (fs.statSync(modPath).isDirectory()) { - let manifestname = null; - if (fs.existsSync(path.join(modPath, "manifest.json"))) { - manifestname = require(path.join(modPath, "manifest.json")).name; - } - - fs.rmSync(modPath, {recursive: true}); - console.log(lang("cli.mods.removed")); - cli.exit(); - ipcMain.emit("gui-getmods"); - ipcMain.emit("removed-mod", "", { - name: mod.replace(/^.*(\\|\/|\:)/, ""), - manifestname: manifestname - }); - } else { - cli.exit(1); - } - }, - - // Toggles mods - // - // If a mod is enabled it'll disable it, vice versa it'll enable it - // if it's disabled. You could have a direct .disable() function if - // 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) => { - if (getNSVersion() == "unknown") { - winLog(lang("general.notinstalled")); - console.log("error: " + lang("general.notinstalled")); - cli.exit(1); - return false; - } - - if (mod == "allmods") { - let modlist = mods.list().all; - for (let i = 0; i < modlist.length; i++) { - mods.toggle(modlist[i].Name, true); - } - - console.log(lang("cli.mods.toggledall")); - cli.exit(0); - return - } - - mods.modfile().toggle(mod); - if (! fork) { - console.log(lang("cli.mods.toggled")); - cli.exit(); - } - ipcMain.emit("gui-getmods"); - } -}; - setInterval(async () => { if (gamepathExists()) { ipcMain.emit("gui-getmods"); @@ -990,7 +507,6 @@ setInterval(async () => { }, 1500) module.exports = { - mods, winLog, updateViper, @@ -1004,16 +520,12 @@ module.exports = { isGameRunning, isOriginRunning, - - settings, - saveSettings, - setpath, gamepathExists, lang, setlang: (lang) => { settings.lang = lang; - saveSettings(); + settings.save(); }, } |