diff options
author | 0neGal <mail@0negal.com> | 2024-06-09 18:15:45 +0200 |
---|---|---|
committer | 0neGal <mail@0negal.com> | 2024-06-09 18:23:29 +0200 |
commit | 760031c079ce830755ba4fea029e149f4140e00b (patch) | |
tree | 8dbb9f8d5bb4e19cdaffb40e8a091b457a597769 /src/app/js | |
parent | 1c1e8fb730b9974cd9b8060499773b0f37ff28d2 (diff) | |
parent | 04b0e9fcea6c60257d7bc68994103eacb340a82b (diff) | |
download | Viper-760031c079ce830755ba4fea029e149f4140e00b.tar.gz Viper-760031c079ce830755ba4fea029e149f4140e00b.zip |
Merge branch 'main' into linux-launch-v2linux-launch-v2
Diffstat (limited to 'src/app/js')
-rw-r--r-- | src/app/js/browser.js | 225 | ||||
-rw-r--r-- | src/app/js/dom_events.js | 42 | ||||
-rw-r--r-- | src/app/js/events.js | 2 | ||||
-rw-r--r-- | src/app/js/gamepath.js | 52 | ||||
-rw-r--r-- | src/app/js/is_running.js | 46 | ||||
-rw-r--r-- | src/app/js/kill.js | 8 | ||||
-rw-r--r-- | src/app/js/launch.js | 15 | ||||
-rw-r--r-- | src/app/js/launcher.js | 49 | ||||
-rw-r--r-- | src/app/js/localize.js | 69 | ||||
-rw-r--r-- | src/app/js/mods.js | 141 | ||||
-rw-r--r-- | src/app/js/popups.js | 6 | ||||
-rw-r--r-- | src/app/js/process.js | 27 | ||||
-rw-r--r-- | src/app/js/request.js | 18 | ||||
-rw-r--r-- | src/app/js/set_buttons.js | 40 | ||||
-rw-r--r-- | src/app/js/settings.js | 589 | ||||
-rw-r--r-- | src/app/js/toasts.js (renamed from src/app/js/toast.js) | 16 | ||||
-rw-r--r-- | src/app/js/tooltip.js | 2 | ||||
-rw-r--r-- | src/app/js/update.js | 148 | ||||
-rw-r--r-- | src/app/js/version.js (renamed from src/app/js/misc.js) | 2 |
19 files changed, 1115 insertions, 382 deletions
diff --git a/src/app/js/browser.js b/src/app/js/browser.js index 06dc244..21bdf3f 100644 --- a/src/app/js/browser.js +++ b/src/app/js/browser.js @@ -1,18 +1,28 @@ +const Fuse = require("fuse.js"); +const ipcRenderer = require("electron").ipcRenderer; + +const lang = require("../../lang"); + +const popups = require("./popups"); +const toasts = require("./toasts"); +const request = require("./request"); + var browser_fuse; var packages = []; var packagecount = 0; -var mod_versions = {}; +var browser_el = document.getElementById("browser") -var Browser = { +var browser = { maxentries: 50, + mod_versions: {}, filters: { getpkgs: () => { let pkgs = []; let other = []; for (let i in packages) { - if (! Browser.filters.isfiltered(packages[i].categories)) { + if (! browser.filters.isfiltered(packages[i].categories)) { pkgs.push(packages[i]); } else { other.push(packages[i]); @@ -24,7 +34,7 @@ var Browser = { get: () => { let filtered = []; let unfiltered = []; - let checks = browser.querySelectorAll("#filters .check"); + let checks = browser_el.querySelectorAll("#filters .check"); for (let i = 0; i < checks.length; i++) { if (! checks[i].classList.contains("checked")) { @@ -40,8 +50,8 @@ var Browser = { }; }, isfiltered: (categories) => { - let filtered = Browser.filters.get().filtered; - let unfiltered = Browser.filters.get().unfiltered; + let filtered = browser.filters.get().filtered; + let unfiltered = browser.filters.get().unfiltered; let state = false; let filters = [ @@ -86,19 +96,19 @@ var Browser = { }, }, toggle: (state) => { - browser.scrollTo(0, 0); + browser_el.scrollTo(0, 0); popups.set("#browser", state); if (state) { if (browserEntries.querySelectorAll(".el").length == 0) { - Browser.loadfront(); + browser.loadfront(); } } else if (state === false) { - Browser.filters.toggle(false); + browser.filters.toggle(false); } }, install: (package_obj, clear_queue = false) => { - return installFromURL( + return mods.install_from_url( package_obj.download || package_obj.versions[0].download_url, package_obj.dependencies || package_obj.versions[0].dependencies, clear_queue, @@ -111,7 +121,7 @@ var Browser = { add_pkg_properties: () => { for (let i = 0; i < packages.length; i++) { let properties = packages[i]; - let normalized = normalize(packages[i].name); + let normalized = mods.normalize(packages[i].name); let has_update = false; let local_name = false; @@ -119,11 +129,11 @@ var Browser = { let remote_version = packages[i].versions[0].version_number; remote_version = version.format(remote_version); - if (modsobj) { - for (let ii = 0; ii < modsobj.all.length; ii++) { - let mod = modsobj.all[ii]; + if (mods.list()) { + for (let ii = 0; ii < mods.list().all.length; ii++) { + let mod = mods.list().all[ii]; - if (normalize(mod.name) !== normalized && ( + if (mods.normalize(mod.name) !== normalized && ( ! mod.package || mod.package.author + "-" + mod.package.package_name !== packages[i].full_name @@ -140,7 +150,7 @@ var Browser = { } let install = () => { - return Browser.install({...properties}); + return browser.install({...properties}); } packages[i].install = install; @@ -148,7 +158,7 @@ var Browser = { packages[i].local_version = local_version; if (local_version) { - mod_versions[normalized] = { + browser.mod_versions[normalized] = { install: install, has_update: has_update, local_name: local_name, @@ -160,7 +170,7 @@ var Browser = { } }, loadfront: async () => { - Browser.loading(); + browser.loading(); packagecount = 0; @@ -181,26 +191,26 @@ var Browser = { console.error(err) } - Browser.add_pkg_properties(); + browser.add_pkg_properties(); browser_fuse = new Fuse(packages, { keys: ["full_name"] }) } - let pkgs = Browser.filters.getpkgs(); + let pkgs = browser.filters.getpkgs(); for (let i in pkgs) { - if (packagecount >= Browser.maxentries) { - Browser.endoflist(); + if (packagecount >= browser.maxentries) { + browser.endoflist(); break } - new BrowserElFromObj(pkgs[i]); + browser.mod_el_from_obj(pkgs[i]); packagecount++; } }, loading: (string) => { - if (Browser.filters.get().unfiltered.length == 0) { + if (browser.filters.get().unfiltered.length == 0) { string = lang("gui.browser.no_results"); } @@ -214,7 +224,7 @@ var Browser = { }, endoflist: (is_end) => { let pkgs = []; - let filtered = Browser.filters.getpkgs(); + let filtered = browser.filters.getpkgs(); for (let i = 0; i < filtered.length; i++) { if (filtered[packagecount + i]) { pkgs.push(filtered[packagecount + i]); @@ -228,26 +238,26 @@ var Browser = { } if (pkgs.length == 0 || is_end) { - Browser.msg(`${lang('gui.browser.endoflist')}`); + browser.msg(`${lang('gui.browser.endoflist')}`); return } - Browser.msg(`<button id="loadmore">` + + browser.msg(`<button id="loadmore">` + `<img src="icons/down.png">` + `<span>${lang("gui.browser.load_more")}</span>` + `</button>`); loadmore.addEventListener("click", () => { - Browser.loadpkgs(pkgs); - Browser.endoflist(! pkgs.length); + browser.loadpkgs(pkgs); + browser.endoflist(! pkgs.length); }) }, search: (string) => { - Browser.loading(); + browser.loading(); let res = browser_fuse.search(string); if (res.length < 1) { - Browser.loading(lang("gui.browser.no_results")); + browser.loading(lang("gui.browser.no_results")); return } @@ -255,18 +265,18 @@ var Browser = { 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); + if (count >= browser.maxentries) {break} + if (browser.filters.isfiltered(res[i].item.categories)) {continue} + browser.mod_el_from_obj(res[i].item); count++; } if (count < 1) { - Browser.loading(lang("gui.browser.no_results")); + browser.loading(lang("gui.browser.no_results")); } }, setbutton: (mod, string, icon) => { - mod = normalize(mod); + mod = mods.normalize(mod); if (browserEntries.querySelector(`#mod-${mod}`)) { let elems = browserEntries.querySelectorAll(`.el#mod-${mod}`); @@ -281,21 +291,21 @@ var Browser = { } else { let make = (str) => { if (browserEntries.querySelector(`#mod-${str}`)) { - return Browser.setbutton(str, string, icon); + return browser.setbutton(str, string, icon); } else { return false; } } setTimeout(() => { - for (let i = 0; i < modsobj.all.length; i++) { - let modname = normalize(modsobj.all[i].name); - let modfolder = normalize(modsobj.all[i].folder_name); + for (let i = 0; i < mods.list().all.length; i++) { + let modname = mods.normalize(mods.list().all[i].name); + let modfolder = mods.normalize(mods.list().all[i].folder_name); if (mod.includes(modname)) { if (! make(modname)) { - if (modsobj.all[i].manifest_name) { - make(normalize(modsobj.all[i].manifest_name)); + if (mods.list().all[i].manifest_name) { + make(mods.normalize(mods.list().all[i].manifest_name)); } } } @@ -313,17 +323,17 @@ var Browser = { let count = 0; for (let i in pkgs) { - if (count >= Browser.maxentries) { + if (count >= browser.maxentries) { if (pkgs[i] === undefined) { - Browser.endoflist(true); + browser.endoflist(true); } - Browser.endoflist(); + browser.endoflist(); break } try { - new BrowserElFromObj(pkgs[i]); + browser.mod_el_from_obj(pkgs[i]); }catch(e) {} count++; @@ -339,18 +349,14 @@ var Browser = { } } -setInterval(Browser.add_pkg_properties, 1500); +setInterval(browser.add_pkg_properties, 1500); if (navigator.onLine) { - Browser.loadfront(); -} - -function openExternal(url) { - require("electron").shell.openExternal(url); + browser.loadfront(); } var view = document.querySelector(".popup#preview webview"); -var Preview = { +browser.preview = { show: () => { preview.classList.add("shown"); }, @@ -358,16 +364,21 @@ var Preview = { preview.classList.remove("shown"); }, set: (url, autoshow) => { - if (autoshow != false) {Preview.show()} + if (autoshow != false) {browser.preview.show()} + view.src = url; - document.querySelector("#preview #external").setAttribute("onclick", `openExternal("${url}")`); + + document.querySelector("#preview #external").setAttribute( + "onclick", + `require("electron").shell.openExternal("${url}")` + ) } } -function BrowserElFromObj(obj) { +browser.mod_el_from_obj = (obj) => { let pkg = {...obj, ...obj.versions[0]}; - new BrowserEl({ + browser.mod_el({ pkg: pkg, title: pkg.name, image: pkg.icon, @@ -381,8 +392,8 @@ function BrowserElFromObj(obj) { }) } -function BrowserEl(properties) { - if (Browser.filters.isfiltered(properties.categories)) {return} +browser.mod_el = (properties) => { + if (browser.filters.isfiltered(properties.categories)) {return} properties = { title: "No name", @@ -405,8 +416,8 @@ function BrowserEl(properties) { let installstr = lang("gui.browser.install"); let normalized_mods = []; - for (let i = 0; i < modsobj.all; i++) { - normalized_mods.push(normalize(mods_list[i].name)); + for (let i = 0; i < mods.list().all; i++) { + normalized_mods.push(mods.normalize(mods_list[i].name)); } if (properties.pkg.local_version) { @@ -421,7 +432,7 @@ function BrowserEl(properties) { let entry = document.createElement("div"); entry.classList.add("el"); - entry.id = `mod-${normalize(properties.title)}`; + entry.id = `mod-${mods.normalize(properties.title)}`; entry.innerHTML = ` <div class="image"> @@ -435,7 +446,7 @@ function BrowserEl(properties) { <img src="icons/${installicon}.png"> <span>${installstr}</span> </button> - <button class="info" onclick="Preview.set('${properties.url}')"> + <button class="info" onclick="browser.preview.set('${properties.url}')"> <img src="icons/open.png"> <span>${lang('gui.browser.view')}</span> </button> @@ -449,7 +460,7 @@ function BrowserEl(properties) { ` entry.querySelector("button.install").addEventListener("click", () => { - Browser.install(properties); + browser.install(properties); }) browserEntries.appendChild(entry); @@ -466,21 +477,21 @@ function add_recent_toast(name, timeout = 3000) { }, timeout) } -ipcRenderer.on("removed-mod", (event, mod) => { - setButtons(true); - Browser.setbutton(mod.name, lang("gui.browser.install"), "downloads"); +ipcRenderer.on("removed-mod", (_, mod) => { + set_buttons(true); + browser.setbutton(mod.name, lang("gui.browser.install"), "downloads"); if (mod.manifest_name) { - Browser.setbutton(mod.manifest_name, lang("gui.browser.install"), "downloads"); + browser.setbutton(mod.manifest_name, lang("gui.browser.install"), "downloads"); } }) -ipcRenderer.on("failed-mod", (event, modname) => { +ipcRenderer.on("failed-mod", (_, modname) => { if (recent_toasts["failed" + modname]) {return} add_recent_toast("failed" + modname); - setButtons(true); - new Toast({ + set_buttons(true); + toasts.show({ timeout: 10000, scheme: "error", title: lang("gui.toast.title.failed"), @@ -488,12 +499,12 @@ ipcRenderer.on("failed-mod", (event, modname) => { }) }) -ipcRenderer.on("legacy-duped-mod", (event, modname) => { +ipcRenderer.on("legacy-duped-mod", (_, modname) => { if (recent_toasts["duped" + modname]) {return} add_recent_toast("duped" + modname); - setButtons(true); - new Toast({ + set_buttons(true); + toasts.show({ timeout: 10000, scheme: "warning", title: lang("gui.toast.title.duped"), @@ -501,9 +512,9 @@ ipcRenderer.on("legacy-duped-mod", (event, modname) => { }) }) -ipcRenderer.on("no-internet", (event, modname) => { - setButtons(true); - new Toast({ +ipcRenderer.on("no-internet", () => { + set_buttons(true); + toasts.show({ timeout: 10000, scheme: "error", title: lang("gui.toast.title.no_internet"), @@ -511,17 +522,17 @@ ipcRenderer.on("no-internet", (event, modname) => { }) }) -ipcRenderer.on("installed-mod", (event, mod) => { +ipcRenderer.on("installed-mod", (_, mod) => { if (recent_toasts["installed" + mod.name]) {return} add_recent_toast("installed" + mod.name); let name = mod.fancy_name || mod.name; - setButtons(true); - Browser.setbutton(name, lang("gui.browser.reinstall"), "redo"); + set_buttons(true); + browser.setbutton(name, lang("gui.browser.reinstall"), "redo"); if (mod.malformed) { - new Toast({ + toasts.show({ timeout: 8000, scheme: "warning", title: lang("gui.toast.title.malformed"), @@ -529,56 +540,38 @@ ipcRenderer.on("installed-mod", (event, mod) => { }) } - new Toast({ + toasts.show({ scheme: "success", title: lang("gui.toast.title.installed"), description: name + " " + lang("gui.toast.desc.installed") }) - if (installqueue.length != 0) { - installFromURL( - "https://thunderstore.io/package/download/" + installqueue[0].pkg, - false, false, installqueue[0].author, installqueue[0].package_name, installqueue[0].version + if (mods.install_queue.length != 0) { + mods.install_from_url( + "https://thunderstore.io/package/download/" + mods.install_queue[0].pkg, + false, false, mods.install_queue[0].author, mods.install_queue[0].package_name, mods.install_queue[0].version ) - installqueue.shift(); + mods.install_queue.shift(); } }) -function normalize(items) { - let main = (string) => { - return string.replaceAll(" ", "") - .replaceAll(".", "").replaceAll("-", "") - .replaceAll("_", "").toLowerCase(); - } - if (typeof items == "string") { - return main(items); - } else { - let newArray = []; - for (let i = 0; i < items.length; i++) { - newArray.push(main(items[i])); - } - - return newArray; - } -} - let searchtimeout; let searchstr = ""; let search = document.querySelector("#browser .search"); search.addEventListener("keyup", () => { - Browser.filters.toggle(false); + browser.filters.toggle(false); clearTimeout(searchtimeout); if (searchstr != search.value) { if (search.value.replaceAll(" ", "") == "") { searchstr = ""; - Browser.loadfront(); + browser.loadfront(); return } searchtimeout = setTimeout(() => { - Browser.search(search.value); + browser.search(search.value); searchstr = search.value; }, 500) } @@ -586,24 +579,24 @@ search.addEventListener("keyup", () => { let mouse_events = ["scroll", "mousedown", "touchdown"]; mouse_events.forEach((event) => { - document.body.addEventListener(event, (e) => { + document.body.addEventListener(event, () => { let mouse_at = document.elementsFromPoint(mouseX, mouseY); if (! mouse_at.includes(document.querySelector("#preview"))) { - Preview.hide(); + browser.preview.hide(); } if (! mouse_at.includes(document.querySelector("#filter")) && ! mouse_at.includes(document.querySelector(".overlay"))) { - Browser.filters.toggle(false); + browser.filters.toggle(false); } }) }); view.addEventListener("dom-ready", () => { let css = [ - fs.readFileSync(__dirname + "/css/theming.css", "utf8"), - fs.readFileSync(__dirname + "/css/webview.css", "utf8") + fs.readFileSync(__dirname + "/../css/theming.css", "utf8"), + fs.readFileSync(__dirname + "/../css/webview.css", "utf8") ] view.insertCSS(css.join(" ")); @@ -623,7 +616,7 @@ view.addEventListener("did-start-loading", () => { let mouseY = 0; let mouseX = 0; -browser.addEventListener("mousemove", (event) => { +browser_el.addEventListener("mousemove", (event) => { mouseY = event.clientY; mouseX = event.clientX; }) @@ -631,6 +624,8 @@ browser.addEventListener("mousemove", (event) => { 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 = ''" + "this.classList.toggle('checked');browser.loadfront();search.value = ''" ) } + +module.exports = browser; diff --git a/src/app/js/dom_events.js b/src/app/js/dom_events.js new file mode 100644 index 0000000..c16a838 --- /dev/null +++ b/src/app/js/dom_events.js @@ -0,0 +1,42 @@ +const settings = require("./settings"); + +let drag_timer; +document.addEventListener("dragover", (e) => { + e.preventDefault(); + e.stopPropagation(); + dragUI.classList.add("shown"); + + clearTimeout(drag_timer); + drag_timer = setTimeout(() => { + dragUI.classList.remove("shown"); + }, 5000) +}) + +document.addEventListener("mouseover", () => { + clearTimeout(drag_timer); + dragUI.classList.remove("shown"); +}) + +document.addEventListener("drop", (e) => { + e.preventDefault(); + e.stopPropagation(); + + dragUI.classList.remove("shown"); + mods.install_from_path(e.dataTransfer.files[0].path); +}) + +document.body.addEventListener("keyup", (e) => { + if (e.key == "Escape") { + browser.toggle(false); + settings.popup.toggle(false); + } +}) + +document.body.addEventListener("click", (e) => { + if (e.target.tagName.toLowerCase() === "a" + && e.target.protocol != "file:") { + + e.preventDefault(); + shell.openExternal(e.target.href); + } +}) diff --git a/src/app/js/events.js b/src/app/js/events.js index cced4a7..040382e 100644 --- a/src/app/js/events.js +++ b/src/app/js/events.js @@ -1,3 +1,5 @@ const EventEmitter = require("events"); class Emitter extends EventEmitter {}; const events = new Emitter(); + +module.exports = events; diff --git a/src/app/js/gamepath.js b/src/app/js/gamepath.js new file mode 100644 index 0000000..c3e5c2b --- /dev/null +++ b/src/app/js/gamepath.js @@ -0,0 +1,52 @@ +const ipcRenderer = require("electron").ipcRenderer; + +const lang = require("../../lang"); +const process = require("./process"); +const launcher = require("./launcher"); +const settings = require("./settings"); + +// frontend part of settings a new game path +ipcRenderer.on("newpath", (_, newpath) => { + set_buttons(true); + + settings.set({gamepath: newpath}); + + ipcRenderer.send("gui-getmods"); + ipcRenderer.send("save-settings", settings.data()); +}) + +// a previously valid gamepath no longer exists, and is therefore lost +ipcRenderer.on("gamepath-lost", () => { + launcher.change_page(0); + set_buttons(false, true); + alert(lang("gui.gamepath.lost")); +}) + +// error out when no game path is set +ipcRenderer.on("no-path-selected", () => { + alert(lang("gui.gamepath.must")); + process.exit(); +}) + +// error out when game path is wrong +ipcRenderer.on("wrong-path", () => { + alert(lang("gui.gamepath.wrong")); + gamepath.set(false); +}) + +// reports to the main process about game path status. +module.exports = { + open: () => { + let gamepath = settings.data().gamepath; + + if (gamepath) { + require("electron").shell.openPath(gamepath); + } else { + alert(lang("gui.settings.miscbuttons.open_gamepath_alert")); + } + }, + + set: (value) => { + ipcRenderer.send("setpath", value); + } +} diff --git a/src/app/js/is_running.js b/src/app/js/is_running.js new file mode 100644 index 0000000..69efd5b --- /dev/null +++ b/src/app/js/is_running.js @@ -0,0 +1,46 @@ +const lang = require("../../lang"); + +// is the game running? +let is_running = false; + +// updates play buttons depending on whether the game is running +ipcRenderer.on("is-running", (event, running) => { + let set_playbtns = (text) => { + let playbtns = document.querySelectorAll(".playBtn"); + for (let i = 0; i < playbtns.length; i++) { + playbtns[i].innerHTML = text; + } + } + + if (running && is_running != running) { + set_buttons(false); + set_playbtns(lang("general.running")); + + is_running = running; + + // show force quit button in Titanfall tab + tfquit.style.display = "inline-block"; + + update.setAttribute("onclick", "kill('game')"); + update.innerHTML = "(" + lang("ns.menu.force_quit") + ")"; + return; + } + + if (is_running != running) { + set_buttons(true); + set_playbtns(lang("gui.launch")); + + is_running = running; + + // hide force quit button in Titanfall tab + tfquit.style.display = "none"; + + update.setAttribute("onclick", "update.ns()"); + update.innerHTML = "(" + lang("gui.update.check") + ")"; + } +}) + +// return whether the game is running +module.exports = () => { + return is_running; +} diff --git a/src/app/js/kill.js b/src/app/js/kill.js new file mode 100644 index 0000000..04f0a84 --- /dev/null +++ b/src/app/js/kill.js @@ -0,0 +1,8 @@ +const ipcRenderer = require("electron").ipcRenderer; + +// attempts to kill something using the main process' `modules/kill.js` +// functions, it simply attempts to run `kill[function_name]()`, if it +// doesn't exist, nothing happens +module.exports = (function_name) => { + ipcRenderer.send("kill", function_name); +} diff --git a/src/app/js/launch.js b/src/app/js/launch.js new file mode 100644 index 0000000..d14d2ee --- /dev/null +++ b/src/app/js/launch.js @@ -0,0 +1,15 @@ +const update = require("./update"); + +// tells the main process to launch `game_version` +module.exports = (game_version) => { + if (game_version == "vanilla") { + ipcRenderer.send("launch", game_version); + return; + } + + if (update.ns.should_install) { + update.ns(); + } else { + ipcRenderer.send("launch", game_version); + } +} diff --git a/src/app/js/launcher.js b/src/app/js/launcher.js index 1c383b4..913762b 100644 --- a/src/app/js/launcher.js +++ b/src/app/js/launcher.js @@ -1,12 +1,13 @@ const markdown = require("marked").parse; +let launcher = {}; + var servercount; var playercount; var masterserver; -// Changes the main page -// This is the tabs in the sidebar -function page(page) { +// changes the main page, this is the tabs in the sidebar +launcher.change_page = (page) => { let btns = document.querySelectorAll(".gamesContainer button"); let pages = document.querySelectorAll(".mainContainer .contentContainer"); @@ -21,9 +22,9 @@ function page(page) { pages[page].classList.remove("hidden"); btns[page].classList.remove("inactive"); bgHolder.setAttribute("bg", page); -}; page(1) +}; launcher.change_page(1) -function formatRelease(notes) { +launcher.format_release = (notes) => { if (! notes) {return ""} let content = ""; @@ -62,7 +63,7 @@ function formatRelease(notes) { // sets content of `div` to a single release block with centered text // inside it, the text being `lang(lang_key)` -let set_error_content = (div, lang_key) => { +launcher.error = (div, lang_key) => { div.innerHTML = "<div class='release-block'>" + "<p><center>" + @@ -71,42 +72,42 @@ let set_error_content = (div, lang_key) => { "</div>"; } -// Updates the Viper release notes +// updates the Viper release notes ipcRenderer.on("vp-notes", (event, response) => { if (! response) { - return set_error_content( + return launcher.error( vpReleaseNotes, "request.no_vp_release_notes" ) } - vpReleaseNotes.innerHTML = formatRelease(response); + vpReleaseNotes.innerHTML = launcher.format_release(response); }); -// Updates the Northstar release notes +// updates the Northstar release notes ipcRenderer.on("ns-notes", (event, response) => { if (! response) { - return set_error_content( + return launcher.error( nsRelease, "request.no_ns_release_notes" ) } - nsRelease.innerHTML = formatRelease(response); + nsRelease.innerHTML = launcher.format_release(response); }); -async function loadVpReleases() { +launcher.load_vp_notes = async () => { ipcRenderer.send("get-vp-notes"); -}; loadVpReleases(); +}; launcher.load_vp_notes(); -async function loadNsReleases() { +launcher.load_ns_notes = async () => { ipcRenderer.send("get-ns-notes"); -}; loadNsReleases(); +}; launcher.load_ns_notes(); // TODO: We gotta make this more automatic instead of switch statements // it's both not pretty, but adding more sections requires way too much // effort, compared to how it should be. -function showVpSection(section) { +launcher.show_vp = (section) => { if (!["main", "release", "info", "credits"].includes(section)) throw new Error("unknown vp section"); vpMainBtn.removeAttribute("active"); vpReleaseBtn.removeAttribute("active"); @@ -132,8 +133,8 @@ function showVpSection(section) { } } -function showNsSection(section) { - if (!["main", "release", "mods"].includes(section)) { +launcher.show_ns = (section) => { + if (! ["main", "release", "mods"].includes(section)) { throw new Error("unknown ns section"); } @@ -162,7 +163,7 @@ function showNsSection(section) { } } -async function loadServers() { +launcher.check_servers = async () => { serverstatus.classList.add("checking"); try { @@ -206,9 +207,11 @@ async function loadServers() { serverstatus.innerHTML = lang("gui.server.offline"); } -}; loadServers() +}; launcher.check_servers() -// Refreshes every 5 minutes +// refreshes every 5 minutes setInterval(() => { - loadServers(); + launcher.check_servers(); }, 300000) + +module.exports = launcher; diff --git a/src/app/js/localize.js b/src/app/js/localize.js new file mode 100644 index 0000000..6776566 --- /dev/null +++ b/src/app/js/localize.js @@ -0,0 +1,69 @@ +// localizes `string`, removing instances of `%%string%%` with +// `lang("string")` and so forth +function localize_string(string) { + let parts = string.split("%%"); + + // basic checks to make sure `string` has lang strings + if (parts.length == 0 + || string.trim() == "" || ! string.match("%%")) { + return string; + } + + for (let i = 0; i < parts.length; i++) { + // simply checks to make sure it is actually a lang string. + if (parts[i][0] != " " && + parts[i][parts[i].length - 1] != " ") { + + // get string + let lang_str = lang(parts[i]); + + // make sure we got a string back, and if not, do nothing + if (typeof lang_str !== "string") { + continue; + } + + // replace this part with the lang string + parts[i] = lang_str; + } + } + + // return finalized formatted string + return parts.join(""); +} + +// runs `localize_string()` on `el`'s attributes, text nodes and children +function localize_el(el) { + // we don't want to mess with script tags + if (el.tagName == "SCRIPT") {return} + + let attributes = el.getAttributeNames(); + + // run through child nodes + for (let i = 0; i < el.childNodes.length; i++) { + // if the node isn't a text node, we do nothing + if (el.childNodes[i].nodeType != Node.TEXT_NODE) { + continue; + } + + // the node is a text node, so we set its `.textContent` by + // running `format_string()` on it + el.childNodes[i].textContent = + localize_string(el.childNodes[i].textContent) + } + + // run through attributes and run `format_string()` on their values + for (let i = 0; i < attributes.length; i++) { + let attr = el.getAttribute(attributes[i]); + el.setAttribute(attributes[i], localize_string(attr)) + } + + // run `replace_in_el()` on `el`'s children + for (let i = 0; i < el.children.length; i++) { + localize_el(el.children[i]); + } +} + +// localizes lang strings on (almost) all the elements inside `<body>` +module.exports = () => { + localize_el(document.body); +} diff --git a/src/app/js/mods.js b/src/app/js/mods.js index 182bddf..f463ddb 100644 --- a/src/app/js/mods.js +++ b/src/app/js/mods.js @@ -1,17 +1,35 @@ -var mods = {}; +const ipcRenderer = require("electron").ipcRenderer; + +const lang = require("../../lang"); + +const version = require("./version"); +const set_buttons = require("./set_buttons"); + +let mods = {}; + +let mods_list = { + all: [], + enabled: [], + disabled: [] +} + +// returns the list of mods +mods.list = () => { + return mods_list; +} mods.load = (mods_obj) => { modcount.innerHTML = `${lang("gui.mods.count")} ${mods_obj.all.length}`; let normalized_names = []; - + let set_mod = (mod) => { let name = mod.name; if (mod.package) { name = mod.package.package_name; } - let normalized_name = "mod-list-" + normalize(name); + let normalized_name = "mod-list-" + mods.normalize(name); normalized_names.push(normalized_name); @@ -133,8 +151,8 @@ mods.load = (mods_obj) => { let image_el = image_container.querySelector("img") let image_blur_el = image_container.querySelector("img.blur") - if (mod_versions[mod]) { - image_el.src = mod_versions[mod].package.versions[0].icon; + if (browser.mod_versions[mod]) { + image_el.src = browser.mod_versions[mod].package.versions[0].icon; } if (image_el.getAttribute("src") && @@ -148,13 +166,13 @@ mods.load = (mods_obj) => { image_container.parentElement.classList.add("has-icon"); } - if (mod_versions[mod] - && mod_versions[mod].has_update) { + if (browser.mod_versions[mod] + && browser.mod_versions[mod].has_update) { mod_els[i].querySelector(".update").style.display = null; mod_els[i].querySelector(".update").setAttribute( - "onclick", `mod_versions["${mod}"].install()` + "onclick", `browser.mod_versions["${mod}"].install()` ) if (mod_update_els.includes(mod_els[i].id)) { @@ -170,11 +188,11 @@ mods.load = (mods_obj) => { mod_el.classList.add("no-animation"); mod_el.querySelector(".switch").addEventListener("click", () => { - if (mod_versions[mod].local_name) { - mods.toggle(mod_versions[mod].local_name); + if (browser.mod_versions[mod].local_name) { + mods.toggle(browser.mod_versions[mod].local_name); } }) - + mod_els[i].remove(); modsdiv.querySelector(".line").after(mod_el); } else { @@ -210,3 +228,104 @@ mods.toggle = (mod) => { ipcRenderer.send("toggle-mod", mod); } + +mods.install_queue = []; + +// tells the main process to install a mod through the file selector +mods.install_prompt = () => { + set_buttons(false); + ipcRenderer.send("install-mod"); +} + +// tells the main process to directly install a mod from this path +mods.install_from_path = (path) => { + set_buttons(false); + ipcRenderer.send("install-from-path", path); +} + +// tells the main process to install a mod from a URL +mods.install_from_url = (url, dependencies, clearqueue, author, package_name, version) => { + if (clearqueue) {mods.install_queue = []}; + + 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 (! mods.is_installed(pkg[1])) { + newdepends.push({ + pkg: depend, + author: pkg[0], + version: pkg[2], + package_name: pkg[1] + }); + + prettydepends.push(`${pkg[1]} v${pkg[2]} - ${lang("gui.browser.made_by")} ${pkg[0]}`); + } + } + } + + dependencies = newdepends; + } + + if (dependencies && dependencies.length != 0) { + let confirminstall = confirm(lang("gui.mods.confirm_dependencies") + prettydepends.join("\n")); + if (! confirminstall) { + return; + } + } + + set_buttons(false); + ipcRenderer.send("install-from-url", url, author, package_name, version); + + if (dependencies) { + mods.install_queue = dependencies; + } +} + +mods.is_installed = (modname) => { + for (let i = 0; i < mods.list().all.length; i++) { + let mod = mods.list().all[i]; + if (mod.manifest_name) { + if (mod.manifest_name.match(modname)) { + return true; + } + } else if (mod.name.match(modname)) { + return true; + } + } + + return false; +} + +mods.normalize = (items) => { + let main = (string) => { + return string.replaceAll(" ", "") + .replaceAll(".", "").replaceAll("-", "") + .replaceAll("_", "").toLowerCase(); + } + if (typeof items == "string") { + return main(items); + } else { + let newArray = []; + for (let i = 0; i < items.length; i++) { + newArray.push(main(items[i])); + } + + return newArray; + } +} + +// updates the installed mods +ipcRenderer.on("mods", (event, mods_obj) => { + mods_list = mods_obj; + if (! mods_obj) {return} + + mods.load(mods_obj); +}) + +module.exports = mods; diff --git a/src/app/js/popups.js b/src/app/js/popups.js index 7411180..10c6995 100644 --- a/src/app/js/popups.js +++ b/src/app/js/popups.js @@ -43,7 +43,7 @@ popups.list = () => { return document.querySelectorAll(".popup"); } -popups.set_all = (state, exclude_popup) => { +popups.set_all = (state = false, exclude_popup) => { let popups_list = document.querySelectorAll(".popup.shown"); for (let i = 0; i < popups_list.length; i++) { @@ -51,6 +51,8 @@ popups.set_all = (state, exclude_popup) => { continue; } - popups.set(popups_list[i], false, false); + popups.set(popups_list[i], state, false); } } + +module.exports = popups; diff --git a/src/app/js/process.js b/src/app/js/process.js new file mode 100644 index 0000000..a4aba6c --- /dev/null +++ b/src/app/js/process.js @@ -0,0 +1,27 @@ +const ipcRenderer = require("electron").ipcRenderer; + +ipcRenderer.on("log", (_, msg) => { + console.log(msg) +}) + +ipcRenderer.on("alert", (_, data) => { + alert(data.message); + ipcRenderer.send("alert-closed-" + data.id); +}) + +ipcRenderer.on("confirm", (_, data) => { + let confirmed = confirm(data.message); + ipcRenderer.send("confirm-closed-" + data.id, confirmed); +}) + +module.exports = { + // attempts to relaunch the process + relaunch: () => { + ipcRenderer.send("relaunch"); + }, + + // attempts to exit the process (closing Viper) + exit: () => { + ipcRenderer.send("exit") + } +} diff --git a/src/app/js/request.js b/src/app/js/request.js new file mode 100644 index 0000000..29b8883 --- /dev/null +++ b/src/app/js/request.js @@ -0,0 +1,18 @@ +const ipcRenderer = require("electron").ipcRenderer; + +// show a toast message if no Internet connection has been detected. +if (! navigator.onLine) { + ipcRenderer.send("no-internet"); +} + +// invokes `requests.get()` from `src/modules/requests.js` through the +// main process, and returns the output +let request = async (...args) => { + return await ipcRenderer.invoke("request", ...args); +} + +request.delete_cache = () => { + ipcRenderer.send("delete-request-cache"); +} + +module.exports = request; diff --git a/src/app/js/set_buttons.js b/src/app/js/set_buttons.js new file mode 100644 index 0000000..9cb9d3e --- /dev/null +++ b/src/app/js/set_buttons.js @@ -0,0 +1,40 @@ +const ipcRenderer = require("electron").ipcRenderer; + +ipcRenderer.on("set-buttons", (_, state) => { + set_buttons(state); +}) + +// disables or enables certain buttons when for example +// updating/installing Northstar. +module.exports = (state, enable_gamepath_btns) => { + playNsBtn.disabled = ! state; + + let disable_array = (array) => { + for (let i = 0; i < array.length; i++) { + array[i].disabled = ! state; + + if (state) { + array[i].classList.remove("disabled") + } else { + array[i].classList.add("disabled") + } + } + } + + disable_array(document.querySelectorAll([ + "#modsdiv .el button", + ".disable-when-installing", + ".playBtnContainer .playBtn", + "#nsMods .buttons.modbtns button", + "#browser #browserEntries .text button", + ])) + + if (enable_gamepath_btns) { + let gamepath_btns = query_all('*[onclick="gamepath.set()"]'); + + for (let i = 0; i < gamepath_btns.length; i++) { + gamepath_btns[i].disabled = false; + gamepath_btns[i].classList.remove("disabled"); + } + } +} diff --git a/src/app/js/settings.js b/src/app/js/settings.js index 9eb6c76..3aa9c43 100644 --- a/src/app/js/settings.js +++ b/src/app/js/settings.js @@ -1,276 +1,410 @@ -var settings_fuse; +const fs = require("fs"); +const Fuse = require("fuse.js"); +const ipcRenderer = require("electron").ipcRenderer; + +const lang = require("../../lang"); + +const events = require("./events"); +const popups = require("./popups"); + +let settings_fuse; + +// base settings, and future set settings data +let settings_data = { + nsargs: "", + gamepath: "", + nsupdate: true, + autolang: true, + forcedlang: "en", + autoupdate: true, + originkill: false, + zip: "/northstar.zip", + lang: navigator.language, + excludes: [ + "ns_startup_args.txt", + "ns_startup_args_dedi.txt" + ] +} -var Settings = { - default: {settings}, - toggle: (state) => { - Settings.load(); - options.scrollTo(0, 0); +// loads the settings +if (fs.existsSync("viper.json")) { + let iteration = 0; + + // loads the config file, it's loaded in an interval like this in + // case the config file is currently being written to, if we were to + // read from the file during that, then it'd be empty or similar. + // + // and because of that, we check if the file is empty when loading + // it, if so we wait 100ms, then check again, and we keep doing that + // hoping it'll become normal again. unless we've checked it 50 + // times, then we simply give up, and force the user to re-select + // the gamepath, as this'll make the config file readable again. + // + // ideally it'll never have to check those 50 times, it's only in + // case something terrible has gone awry, like if a write got + // interrupted, or a user messed with the file. + // + // were it to truly be broken, then it'd take up to 5 seconds to + // then reset, this is basically nothing, especially considering + // this should only happen in very rare cases... hopefully! + let config_interval = setInterval(() => { + let gamepath = require("./gamepath"); + + iteration++; + + config = json("viper.json") || {}; + + // checks whether `settings_data.gamepath` is set, and if so, + // it'll attempt to load ns_startup_args.txt + let parse_settings = () => { + // if gamepath is not set, attempt to set it + if (settings_data.gamepath.length === 0) { + gamepath.set(false); + } else { + // if the gamepath is set, we'll simply tell the main + // process about it, and it'll then show the main + // renderer BrowserWindow + gamepath.set(true); + } - popups.set("#options", state); - }, - apply: () => { - settings = {...settings, ...Settings.get()}; - ipcRenderer.send("save-settings", Settings.get()); - }, - get: () => { - let opts = {}; - let options = document.querySelectorAll(".option"); - - for (let i = 0; i < options.length; i++) { - let optName = options[i].getAttribute("name"); - if (options[i].querySelector(".actions input")) { - let input = options[i].querySelector(".actions input").value; - if (options[i].getAttribute("type")) { - opts[optName] = input.split(" "); - } else { - opts[optName] = input; - } - } else if (options[i].querySelector(".actions select")) { - opts[optName] = options[i].querySelector(".actions select").value; - } else if (options[i].querySelector(".actions .switch")) { - if (options[i].querySelector(".actions .switch.on")) { - opts[optName] = true; - } else { - opts[optName] = false; - } + // filepath to Northstar's startup args file + let args = path.join(settings_data.gamepath, "ns_startup_args.txt"); + + // check file exists, and that no `nsargs` setting was set + if (! settings_data.nsargs && fs.existsSync(args)) { + // load arguments from file into `settings` + settings_data.nsargs = fs.readFileSync(args, "utf8") || ""; } } - return opts; + // make sure config isn't empty + if (Object.keys(config).length !== 0) { + // add `config` to `settings_data` + settings.set(config); + parse_settings(); + + clearInterval(config_interval); + return; + } + + // we've attempted to load the config 50 times now, give up + if (iteration >= 50) { + // request a new gamepath be set + gamepath.set(false); + clearInterval(config_interval); + } + }, 100) +} else { + require("./gamepath").set(); +} + +ipcRenderer.on("changed-settings", (e, new_settings) => { + // attempt to set `settings_data` to `new_settings` + try { + settings.set(new_settings); + }catch(e) {} +}) + +let settings = { + default: {...settings_data}, + + data: () => {return settings_data}, + + // asks the main process to reset the config/settings file + reset: () => { + ipcRenderer.send("reset-config"); }, - load: () => { - // re-opens any closed categories - let categories = document.querySelectorAll("#options details"); - for (let i = 0; i < categories.length; i++) { - categories[i].setAttribute("open", true); - - // hide categories that aren't for the current platform - let for_platform = categories[i].getAttribute("platform"); - if (for_platform && process.platform != for_platform) { - categories[i].style.display = "none"; - categories[i].setAttribute("perma-hidden", true); - } + + // merges `object` with `settings_data`, unless `no_merge` is set, + // then it replaces it entirely + set: (object, no_merge) => { + if (no_merge) { + settings_data = object; + return; } - let options = document.querySelectorAll(".option"); + settings_data = { + ...settings_data, + ...object + } + }, + + popup: {} +} + +settings.popup.toggle = (state) => { + settings.popup.load(); + options.scrollTo(0, 0); + + popups.set("#options", state); +} + +settings.popup.apply = () => { + settings.set(settings.popup.get()); + ipcRenderer.send("save-settings", settings.popup.get()); +} - for (let i = 0; i < options.length; i++) { - // hide options that aren't for the current platform - let for_platform = options[i].getAttribute("platform"); - if (for_platform && process.platform != for_platform) { - options[i].style.display = "none"; - options[i].setAttribute("perma-hidden", true); +settings.popup.get = () => { + let opts = {}; + let options = document.querySelectorAll(".option"); + + for (let i = 0; i < options.length; i++) { + let optName = options[i].getAttribute("name"); + if (options[i].querySelector(".actions input")) { + let input = options[i].querySelector(".actions input").value; + if (options[i].getAttribute("type")) { + opts[optName] = input.split(" "); + } else { + opts[optName] = input; + } + } else if (options[i].querySelector(".actions select")) { + opts[optName] = options[i].querySelector(".actions select").value; + } else if (options[i].querySelector(".actions .switch")) { + if (options[i].querySelector(".actions .switch.on")) { + opts[optName] = true; + } else { + opts[optName] = false; } + } + } - let optName = options[i].getAttribute("name"); - if (optName == "forcedlang") { - let div = options[i].querySelector("select"); + return opts; +} - div.innerHTML = ""; - let lang_dir = __dirname + "/../lang"; - let langs = fs.readdirSync(lang_dir); - for (let i in langs) { - let lang_file = require(lang_dir + "/" + langs[i]); - let lang_no_extension = langs[i].replace(/\..*$/, ""); +settings.popup.load = () => { + // re-opens any closed categories + let categories = document.querySelectorAll("#options details"); + for (let i = 0; i < categories.length; i++) { + categories[i].setAttribute("open", true); + + // hide categories that aren't for the current platform + let for_platform = categories[i].getAttribute("platform"); + if (for_platform && process.platform != for_platform) { + categories[i].style.display = "none"; + categories[i].setAttribute("perma-hidden", true); + } + } - if (! lang_file.lang || ! lang_file.lang.title) { - continue; - } + let options = document.querySelectorAll(".option"); - let title = lang_file.lang.title; + for (let i = 0; i < options.length; i++) { + // hide options that aren't for the current platform + let for_platform = options[i].getAttribute("platform"); + if (for_platform && process.platform != for_platform) { + options[i].style.display = "none"; + options[i].setAttribute("perma-hidden", true); + } - if (title) { - div.innerHTML += `<option value="${lang_no_extension}">${title}</option>` - } - + let optName = options[i].getAttribute("name"); + if (optName == "forcedlang") { + let div = options[i].querySelector("select"); + + div.innerHTML = ""; + let lang_dir = __dirname + "/../../lang"; + let langs = fs.readdirSync(lang_dir); + for (let i in langs) { + let lang_file = require(lang_dir + "/" + langs[i]); + let lang_no_extension = langs[i].replace(/\..*$/, ""); + + if (! lang_file.lang || ! lang_file.lang.title) { + continue; } - div.value = settings.forcedlang; - continue; + let title = lang_file.lang.title; + + if (title) { + div.innerHTML += `<option value="${lang_no_extension}">${title}</option>` + } + } - if (settings[optName] != undefined) { - // check if setting has a `<select>` - let select_el = options[i].querySelector(".actions select"); - if (select_el) { - // get `<option>` for settings value, if it exists - let option = select_el.querySelector( - `option[value="${settings[optName]}"]` - ) - - // check if it exists - if (option) { - // set the `<select>` to the settings value - select_el.value = settings[optName]; - } else { // use the default value - select_el.value = Settings.default[optName]; - } + div.value = settings_data.forcedlang; + continue; + } - continue; + if (settings_data[optName] != undefined) { + // check if setting has a `<select>` + let select_el = options[i].querySelector(".actions select"); + if (select_el) { + // get `<option>` for settings value, if it exists + let option = select_el.querySelector( + `option[value="${settings_data[optName]}"]` + ) + + // check if it exists + if (option) { + // set the `<select>` to the settings value + select_el.value = settings_data[optName]; + } else { // use the default value + select_el.value = settings.default[optName]; } - switch(typeof settings[optName]) { - case "string": - options[i].querySelector(".actions input").value = settings[optName]; - break - case "object": - options[i].querySelector(".actions input").value = settings[optName].join(" "); - break - case "boolean": - let switchDiv = options[i].querySelector(".actions .switch"); - if (settings[optName]) { - switchDiv.classList.add("on"); - switchDiv.classList.remove("off"); - } else { - switchDiv.classList.add("off"); - switchDiv.classList.remove("on"); - } - break - } + continue; + } + + switch(typeof settings_data[optName]) { + case "string": + options[i].querySelector(".actions input").value = settings_data[optName]; + break + case "object": + options[i].querySelector(".actions input").value = settings_data[optName].join(" "); + break + case "boolean": + let switchDiv = options[i].querySelector(".actions .switch"); + if (settings_data[optName]) { + switchDiv.classList.add("on"); + switchDiv.classList.remove("off"); + } else { + switchDiv.classList.add("off"); + switchDiv.classList.remove("on"); + } + break + } } + } - // create Fuse based on options from `get_search_arr()` - settings_fuse = new Fuse(get_search_arr(), { - keys: ["text"], - threshold: 0.4, - ignoreLocation: true - }) - - // reset search - Settings.search(); - search_el.value = ""; - - ipcRenderer.send("can-autoupdate"); - ipcRenderer.on("cant-autoupdate", () => { - document.querySelector(".option[name=autoupdate]") - .style.display = "none"; - - document.querySelector(".option[name=autoupdate]") - .setAttribute("perma-hidden", true); - }) - }, - switch: (el, state) => { + // create Fuse based on options from `get_search_arr()` + settings_fuse = new Fuse(get_search_arr(), { + keys: ["text"], + threshold: 0.4, + ignoreLocation: true + }) + + // reset search + settings.popup.search(); + search_el.value = ""; + + ipcRenderer.send("can-autoupdate"); + ipcRenderer.on("cant-autoupdate", () => { + document.querySelector(".option[name=autoupdate]") + .style.display = "none"; + + document.querySelector(".option[name=autoupdate]") + .setAttribute("perma-hidden", true); + }) +} + +settings.popup.switch = (el, state) => { + if (state) { + return el.classList.add("on"); + } else if (state === false) { + return el.classList.remove("on"); + } + + if (el.classList.contains("switch") && el.tagName == "BUTTON") { + el.classList.toggle("on"); + } +} + +// searches for `query` in the list of options, hides the options +// that dont match, and shows the one that do, if `query` is falsy, +// it'll simply reset everything back to being visible +settings.popup.search = (query = "") => { + // get list of elements that can be hidden + let search_els = [ + ...document.querySelectorAll(".options .title"), + ...document.querySelectorAll(".options .option"), + ...document.querySelectorAll(".options .buttons"), + ...document.querySelectorAll(".options .details"), + ] + + // this sets the visibility of all elements found in + // `search_els` unless the element has the `perma-hidden` + // attribute set + let set_all = (state) => { + // set `state` to the CSS equivalent if (state) { - return el.classList.add("on"); - } else if (state === false) { - return el.classList.remove("on"); + state = null; + } else { + state = "none"; } - if (el.classList.contains("switch") && el.tagName == "BUTTON") { - el.classList.toggle("on"); + // run through elements + for (let i = 0; i < search_els.length; i++) { + // check that the element shouldn't be perma hidden, and + // if so, hide it correctly. + if (search_els[i].hasAttribute("perma-hidden")) { + search_els[i].style.display = "none"; + continue; + } + + // set the visibility + search_els[i].style.display = state; } - }, + } + + // check if `query` is empty, and reset search if so + if (! query || ! query.trim()) { + set_all(true); + } else { + // hide everything + set_all(false); + } - // searches for `query` in the list of options, hides the options - // that dont match, and shows the one that do, if `query` is falsy, - // it'll simply reset everything back to being visible - search: (query = "") => { - // get list of elements that can be hidden - let search_els = [ - ...document.querySelectorAll(".options .title"), - ...document.querySelectorAll(".options .option"), - ...document.querySelectorAll(".options .buttons"), - ...document.querySelectorAll(".options .details"), + // unhides `el` and relevant elements related to it + let unhide = (el) => { + // list of elements that could be relevant through `el`'s + // `.closest()` function + let closest = [ + ".option", + "details", + ".buttons", ] - // this sets the visibility of all elements found in - // `search_els` unless the element has the `perma-hidden` - // attribute set - let set_all = (state) => { - // set `state` to the CSS equivalent - if (state) { - state = null; - } else { - state = "none"; - } + // run through `closest` + for (let i = 0; i < closest.length; i++) { + // shorthand + let closest_el = el.closest(closest[i]); - // run through elements - for (let i = 0; i < search_els.length; i++) { - // check that the element shouldn't be perma hidden, and - // if so, hide it correctly. - if (search_els[i].hasAttribute("perma-hidden")) { - search_els[i].style.display = "none"; - continue; - } + // was a relevant element found? + if (closest_el) { + // is it supposed to always be hidden? do nothing + if (el.hasAttribute("perma-hidden") + || closest_el.hasAttribute("perma-hidden")) { - // set the visibility - search_els[i].style.display = state; - } - } + break; + } - // check if `query` is empty, and reset search if so - if (! query || ! query.trim()) { - set_all(true); - } else { - // hide everything - set_all(false); - } + // reset visibility + closest_el.style.display = null; - // unhides `el` and relevant elements related to it - let unhide = (el) => { - // list of elements that could be relevant through `el`'s - // `.closest()` function - let closest = [ - ".option", - "details", - ".buttons", - ] - - // run through `closest` - for (let i = 0; i < closest.length; i++) { - // shorthand - let closest_el = el.closest(closest[i]); - - // was a relevant element found? - if (closest_el) { - // is it supposed to always be hidden? do nothing - if (el.hasAttribute("perma-hidden") - || closest_el.hasAttribute("perma-hidden")) { - - break; - } + // are we at a `<details>`? + if (closest[i] == "details") { + // attempt to get a `.title` inside that + // `<details>` element + let title = closest_el.querySelector(".title"); - // reset visibility - closest_el.style.display = null; - - // are we at a `<details>`? - if (closest[i] == "details") { - // attempt to get a `.title` inside that - // `<details>` element - let title = closest_el.querySelector(".title"); - - // did we find a title? - if (title) { - // reset visibility of title and also reset - // open state of `<details>` - title.style.display = null; - closest_el.setAttribute("open", false); - } + // did we find a title? + if (title) { + // reset visibility of title and also reset + // open state of `<details>` + title.style.display = null; + closest_el.setAttribute("open", false); } } } } + } - // do a Fuse.js search with `query` - let res = settings_fuse.search(query); + // do a Fuse.js search with `query` + let res = settings_fuse.search(query); - // if nothing was found, reset all - if (res.length == 0) { - return set_all(true); - } + // if nothing was found, reset all + if (res.length == 0) { + return set_all(true); + } - // run through results and unhide all of them - for (let i = 0; i < res.length; i++) { - unhide(res[i].item.el); - } + // run through results and unhide all of them + for (let i = 0; i < res.length; i++) { + unhide(res[i].item.el); } } // search on key events in search input let search_el = document.body.querySelector("#options .search"); search_el.addEventListener("keyup", () => { - Settings.search(search_el.value); + settings.popup.search(search_el.value); }) // returns a Fuse.js compatible Array for searching through the settings @@ -370,7 +504,12 @@ events.on("popup-changed", () => { document.body.addEventListener("click", (e) => { let el = document.elementFromPoint(e.clientX, e.clientY); - Settings.switch(el); + settings.popup.switch(el); }) -Settings.load(); +settings.popup.load(); + +// sets the lang to the system default +ipcRenderer.send("setlang", settings_data.lang); + +module.exports = settings; diff --git a/src/app/js/toast.js b/src/app/js/toasts.js index 501bf42..83ddf6a 100644 --- a/src/app/js/toast.js +++ b/src/app/js/toasts.js @@ -1,4 +1,6 @@ -function Toast(properties) { +let toasts = {}; + +toasts.show = (properties) => { let toast = { timeout: 3000, fg: "#FFFFFF", @@ -36,7 +38,7 @@ function Toast(properties) { el.id = id; el.addEventListener("click", () => { - dismissToast(id); + toasts.dismiss(id); toast.callback(); }) @@ -53,15 +55,17 @@ function Toast(properties) { el.querySelector(".description").remove(); } - toasts.appendChild(el); + document.getElementById("toasts").appendChild(el); setTimeout(() => { - dismissToast(id); + toasts.dismiss(id); }, toast.timeout) } -function dismissToast(id) { +// dismissed/closes toasts with `id` as their ID +toasts.dismiss = (id) => { id = document.getElementById(id); + if (id) { id.classList.add("hidden"); setTimeout(() => { @@ -73,3 +77,5 @@ function dismissToast(id) { ipcRenderer.on("toast", (_, properties) => { Toast(properties); }) + +module.exports = toasts; diff --git a/src/app/js/tooltip.js b/src/app/js/tooltip.js index 13dcd67..f2d97a0 100644 --- a/src/app/js/tooltip.js +++ b/src/app/js/tooltip.js @@ -128,3 +128,5 @@ document.addEventListener("mousedown", tooltip_event); document.addEventListener("mousemove", tooltip_event); document.addEventListener("mouseenter", tooltip_event); document.addEventListener("mouseleave", tooltip_event); + +module.exports = tooltip; diff --git a/src/app/js/update.js b/src/app/js/update.js new file mode 100644 index 0000000..6aa1b6d --- /dev/null +++ b/src/app/js/update.js @@ -0,0 +1,148 @@ +const ipcRenderer = require("electron").ipcRenderer; + +const lang = require("../../lang"); + +// updates version numbers +ipcRenderer.on("version", (event, versions) => { + vpversion.innerText = versions.vp; + nsversion.innerText = versions.ns; + tf2Version.innerText = versions.tf2; + + if (versions.ns == "unknown") { + let buttons = document.querySelectorAll(".modbtns button"); + + for (let i = 0; i < buttons.length; i++) { + buttons[i].disabled = true; + } + + // since Northstar is not installed, we cannot launch it + update.ns.should_install = true; + playNsBtn.innerText = lang("gui.install"); + } +}); ipcRenderer.send("get-version"); + +// when an update is available it'll ask the user about it +ipcRenderer.on("update-available", () => { + if (confirm(lang("gui.update.available"))) { + ipcRenderer.send("update-now"); + } +}) + +// frontend part of updating Northstar +ipcRenderer.on("ns-update-event", (event, options) => { + let key = options.key; + if (typeof options == "string") { + key = options; + } + + // updates text in update button to `lang(key)` + let update_btn = () => { + document.getElementById("update") + .innerText = `(${lang(key)})`; + } + + switch(key) { + case "cli.update.uptodate_short": + case "cli.update.no_internet": + // initial value + let delay = 0; + + // get current time + let now = new Date().getTime(); + + // check if `update.ns.last_checked` was less than 500ms + // since now, this variable is set when `update.ns()` is + // called + if (now - update.ns.last_checked < 500) { + // if less than 500ms has passed, set `delay` to the + // amount of milliseconds missing until we've hit that + // 500ms threshold + delay = 500 - (now - update.ns.last_checked); + } + + // request up-to-date version numbers + ipcRenderer.send("get-version"); + + // wait `delay`ms + setTimeout(() => { + // set buttons accordingly + update_btn(); + set_buttons(true); + update.ns.progress(false); + playNsBtn.innerText = lang("gui.launch"); + }, delay) + + break; + default: + update_btn(); + + if (options.progress) { + update.ns.progress(options.progress); + } + + if (options.btn_text) { + playNsBtn.innerText = options.btn_text; + } + + set_buttons(false); + break; + } +}) + +let update = { + // deletes install and update cache + delete_cache: () => { + ipcRenderer.send("delete-install-cache"); + }, + + // updates Northstar, `force_update` forcefully updates Northstar, + // causing it to update, even if its already up-to-date + ns: (force_update) => { + update.ns.last_checked = new Date().getTime(); + ipcRenderer.send("update-northstar", force_update); + update.ns.should_install = false; + } +} + +// should Northstar be updated? +update.ns.should_install = false; + +// when was the last time we checked for a Northstar update +update.ns.last_checked = new Date().getTime(); + +// `percent` should be a number between 0 to 100, if it's `false` it'll +// reset it back to nothing instantly, with no animation +update.ns.progress = (percent) => { + // reset button progress + if (percent === false) { + document.querySelector(".contentContainer #nsMain .playBtn") + .style.setProperty("--progress", "unset"); + + return; + } + + percent = parseInt(percent); + + // make sure we're dealing with a number + if (isNaN(percent) || typeof percent !== "number") { + return false; + } + + // limit percent, while this barely has a difference, if you were to + // set a very high number, the CSS would then use a very high + // number, not great. + if (percent > 100) { + percent = 100; + } else if (percent < 0) { + percent = 0; + } + + // invert number to it works in the CSS + percent = 100 - percent; + + // set the CSS progress variable + document.querySelector(".contentContainer #nsMain .playBtn") + .style.setProperty("--progress", percent + "%"); +} + +module.exports = update; diff --git a/src/app/js/misc.js b/src/app/js/version.js index b35f239..97223f9 100644 --- a/src/app/js/misc.js +++ b/src/app/js/version.js @@ -1,4 +1,4 @@ -version = { +module.exports = { is_newer: (version1, version2) => { version1 = version.format(version1, true).split("."); version2 = version.format(version2, true).split("."); |