diff options
Diffstat (limited to 'src/app')
-rw-r--r-- | src/app/browser.js | 259 | ||||
-rw-r--r-- | src/app/css/dragui.css | 58 | ||||
-rw-r--r-- | src/app/css/launcher.css | 328 | ||||
-rw-r--r-- | src/app/css/popups.css | 371 | ||||
-rw-r--r-- | src/app/css/theming.css | 62 | ||||
-rw-r--r-- | src/app/css/toasts.css | 52 | ||||
-rw-r--r-- | src/app/css/webview.css | 16 | ||||
-rw-r--r-- | src/app/icons/external.png | bin | 0 -> 9764 bytes | |||
-rw-r--r-- | src/app/icons/game.png | bin | 0 -> 1454 bytes | |||
-rw-r--r-- | src/app/icons/language.png | bin | 0 -> 7132 bytes | |||
-rw-r--r-- | src/app/icons/updates.png | bin | 0 -> 10300 bytes | |||
-rw-r--r-- | src/app/index.html | 47 | ||||
-rw-r--r-- | src/app/lang.js | 2 | ||||
-rw-r--r-- | src/app/launcher.js | 99 | ||||
-rw-r--r-- | src/app/main.css | 742 | ||||
-rw-r--r-- | src/app/main.js | 126 | ||||
-rw-r--r-- | src/app/settings.js | 14 |
17 files changed, 1354 insertions, 822 deletions
diff --git a/src/app/browser.js b/src/app/browser.js index 40670bc..db68d31 100644 --- a/src/app/browser.js +++ b/src/app/browser.js @@ -2,9 +2,24 @@ const Fuse = require("fuse.js"); var fuse; var packages = []; +var packagecount = 0; + var Browser = { maxentries: 50, filters: { + getpkgs: () => { + let pkgs = []; + let other = []; + for (let i in packages) { + if (! Browser.filters.isfiltered(packages[i].categories)) { + pkgs.push(packages[i]); + } else { + other.push(packages[i]); + } + } + + return pkgs; + }, get: () => { let filtered = []; let unfiltered = []; @@ -27,6 +42,19 @@ var Browser = { let filtered = Browser.filters.get().filtered; let unfiltered = Browser.filters.get().unfiltered; let state = false; + + let filters = [ + "Mods", "Skins", + "Client-side", "Server-side", + ]; + + let newcategories = []; + for (let i = 0; i < categories.length; i++) { + if (filters.includes(categories[i])) { + newcategories.push(categories[i]); + } + }; categories = newcategories; + if (categories.length == 0) {return true} for (let i = 0; i < categories.length; i++) { if (filtered.includes(categories[i])) { @@ -53,14 +81,14 @@ var Browser = { let spacing = parseInt(getComputedStyle(filters).getPropertyValue("--spacing")); filters.style.top = filterRect.bottom - spacing; - filters.style.right = filterRect.right - filterRect.left + filterRect.width; + filters.style.right = filterRect.right - filterRect.left + filterRect.width - (spacing / 2); }, }, toggle: (state) => { if (state) { browser.scrollTo(0, 0); - overlay.classList.add("shown") - browser.classList.add("shown") + overlay.classList.add("shown"); + browser.classList.add("shown"); if (browserEntries.querySelectorAll(".el").length == 0) { Browser.loadfront(); @@ -69,18 +97,21 @@ var Browser = { } else if (! state) { if (state != undefined) { Browser.filters.toggle(false); - overlay.classList.remove("shown") - browser.classList.remove("shown") + overlay.classList.remove("shown"); + browser.classList.remove("shown"); + preview.classList.remove("shown"); return } } browser.scrollTo(0, 0); - overlay.classList.toggle("shown") - browser.classList.toggle("shown") + overlay.classList.toggle("shown"); + browser.classList.toggle("shown"); }, loadfront: async () => { Browser.loading(); + + packagecount = 0; if (packages.length < 1) { packages = await (await fetch("https://northstar.thunderstore.io/api/v1/package/")).json(); @@ -90,8 +121,15 @@ var Browser = { }) } - for (let i in packages) { - new BrowserElFromObj(packages[i]); + let pkgs = Browser.filters.getpkgs(); + for (let i in pkgs) { + if (packagecount >= Browser.maxentries) { + Browser.endoflist(); + break + } + + new BrowserElFromObj(pkgs[i]); + packagecount++; } }, loading: (string) => { @@ -107,21 +145,53 @@ var Browser = { browserEntries.innerHTML = `<div class="loading">${lang('gui.browser.loading')}</div>`; } }, - endoflist: () => { - if (browserEntries.querySelector(".message")) {return} - browserEntries.innerHTML += `<div class="message">${lang('gui.browser.endoflist')}</div>` + endoflist: (isEnd) => { + let pkgs = []; + let filtered = Browser.filters.getpkgs(); + for (let i = 0; i < filtered.length; i++) { + if ([packagecount + i]) { + pkgs.push(filtered[packagecount + i]); + } else { + break + } + } + + if (browserEntries.querySelector(".message")) { + browserEntries.querySelector(".message").remove(); + } + + if (pkgs.length == 0 || isEnd) { + Browser.msg(`${lang('gui.browser.endoflist')}`); + return + } + + Browser.msg(`<button id="loadmore">${lang("gui.browser.loadmore")}</button>`); + loadmore.addEventListener("click", () => { + Browser.loadpkgs(pkgs); + Browser.endoflist(pkgs); + }) }, search: (string) => { Browser.loading(); let res = fuse.search(string); if (res.length < 1) { - Browser.loading(lang("gui.browser.noresults")) + Browser.loading(lang("gui.browser.noresults")); return } + packagecount = 0; + + let count = 0; for (let i = 0; i < res.length; i++) { + if (count >= Browser.maxentries) {break} + if (Browser.filters.isfiltered(res[i].item.categories)) {continue} new BrowserElFromObj(res[i].item); + count++; + } + + if (count < 1) { + Browser.loading(lang("gui.browser.noresults")); } }, setbutton: (mod, string) => { @@ -157,6 +227,58 @@ var Browser = { } }, 1501) } + }, + loadpkgs: (pkgs, clear) => { + if (clear) {packagecount = 0} + + if (browserEntries.querySelector(".message")) { + browserEntries.querySelector(".message").remove(); + } + + let count = 0; + for (let i in pkgs) { + if (count >= Browser.maxentries) { + if (pkgs[i] === undefined) { + Browser.endoflist(true); + } + + Browser.endoflist(); + break + } + + try { + new BrowserElFromObj(pkgs[i]); + }catch(e) {} + + count++; + packagecount++; + } + }, + msg: (html) => { + let msg = document.createElement("div"); + msg.classList.add("message"); + msg.innerHTML = html; + + browserEntries.appendChild(msg); + } +} + +function openExternal(url) { + require("electron").shell.openExternal(url); +} + +var view = document.querySelector(".popup#preview webview"); +var Preview = { + show: () => { + preview.classList.add("shown"); + }, + hide: () => { + preview.classList.remove("shown"); + }, + set: (url, autoshow) => { + if (autoshow != false) {Preview.show()} + view.src = url; + document.querySelector("#preview #external").setAttribute("onclick", `openExternal("${url}")`); } } @@ -171,16 +293,14 @@ function BrowserElFromObj(obj) { download: pkg.download_url, version: pkg.version_number, categories: pkg.categories, - description: pkg.description + description: pkg.description, + dependencies: pkg.dependencies, }) } function BrowserEl(properties) { if (Browser.filters.isfiltered(properties.categories)) {return} - let entries = browser.querySelectorAll(".el").length; - if (entries == Browser.maxentries) {Browser.endoflist();return} - properties = { title: "No name", version: "1.0.0", @@ -230,25 +350,30 @@ function BrowserEl(properties) { } } } - - browserEntries.innerHTML += ` - <div class="el" id="mod-${normalize(properties.title)}"> - <div class="image"> - <img src="${properties.image}"> - </div> - <div class="text"> - <div class="title">${properties.title}</div> - <div class="description">${properties.description}</div> - <button onclick="installFromURL('${properties.download}')">${installstr}</button> - <button onclick="require('electron').shell.openExternal('${properties.url}')">${lang('gui.browser.info')}</button> - <button class="visual">${properties.version}</button> - <button class="visual">${lang("gui.browser.madeby")} ${properties.author}</button> - </div> + + let entry = document.createElement("div"); + entry.classList.add("el"); + entry.id = `mod-${normalize(properties.title)}`; + + entry.innerHTML = ` + <div class="image"> + <img src="${properties.image}"> + <img class="blur" src="${properties.image}"> + </div> + <div class="text"> + <div class="title">${properties.title}</div> + <div class="description">${properties.description}</div> + <button class="install" onclick='installFromURL("${properties.download}", ${JSON.stringify(properties.dependencies)}, true)'>${installstr}</button> + <button class="info" onclick="Preview.set('${properties.url}')">${lang('gui.browser.view')}</button> + <button class="visual">${properties.version}</button> + <button class="visual">${lang("gui.browser.madeby")} ${properties.author}</button> </div> ` + + browserEntries.appendChild(entry); } -ipcRenderer.on("removedmod", (event, mod) => { +ipcRenderer.on("removed-mod", (event, mod) => { setButtons(true); Browser.setbutton(mod.name, lang("gui.browser.install")); if (mod.manifestname) { @@ -256,7 +381,7 @@ ipcRenderer.on("removedmod", (event, mod) => { } }) -ipcRenderer.on("failedmod", (event, modname) => { +ipcRenderer.on("failed-mod", (event, modname) => { setButtons(true); new Toast({ timeout: 10000, @@ -266,7 +391,17 @@ ipcRenderer.on("failedmod", (event, modname) => { }) }) -ipcRenderer.on("installedmod", (event, mod) => { +ipcRenderer.on("no-internet", (event, modname) => { + setButtons(true); + new Toast({ + timeout: 10000, + scheme: "error", + title: lang("gui.toast.noInternet.title"), + description: lang("gui.toast.noInternet.desc") + }) +}) + +ipcRenderer.on("installed-mod", (event, mod) => { setButtons(true); Browser.setbutton(mod.name, lang("gui.browser.reinstall")); @@ -284,14 +419,21 @@ ipcRenderer.on("installedmod", (event, mod) => { title: lang("gui.toast.title.installed"), description: mod.name + " " + lang("gui.toast.desc.installed") }) + + if (installqueue.length != 0) { + installFromURL("https://thunderstore.io/package/download/" + installqueue[0]); + installqueue.shift(); + } }) function normalize(items) { let main = (string) => { - return string.replaceAll(" ", "").replaceAll(".", "").replaceAll("-", "").replaceAll("_", "").toLowerCase() + return string.replaceAll(" ", "") + .replaceAll(".", "").replaceAll("-", "") + .replaceAll("_", "").toLowerCase(); } if (typeof items == "string") { - return main(items) + return main(items); } else { let newArray = []; for (let i = 0; i < items.length; i++) { @@ -322,11 +464,50 @@ search.addEventListener("keyup", () => { } }) -browser.addEventListener("scroll", () => { - Browser.filters.toggle(false); +let events = ["scroll", "mousedown", "touchdown"]; +events.forEach((event) => { + browser.addEventListener(event, () => { + Preview.hide(); + + let mouseAt = document.elementsFromPoint(mouseX, mouseY); + if (! mouseAt.includes(document.querySelector("#filter")) + && ! mouseAt.includes(document.querySelector(".overlay"))) { + Browser.filters.toggle(false); + } + }) +}); + +view.addEventListener("dom-ready", () => { + let css = [ + fs.readFileSync(__dirname + "/css/theming.css", "utf8"), + fs.readFileSync(__dirname + "/css/webview.css", "utf8") + ] + + view.insertCSS(css.join(" ")); +}) + +view.addEventListener("did-stop-loading", () => { + view.style.display = "flex"; + setTimeout(() => { + view.classList.remove("loading"); + }, 200) +}) + +view.addEventListener("did-start-loading", () => { + view.style.display = "none"; + view.classList.add("loading"); +}) + +let mouseY = 0; +let mouseX = 0; +browser.addEventListener("mousemove", (event) => { + mouseY = event.clientY; + mouseX = event.clientX; }) let checks = document.querySelectorAll(".check"); for (let i = 0; i < checks.length; i++) { - checks[i].setAttribute("onclick", "this.classList.toggle('checked');Browser.loadfront();search.value = ''") + checks[i].setAttribute("onclick", + "this.classList.toggle('checked');Browser.loadfront();search.value = ''" + ) } diff --git a/src/app/css/dragui.css b/src/app/css/dragui.css new file mode 100644 index 0000000..fb5b1a7 --- /dev/null +++ b/src/app/css/dragui.css @@ -0,0 +1,58 @@ +@import "theming.css"; + +/* + This stylesheet is meant for the DragUI, i.e the UI that pops up when dragging + a modfile over the window. +*/ + +#dragUI { + top: 0; + left: 0; + right: 0; + bottom: 0; + color: white; + opacity: 0.0; + position: fixed; + z-index: 1000000; + pointer-events: none; + background: var(--bg); + backdrop-filter: blur(15px); + transition: 0.1s ease-in-out; +} + +#dragUI.shown { + opacity: 1.0; + pointer-events: all; +} + +#dragUI #dragitems { + --size: 25vw; + top: 50%; + left: 50%; + opacity: 0.6; + position: absolute; + text-align: center; + width: var(--size); + height: var(--size); + margin-top: calc(var(--size) / 2 * -1); + margin-left: calc(var(--size) / 2 * -1); +} + +#dragUI #dragitems #icon { + width: 100%; + height: 100%; + filter: invert(1); + transform: scale(0.45); + background-size: cover; + background-image: url("../icons/download.png"); + transition: 0.1s ease-in-out; +} + +#dragUI.shown #dragitems #icon { + transform: scale(0.5); +} + +#dragUI #dragitems #text { + top: -5vw; + position: relative; +} diff --git a/src/app/css/launcher.css b/src/app/css/launcher.css new file mode 100644 index 0000000..ce54ddf --- /dev/null +++ b/src/app/css/launcher.css @@ -0,0 +1,328 @@ +@import "theming.css"; + +/* + This stylesheet is meant for various elements around the launcher, + notably the navbar, launch buttons, and sidebar. +*/ + +.gamesContainer { + width: 10%; + height: 100%; + min-width: 95px; + max-width: 120px; + + float: left; + display: flex; + flex-wrap: wrap; + align-content: center; +} + +.mainContainer { + height: 100%; + flex-grow: 1; + display: flex; + position: relative; +} + +/* nav bar buttons */ +.gamesContainer button { + background-size: 90%; + background-position: center; + background-repeat: no-repeat; + + border: none; + transition: 0.3s ease-in-out; + background-color: transparent; + + + margin: 20px; + position: relative; + box-sizing: border-box; + flex-basis: calc(100% - 10px); +} + +.gamesContainer button.inactive { + opacity: 0.5; + transform: scale(0.9); +} + +.gamesContainer button::before { + content: ""; + display: block; + padding-top: 100%; +} + +.gamesContainer button .content { + width: 100%; + height: 100%; + top: 0; left: 0; + position: absolute; +} + +#vpBtn {background-image: url("../icons/viper.png")} +#nsBtn {background-image: url("../icons/northstar.png")} +#tfBtn { + background-image: url("../icons/titanfall2.png"); + background-size: 69%; /* nice */ +} + +.contentContainer { + width: 85%; + color: white; + flex-grow: 1; + opacity: 1.0; + margin-left: 5%; + position: absolute; + transition: 0.15s ease-in-out; +} + +.contentContainer.hidden { + opacity: 0.0; + pointer-events: none; +} + +.contentMenu { + padding: 0; + flex-grow: 1; + display: flex; + font-size: 20px; + list-style: none; + margin-bottom: 0; + align-items: center; + justify-content: center; + margin-top: var(--padding); +} + +/* some sections do not need space between them and menu */ +#nsMain, #nsRelease, #vpReleaseNotes, .playBtnContainer { + margin-top: 0 !important; +} + +.contentMenu li { + opacity: 0.6; + margin: 0 26px; + cursor: pointer; + font-weight: 700; + text-align: center; + height: fit-content; + transition: opacity 0.3s ease-in-out; +} + +.contentMenu li:last-child {margin-right: 0px} +.contentMenu li:first-child {margin-left: 0px} + +.contentMenu li:hover {opacity: 0.7} + +.contentMenu li[active] { + opacity: 1.0; + pointer-events: none; +} + +.contentMenu li::after { + top: 10px; + width: 30px; + height: 5px; + opacity: 0.0; + content: " "; + display: block; + text-align: center; + position: relative; + border-radius: 50px; + left: calc(50% - 15px); + background: rgb(var(--red)); + transition: 0.2s ease-in-out; +} + +.contentMenu li[active]::after { + top: 5px; + opacity: 1.0; +} + +.section { + opacity: 1.0; + position: fixed; + right: calc(var(--padding) * 2); + left: calc(100px + var(--padding)); + transition: opacity 0.15s ease-in-out; +} + +.section.hidden { + opacity: 0.0; + pointer-events: none; +} + +.contentBody img {max-width: 100%} +.contentBody .img {text-align: center} + +.contentBody .section > :first-child:not(.img) { + margin-top: 35px; +} + +.contentContainer .playBtnContainer { + text-align: center; +} + +.contentContainer .playBtn { + width: 27%; + height: 11%; + border: none; + color: white; + padding: 20px; + font-size: 24px; + font-weight: bold; + margin-top: 100px; + margin-bottom: 10px; + border-radius: 10px; + background: var(--redbg); + transition: 0.2s ease-in-out; + filter: drop-shadow(0px 8px 5px rgba(0, 0, 0, 0.1)); +} + +.contentContainer .playBtn:hover { + transform: scale(1.05); + filter: drop-shadow(0px 5px 15px rgba(0, 0, 0, 0.3)) brightness(110%); +} + +.contentContainer .playBtn:active { + opacity: 0.7;transform: scale(0.98); + filter: drop-shadow(0px 5px 10px rgba(0, 0, 0, 0.4)); +} + +.contentContainer #nsMain .playBtn { + background: var(--bluebg); +} + +#nsContent .contentMenu { + margin-bottom: 0; +} + +.contentBody .img img { + transform: scale(0.85); +} + +.contentBody .img { + width: 100%; + text-align: center; +} + +#nsRelease, #vpReleaseNotes { + height: 80vh; + overflow-y: scroll; + flex-direction: column; + margin-top: 10px !important; +} + +.inline * { + display: inline-block; +} + +#vpMain { + margin-top: 140px; + text-align: center; +} + +#vpMain img { + width: 20%; +} + +#vpVersion { + font-size: 16px; +} + +.simplebar-scrollbar:before { + background: rgb(var(--red)) !important; +} + +#installmod {background: rgb(var(--blue))} +#findmod {background: rgb(var(--blue2))} + +#togglemod {background: rgb(var(--orange))} +#toggleall {background: rgb(var(--orange2))} + +#removemod {background: rgb(var(--red))} +#removeall {background: rgb(var(--red2))} +button:disabled { + opacity: 0.5; + pointer-events: none; +} + +button.visual { + opacity: 1.0; + padding-right: 0px; + pointer-events: none; + background: transparent !important; +} + +code { + font-size: 16px; + padding: 2px 5px; + border-radius: 3px; + background-color: #00000070; +} + +#nsMods .line { + display: flex; + align-items: center; + margin-top: calc(var(--padding) / 2); +} + +#modsdiv { + height: 50vh; + overflow-y: scroll; + border-radius: 5px; + background: var(--bg); + backdrop-filter: blur(15px); + padding: calc(var(--padding) / 4); +} + +#modsdiv .mod { + display: flex; + border-radius: 5px; + transition: 0.1s ease-in-out; + margin: calc(var(--padding) / 3); + padding: calc(var(--padding) / 3); +} + +#modsdiv .mod.selected { + background: var(--selbg); +} + +#modsdiv .mod .disabled, .modbtns { + margin-left: auto; +} + +.modbtns button { + margin-left: var(--spacing); + --spacing: calc(var(--padding) / 3); + margin-top: calc(var(--spacing) / 2); + margin-bottom: calc(var(--spacing) / 2); +} + +#serverstatus { + --spacing: calc(var(--padding) / 5); + + transition-duration: 0.2s; + transition-timing-function: ease-in-out; + transition-property: background, opacity; + + opacity: 0.0; + display: block; + margin: 0 auto; + font-weight: 700; + width: fit-content; + color: transparent; + border-radius: 50px; + flex-basis: max-content; + background: transparent; + margin-top: calc(var(--spacing) * 2); + padding: var(--spacing) calc(var(--spacing) * 3); +} + +#serverstatus.up, +#serverstatus.down { + color: white; + opacity: 1.0; +} + +#serverstatus.up {background: rgb(var(--blue));} +#serverstatus.down {background: rgb(var(--red));} diff --git a/src/app/css/popups.css b/src/app/css/popups.css new file mode 100644 index 0000000..826955b --- /dev/null +++ b/src/app/css/popups.css @@ -0,0 +1,371 @@ +@import "theming.css"; + +/* + This stylesheet is meant for the various popups we use, whether it be the + previewer, the browser, the settings menu, or anything alike. +*/ + +.popup { + --spacing: var(--padding); + + z-index: 2; + opacity: 0.0; + position: fixed; + overflow-y: scroll; + top: var(--spacing); + pointer-events: none; + left: var(--spacing); + background: var(--bg); + right: var(--spacing); + bottom: var(--spacing); + transform: scale(0.98); + backdrop-filter: blur(15px); + border-radius: calc(var(--padding) / 3); + + transition-duration: 0.15s; + transition-property: opacity, transform; + transition-timing-function: ease-in-out; +} + +.popup.shown { + opacity: 1.0; + pointer-events: all; + transform: scale(1.0); +} + +.popup.small { + left: 20vw; + right: 20vw; + top: calc(var(--padding) * 2); + bottom: calc(var(--padding) * 2); +} + +#overlay { + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1; + opacity: 0.0; + position: fixed; + background: var(--bg); + pointer-events: none; + transition: opacity 0.15s ease-in-out; +} + +#overlay.shown { + opacity: 0.8; + pointer-events: all; +} + +/* browser/preview popup { */ +@keyframes fadein { + 0% {opacity: 0.0} + 100% {opacity: 1.0} +} + +#browserEntries { + display: flex; + flex-wrap: wrap; +} + +.popup webview { + width: 78%; + margin: 0 auto; + filter: opacity(1.0); + transition: filter 0.15s ease-in-out; + margin-top: calc(var(--spacing) / 2); + height: calc(100% - calc(var(--spacing) / 2)); +} + +.popup webview.loading { + filter: opacity(0.0); + pointer-events: none; +} + +.popup .el, .popup .misc, .popup .loading { + --spacing: calc(var(--padding) / 2); + --height: calc(var(--padding) * 3.5); + --mischeight: calc(var(--padding) * 1.5); + + animation-duration: 0.15s; + animation-iteration-count: 1; + animation-name: fadein; + animation-fill-mode: forwards; + animation-timing-function: ease-in-out; + + opacity: 0.0; + transition: 0.15s ease-in-out; +} + +.popup .el, .popup #search, +.popup #close, .popup .misc button, +.option .actions select, .option .actions input { + color: white; + display: flex; + align-items: center; + height: var(--height); + margin: var(--spacing); + padding: var(--spacing); + background: var(--selbg); + border-radius: var(--spacing); + width: calc(50% - var(--spacing) * 4); +} + +.popup .misc, .popup #search, .option .actions input { + --height: var(--mischeight); +} + +.popup .misc { + display: flex; +} + +.popup .misc.vertical { + display: block; +} + +.popup .misc.fixed { + width: 100%; + position: fixed; +} + +.popup #search, +.option .actions input, +.option .actions select { + border: none; + outline: none; + transition: filter 0.15s ease-in-out; + width: calc(100% - var(--spacing) * 2); +} + +.popup #search:focus, +.option .actions input:focus, +.option .actions button:active { + filter: brightness(1.5); +} + +.popup .misc button { + --height: calc(var(--padding) * 1.5); + + padding: 0px; + margin-left: 0px; + padding: 0px !important; + width: var(--height) !important; +} + +.popup .misc button img { + opacity: 0.6; + width: var(--height); + transform: scale(0.5); + height: var(--height) !important; +} + +.popup .misc button:last-child { + margin-left: 0px !important; +} + +.popup#preview #close, +.popup .misc.vertical button { + margin: var(--spacing) var(--spacing) 0 auto !important; +} + +.popup .loading { + width: 100%; + color: white; + display: flex; + position: absolute; + text-align: center; + align-items: center; + justify-content: center; + height: calc(100% - var(--mischeight) - var(--height)); +} + +.popup .message { + color: white; + text-align: center; + margin: var(--padding); + width: calc(100% - var(--padding)); +} + +.popup .el .image, .popup .el .image img { + width: var(--height); + height: var(--height); + margin-right: var(--spacing); + border-radius: var(--spacing); +} + +.popup .el .image img.blur { + z-index: -1; + position: relative; + filter: blur(10px); + top: calc(var(--height) * -1 + 5px); +} + +.popup .el .text { + overflow: hidden; +} + +.popup .el .title, .popup .el .description { + height: 1.2em; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.popup .el .title { + font-size: 1.2em; + font-weight: 700; +} + +.popup .message #loadmore { + background: rgb(var(--blue2)); +} + +.popup .el .description {font-size: 0.8em} +.popup .el button { + background: rgb(var(--blue)); + margin-top: var(--spacing); +} + +.popup .el button.info { + background: rgb(var(--blue2)); +} +/* } */ + +/* settings popup { */ + +.popup .options { + color: white; + margin: calc(var(--padding) / 2); +} + +.popup .options .option { + width: 100%; + display: flex; + margin-bottom: var(--padding); + justify-content: space-between; +} + +.popup .overlay { + z-index: 1; + color: white; + opacity: 0.0; + position: fixed; + pointer-events: none; + transform: scale(0.9); + background: var(--selbg); + backdrop-filter: blur(15px); + transition: 0.15s ease-in-out; + padding: calc(var(--spacing) / 2); + border-radius: calc(var(--spacing) / 2); +} + +.popup .overlay.shown { + opacity: 1.0; + pointer-events: all; + transform: scale(1.0); +} + +#options.popup .misc button { + margin-left: 0px; + width: auto !important; + padding-right: calc(var(--padding) / 2) !important; +} + +.check { + display:flex; + cursor: pointer; +} + +.check::before { + width: 1em; + height: 1em; + content: " "; + background-size: 75%; + filter: brightness(1.3); + background-position: center; + background-repeat: no-repeat; + transition: 0.15s ease-in-out; + background-color: var(--selbg); + margin-right: calc(var(--spacing) / 3); + border-radius: calc(var(--spacing) / 4); +} + +.check.checked::before { + background-color: rgb(var(--red)); + background-image: url(../icons/check.png); +} + +.option .text {font-weight: 600} +.option .text .desc { + opacity: 0.8; + font-weight: 500; + font-size: 0.9em; + max-width: 400px; + margin-top: calc(var(--padding) / 3); +} + +.option .actions input, .option .actions select { + width: 100%; + margin: 0px; + --spacing: calc(var(--padding) / 3); +} + +.option[type=array] .actions input { + word-spacing: 15px; + margin-right: 15vw; +} + +.option .actions button { + background: var(--selbg); +} + +.switch { + width: 50px; + height: 25px; + border-radius: 50px; +} + +.switch.on { + background: rgba(var(--red), 0.2) !important; +} + +.switch::after { + left: -5px; + width: 15px; + height: 15px; + content: " "; + display: block; + background: red; + position: relative; + border-radius: 50px; + background: var(--bg); + transition: 0.2s ease-in-out; +} + +.switch.on::after { + left: 15px; + width: 20px; + opacity: 0.5; + background: rgb(var(--red)); +} + +.switch.on:hover::after { + transform: scale(1.2); +} + +.title { + display: flex; +} + +.title img { + width: 30px; + height: 30px; + margin: auto 0; +} + +.title h2 { + margin-left: calc(var(--padding) / 3); +} + +/* } */ diff --git a/src/app/css/theming.css b/src/app/css/theming.css new file mode 100644 index 0000000..2d45b1d --- /dev/null +++ b/src/app/css/theming.css @@ -0,0 +1,62 @@ +/* + the only reason some of these properties have an !important property + is for it to overwrite Thunderstore's CSS in the preview <webview> +*/ + +:root { + --red: 199, 119, 127 !important; + --red2: 181 97 105; + + --blue: 129, 161, 193; + --blue2: 139, 143, 185; + + --orange: 213, 151, 131; + --orange2: 197 129 107; + + + --padding: 25px; + + --bg: rgba(0, 0, 0, 0.5); + --selbg: rgba(80, 80, 80, 0.5); + --redbg: linear-gradient(45deg, rgb(var(--red)), #FA4343); + --bluebg: linear-gradient(45deg, rgb(var(--blue)), #7380ED); +} + +body, button, input { + font-family: "Roboto", sans-serif !important; +} + +body, button, img, a { + user-select: none; + -webkit-user-drag: none; +} + +a { + text-decoration: none !important; + color: rgb(var(--red)) !important; + transition: filter 0.2s ease-in !important; +} + +a:hover { + filter: brightness(80%) !important; +} + + +::-webkit-scrollbar { + width: 8px !important; +} + +::-webkit-scrollbar-track { + border-radius: 10px !important; + background: transparent !important; +} + +::-webkit-scrollbar-thumb { + border-radius: 10px !important; + background: rgb(var(--red)) !important; +} + +::selection { + color: black !important; + background: rgb(var(--red)) !important; +} diff --git a/src/app/css/toasts.css b/src/app/css/toasts.css new file mode 100644 index 0000000..57ba055 --- /dev/null +++ b/src/app/css/toasts.css @@ -0,0 +1,52 @@ +@import "theming.css"; + +#toasts { + position: fixed; + z-index: 100000; + right: calc(var(--padding) * 1.5); + bottom: calc(var(--padding) * 1.5); +} + +@keyframes bodyfadeaway { + 0% {opacity: 0.0; transform: scale(0.95)} + 100% {opacity: 1.0; transform: scale(1.0)} +} + +#toasts .toast { + width: 300px; + opacity: 0.0; + cursor: pointer; + overflow: hidden; + max-height: 100vh; + background: #FFFFFF; + transform: scale(0.95); + transition: 0.2s ease-in-out; + padding: calc(var(--padding) / 2); + margin-top: calc(var(--padding) / 2); + border-radius: calc(var(--padding) / 2.5); + box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.2); + + animation-duration: 0.2s; + animation-iteration-count: 1; + animation-name: bodyfadeaway; + animation-fill-mode: forwards; + animation-timing-function: ease-in-out; +} + +#toasts .toast.hidden { + margin-top: 0px; + max-height: 0px; + padding-top: 0px; + padding-bottom: 0px; + filter: opacity(0.0); + transform: scale(0.95); +} + +#toasts .toast:not(.hidden):hover {filter: opacity(0.9)} +#toasts .toast:not(.hidden):active {filter: opacity(0.8)} + +.toast .description { + opacity: 0.8; + font-size: 0.8em; + font-weight: 600; +} diff --git a/src/app/css/webview.css b/src/app/css/webview.css new file mode 100644 index 0000000..982ebfc --- /dev/null +++ b/src/app/css/webview.css @@ -0,0 +1,16 @@ +body { + overflow-x: hidden; + background: transparent !important; + background-color: transparent !important; +} + +#ncmp_tool, +.navbar, .bottom-padding, +.card-header, .breadcrumb, +.list-group, .mb-4, .my-2, .mt-2, +#thunderstore-mod-manager-ad-alert { + display: none !important; +} + +.mt-2.mb-2 {display: block !important} +.card {transform: translateY(-1.0rem)} diff --git a/src/app/icons/external.png b/src/app/icons/external.png Binary files differnew file mode 100644 index 0000000..0b4f99e --- /dev/null +++ b/src/app/icons/external.png diff --git a/src/app/icons/game.png b/src/app/icons/game.png Binary files differnew file mode 100644 index 0000000..7db058f --- /dev/null +++ b/src/app/icons/game.png diff --git a/src/app/icons/language.png b/src/app/icons/language.png Binary files differnew file mode 100644 index 0000000..011e1f9 --- /dev/null +++ b/src/app/icons/language.png diff --git a/src/app/icons/updates.png b/src/app/icons/updates.png Binary files differnew file mode 100644 index 0000000..4b505ee --- /dev/null +++ b/src/app/icons/updates.png diff --git a/src/app/index.html b/src/app/index.html index 794e209..5143520 100644 --- a/src/app/index.html +++ b/src/app/index.html @@ -36,7 +36,10 @@ </button> </div> <div class="options"> - <h2>%%gui.settings.title.ns%%</h2> + <div class="title"> + <img src="icons/game.png"> + <h2>%%gui.settings.title.ns%%</h2> + </div> <div class="option" name="nsargs"> <div class="text"> %%gui.settings.nsargs.title%% @@ -72,6 +75,10 @@ </div> </div> <h2>%%gui.settings.title.language%%</h2> + <div class="title"> + <img src="icons/language.png"> + <h2>%%gui.settings.title.language%%</h2> + </div> <div class="option" name="autolang"> <div class="text"> %%gui.settings.autolang.title%% @@ -96,7 +103,10 @@ </select> </div> </div> - <h2>%%gui.settings.title.updates%%</h2> + <div class="title"> + <img src="icons/updates.png"> + <h2>%%gui.settings.title.updates%%</h2> + </div> <div class="option" name="autoupdate"> <div class="text"> %%gui.settings.autoupdate.title%% @@ -130,6 +140,21 @@ <input type="text"> </div> </div> + <div class="title"> + <img src="icons/settings.png"> + <h2>%%gui.settings.title.misc%%</h2> + </div> + <div class="option" name="originkill"> + <div class="text"> + %%gui.settings.originkill.title%% + <div class="desc"> + %%gui.settings.originkill.desc%% + </div> + </div> + <div class="actions"> + <button class="switch off"></button> + </div> + </div> </div> </div> @@ -156,6 +181,17 @@ <div class="loading">%%gui.browser.loading%%</div> </div> </div> + <div class="popup small" id="preview"> + <div class="misc fixed vertical"> + <button id="close" onclick="Preview.hide()"> + <img src="icons/close.png"> + </button> + <button id="external" onclick=""> + <img src="icons/external.png"> + </button> + </div> + <webview></webview> + </div> <nav class="gamesContainer"> <button id="vpBtn" onclick="page(0)"></button> @@ -208,7 +244,8 @@ <button id="playNsBtn" class="playBtn" onclick="launch()">%%gui.launch%%</button> <div class="inline"> <div id="nsversion"></div> - <a id="update" href="#" onclick="update()">(%%gui.update.check%%)</a> + <a id="update" href="#" onclick="updateNorthstar()">(%%gui.update.check%%)</a> + <div id="serverstatus" class="checking"></div> </div> </div> </div> @@ -223,7 +260,7 @@ <button id="togglemod" onclick="selected().toggle()">%%gui.mods.toggle%%</button> <button id="toggleall" onclick="selected(true).toggle(true)">%%gui.mods.toggleall%%</button> <button id="installmod" onclick="installmod()">%%gui.mods.install%%</button> - <button id="installmod" onclick="Browser.toggle(true)">%%gui.mods.find%%</button> + <button id="findmod" onclick="Browser.toggle(true)">%%gui.mods.find%%</button> </div> </div> </div> @@ -252,4 +289,4 @@ <script src="settings.js"></script> <script src="launcher.js"></script> </body> -</html> +< diff --git a/src/app/lang.js b/src/app/lang.js index 6fdcd8d..f1c31d3 100644 --- a/src/app/lang.js +++ b/src/app/lang.js @@ -10,7 +10,7 @@ function setlang() { if (html[i][0] != " " && html[i][html[i].length - 1] != " ") { // Replaces it with it's string - html[i] = lang(html[i]) + html[i] = lang(html[i]); } } diff --git a/src/app/launcher.js b/src/app/launcher.js index e0d7619..e1dbbe0 100644 --- a/src/app/launcher.js +++ b/src/app/launcher.js @@ -1,10 +1,14 @@ const markdown = require("marked").parse; +var servercount; +var playercount; +var masterserver; + // Changes the main page // This is the tabs in the sidebar function page(page) { - let pages = document.querySelectorAll(".mainContainer .contentContainer") - let btns = document.querySelectorAll(".gamesContainer button") + let btns = document.querySelectorAll(".gamesContainer button"); + let pages = document.querySelectorAll(".mainContainer .contentContainer"); for (let i = 0; i < pages.length; i++) { pages[i].classList.add("hidden"); @@ -19,36 +23,39 @@ function page(page) { bgHolder.setAttribute("bg", page); }; page(1) - -// Updates the Viper release notes -ipcRenderer.on("vp-notes", (event, response) => { +function formatRelease(notes) { let content = ""; - for (const release of response) { - content += "# " + release.name + "\n\n" - + release.body.replaceAll("\r\n", "\n") + "\n\n\n"; + if (notes.length === 1) { + content = notes[0]; + } else { + for (let release of notes) { + if (release.prerelease) {continue} + content += "# " + release.name + "\n\n" + release.body + "\n\n\n"; + } + + content = content.replaceAll(/\@(\S+)/g, `<a href="https://github.com/$1">@$1</a>`); } - vpReleaseNotes.innerHTML = markdown(content); -}); - -async function loadVpReleases() { - ipcRenderer.send("get-vp-notes"); -}; loadVpReleases(); + return markdown(content, { + breaks: true + }); +} +// Updates the Viper release notes +ipcRenderer.on("vp-notes", (event, response) => { + vpReleaseNotes.innerHTML = formatRelease(response); +}); // Updates the Northstar release notes ipcRenderer.on("ns-notes", (event, response) => { - let content = ""; - - for (let release of response) { - content += "# " + release.name + "\n\n" - + release.body.replaceAll("\r\n", "\nhtmlbreak") + "\n\n\n"; - } - - nsRelease.innerHTML = markdown(content).replaceAll("htmlbreak", "<br>"); + nsRelease.innerHTML = formatRelease(response); }); +async function loadVpReleases() { + ipcRenderer.send("get-vp-notes"); +}; loadVpReleases(); + async function loadNsReleases() { ipcRenderer.send("get-ns-notes"); }; loadNsReleases(); @@ -83,7 +90,10 @@ function showVpSection(section) { } function showNsSection(section) { - if (!["main", "release", "mods"].includes(section)) throw new Error("unknown ns section"); + if (!["main", "release", "mods"].includes(section)) { + throw new Error("unknown ns section"); + } + nsMainBtn.removeAttribute("active"); nsModsBtn.removeAttribute("active"); nsReleaseBtn.removeAttribute("active"); @@ -108,3 +118,46 @@ function showNsSection(section) { break; } } + +async function loadServers() { + serverstatus.classList.add("checking"); + + try { + let servers = await (await fetch("https://northstar.tf/client/servers")).json(); + masterserver = true; + + playercount = 0; + servercount = servers.length; + + for (let i = 0; i < servers.length; i++) { + playercount += servers[i].playerCount + } + }catch (err) { + playercount = 0; + servercount = 0; + masterserver = false; + } + + serverstatus.classList.remove("checking"); + + if (servercount == 0 || ! servercount || ! playercount) {masterserver = false} + + let playerstr = lang("gui.server.players"); + if (playercount == 1) { + playerstr = lang("gui.server.player"); + } + + if (masterserver) { + serverstatus.classList.add("up"); + serverstatus.innerHTML = `${servercount} ${lang("gui.server.servers")} - ${playercount} ${playerstr}`; + } else { + serverstatus.classList.add("down"); + serverstatus.innerHTML = lang("gui.server.offline"); + + } +}; loadServers() + +// Refreshes every 5 minutes +setInterval(() => { + loadServers(); +}, 300000) diff --git a/src/app/main.css b/src/app/main.css index 87d89e0..f39c5a1 100644 --- a/src/app/main.css +++ b/src/app/main.css @@ -1,52 +1,14 @@ -:root { - --red: 199, 119, 127; - --blue: 129, 161, 193; - --orange: 213, 151, 131; - - --padding: 25px; - - --bg: rgba(0, 0, 0, 0.5); - --selbg: rgba(80, 80, 80, 0.5); - --redbg: linear-gradient(45deg, rgb(var(--red)), #FA4343); - --bluebg: linear-gradient(45deg, rgb(var(--blue)), #7380ED); -} - -.popup, #modsdiv { - outline: 1px solid #444444; - border: 3px solid var(--bg); -} - -::-webkit-scrollbar { - width: 8px; -} - -::-webkit-scrollbar-track { - border-radius: 10px; - background: transparent; -} - -::-webkit-scrollbar-thumb { - border-radius: 10px; - background: rgb(var(--red)); -} - -::selection { - color: black; - background: rgb(var(--red)); -} +@import "css/dragui.css"; +@import "css/toasts.css"; +@import "css/popups.css"; +@import "css/theming.css"; +@import "css/launcher.css"; body { margin: 0; overflow: hidden; } -body, button, input {font-family: "Roboto", sans-serif} - -body, button, img, a { - -webkit-user-drag: none; - user-select: none; -} - button {outline: none} b, strong {font-weight: 700} body, input, button {font-weight: 500} @@ -62,281 +24,19 @@ button { transition: 0.2s ease-in-out; } -.playBtn, .gamesContainer button, #winbtns div { - cursor: pointer; -} - -.popup { - --spacing: var(--padding); - - z-index: 2; - opacity: 0.0; - position: fixed; - overflow-y: scroll; - top: var(--spacing); - pointer-events: none; - left: var(--spacing); - background: var(--bg); - right: var(--spacing); - bottom: var(--spacing); - transform: scale(0.98); - backdrop-filter: blur(15px); - border-radius: calc(var(--padding) / 3); - transition: opacity 0.15s ease-in-out, transform 0.15s ease-in-out; -} - -.popup.shown { - opacity: 1.0; - pointer-events: all; - transform: scale(1.0); -} - -#overlay { - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 1; - opacity: 0.0; - position: fixed; - background: var(--bg); - pointer-events: none; - transition: opacity 0.15s ease-in-out; -} - -#overlay.shown { - opacity: 0.8; - pointer-events: all; -} - -@keyframes fadein { - 0% {opacity: 0.0} - 100% {opacity: 1.0} -} - -.popup .el, .popup .misc, .popup .loading { - --spacing: calc(var(--padding) / 2); - --height: calc(var(--padding) * 3); - --mischeight: calc(var(--padding) * 1.5); - - animation-duration: 0.15s; - animation-iteration-count: 1; - animation-name: fadein; - animation-fill-mode: forwards; - animation-timing-function: ease-in-out; - - opacity: 0.0; - transition: 0.15s ease-in-out; -} - -.popup .el, .popup #search, .option .actions select, .option .actions input, .popup #close, .popup .misc button { - color: white; - display: flex; - align-items: center; - height: var(--height); - margin: var(--spacing); - padding: var(--spacing); - background: var(--selbg); - border-radius: var(--spacing); - width: calc(100% - var(--spacing) * 4); -} - -.popup .misc, .popup #search, .option .actions input { - --height: var(--mischeight); -} - -.popup .misc { - display: flex; -} - -.popup #search, .option .actions input, .option .actions select { - border: none; - outline: none; - transition: filter 0.15s ease-in-out; - width: calc(100% - var(--spacing) * 2); -} - -.popup #search:focus, .option .actions input:focus, .option .actions button:active { - filter: brightness(1.5); -} - -.popup .misc button { - --height: calc(var(--padding) * 1.5); - - padding: 0px; - margin-left: 0px; - padding: 0px !important; - width: var(--height) !important; -} - -.popup .misc button img { - opacity: 0.6; - width: var(--height); - transform: scale(0.6); - height: var(--height) !important; -} - -#options.popup .misc button { - margin-left: 0px; - width: auto !important; - padding-right: calc(var(--padding) / 2) !important; -} - -.popup .loading { - width: 100%; - color: white; - display: flex; - text-align: center; - align-items: center; - justify-content: center; - height: calc(100% - var(--mischeight) - var(--height)); -} - -.popup .message { - color: white; - text-align: center; - margin: var(--padding); - width: calc(100% - var(--padding)); -} - -.popup .el .image, .popup .el .image img { - width: var(--height); - height: var(--height); - margin-right: var(--spacing); - border-radius: var(--spacing); -} - -.popup .el .text { - overflow: hidden; -} - -.popup .el .title, .popup .el .description { - height: 1.2em; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - -.popup .el .title {font-size: 1.2em} -.popup .el .description {font-size: 0.8em} -.popup .el button { - background: rgb(var(--blue)); - margin-top: var(--spacing); -} - -.popup .options { - color: white; - margin: calc(var(--padding) / 2); -} - -.popup .options .option { - width: 100%; - display: flex; - margin-bottom: var(--padding); - justify-content: space-between; -} - -.popup .overlay { - z-index: 1; - color: white; - opacity: 0.0; - position: fixed; - pointer-events: none; - transform: scale(0.9); - background: var(--selbg); - backdrop-filter: blur(15px); - transition: 0.15s ease-in-out; - padding: calc(var(--spacing) / 2); - border-radius: calc(var(--spacing) / 2); -} - -.popup .overlay.shown { - opacity: 1.0; - pointer-events: all; - transform: scale(1.0); -} +button:hover {filter: brightness(110%)} +button:active {filter: brightness(90%)} -.checks { +.popup, #modsdiv { + outline: 1px solid #444444; + border: 3px solid var(--bg); } -.check { - display:flex; +.playBtn, .gamesContainer button, #winbtns div { cursor: pointer; } -.check::before { - width: 1em; - height: 1em; - content: " "; - background-size: 75%; - filter: brightness(1.3); - background-position: center; - background-repeat: no-repeat; - transition: 0.15s ease-in-out; - background-color: var(--selbg); - margin-right: calc(var(--spacing) / 3); - border-radius: calc(var(--spacing) / 4); -} - -.check.checked::before { - background-color: rgb(var(--red)); - background-image: url(icons/check.png); -} - -.option .text {font-weight: 600} -.option .text .desc { - opacity: 0.8; - font-weight: 500; - font-size: 0.9em; - max-width: 400px; - margin-top: calc(var(--padding) / 3); -} - -.option .actions input, .option .actions select { - width: 100%; - margin: 0px; - --spacing: calc(var(--padding) / 3); -} - -.option[type=array] .actions input { - word-spacing: 15px; - margin-right: 15vw; -} - -.option .actions button { - background: var(--selbg); -} - -.switch { - width: 50px; - height: 25px; - border-radius: 50px; -} - -.switch.on { - background: rgba(var(--red), 0.2) !important; -} - -.switch::after { - left: -5px; - width: 15px; - height: 15px; - content: " "; - display: block; - background: red; - position: relative; - border-radius: 50px; - background: var(--bg); - transition: 0.2s ease-in-out; -} - -.switch.on::after { - left: 15px; - width: 20px; - opacity: 0.5; - background: rgb(var(--red)); -} - +/* window buttons { */ #winbtns { z-index: 2; display: flex; @@ -359,11 +59,13 @@ button { #winbtns #minimize {background-image: url("icons/minimize.png")} #winbtns #settings {background-image: url("icons/settings.png")} +#winbtns #settings:hover { + transform: rotate(90deg); +} + #winbtns div:hover {opacity: 1.0} #winbtns div:active {transform: scale(0.95)} - -button:hover {filter: brightness(110%)} -button:active {filter: brightness(90%)} +/* } */ img {pointer-events: none} @@ -377,7 +79,7 @@ img {pointer-events: none} background-size: cover; background-position: center; background-repeat: no-repeat; - transition: background-image 0.5s ease-in-out; + transition: background-image 0.1s ease-in-out; filter: brightness(0.4) blur(2px) grayscale(0.6); } @@ -385,412 +87,7 @@ img {pointer-events: none} #bgHolder[bg="1"] {background-image: url("../assets/bg/northstar.jpg")} #bgHolder[bg="2"] {background-image: url("../assets/bg/tf2.jpg")} -.gamesContainer { - width: 10%; - height: 100%; - min-width: 95px; - max-width: 120px; - - float: left; - display: flex; - flex-wrap: wrap; - align-content: center; -} - -.mainContainer { - height: 100%; - flex-grow: 1; - display: flex; - position: relative; -} - - -/* nav bar buttons */ -.gamesContainer button { - background-size: 90%; - background-position: center; - background-repeat: no-repeat; - - border: none; - transition: 0.3s ease-in-out; - background-color: transparent; - - - margin: 20px; - position: relative; - box-sizing: border-box; - flex-basis: calc(100% - 10px); -} - -.gamesContainer button.inactive { - opacity: 0.5; - transform: scale(0.9); -} - -.gamesContainer button::before { - content: ""; - display: block; - padding-top: 100%; -} - -.gamesContainer button .content { - width: 100%; - height: 100%; - top: 0; left: 0; - position: absolute; -} - -#vpBtn {background-image: url("icons/viper.png")} -#nsBtn {background-image: url("icons/northstar.png")} -#tfBtn { - background-image: url("icons/titanfall2.png"); - background-size: 69%; /* nice */ -} - -.contentContainer { - width: 85%; - color: white; - flex-grow: 1; - opacity: 1.0; - margin-left: 5%; - position: absolute; - transition: 0.15s ease-in-out; -} - -.contentContainer.hidden { - opacity: 0.0; - pointer-events: none; -} - -.contentMenu { - padding: 0; - flex-grow: 1; - display: flex; - font-size: 20px; - list-style: none; - margin-bottom: 0; - align-items: center; - justify-content: center; - margin-top: var(--padding); -} - -/* some sections do not need space between them and menu */ -#nsMain, #nsRelease, #vpReleaseNotes, .playBtnContainer { - margin-top: 0 !important; -} - -.contentMenu li { - opacity: 0.6; - margin: 0 26px; - cursor: pointer; - font-weight: 700; - text-align: center; - height: fit-content; - transition: opacity 0.3s ease-in-out; -} - -.contentMenu li:last-child {margin-right: 0px} -.contentMenu li:first-child {margin-left: 0px} - -.contentMenu li:hover {opacity: 0.7} - -.contentMenu li[active] { - opacity: 1.0; - pointer-events: none; -} - -.contentMenu li::after { - top: 10px; - width: 30px; - height: 5px; - opacity: 0.0; - content: " "; - display: block; - text-align: center; - position: relative; - border-radius: 50px; - background: rgb(var(--red)); - left: calc(50% - 15px); - transition: 0.2s ease-in-out; -} - -.contentMenu li[active]::after { - top: 5px; - opacity: 1.0; -} - -.section { - opacity: 1.0; - position: fixed; - right: calc(var(--padding) * 2); - left: calc(100px + var(--padding)); - transition: opacity 0.15s ease-in-out; -} - -.section.hidden { - opacity: 0.0; - pointer-events: none; -} - -.contentBody img {max-width: 100%} -.contentBody .img {text-align: center} - -.contentBody .section > :first-child:not(.img) { - margin-top: 35px; -} - -.contentContainer .playBtnContainer { - text-align: center; -} - -.contentContainer .playBtn { - width: 27%; - height: 11%; - border: none; - color: white; - padding: 20px; - font-size: 24px; - font-weight: bold; - margin-top: 100px; - margin-bottom: 10px; - border-radius: 10px; - background: var(--redbg); - transition: 0.3s ease-in-out; - filter: drop-shadow(0px 8px 5px rgba(0, 0, 0, 0.1)); -} - -.contentContainer .playBtn:hover { - transform: scale(1.05); - filter: drop-shadow(0px 5px 15px rgba(0, 0, 0, 0.3)) brightness(110%); -} - -.contentContainer .playBtn:active { - opacity: 0.7;transform: scale(0.98); - filter: drop-shadow(0px 5px 10px rgba(0, 0, 0, 0.4)); -} - -.contentContainer #nsMain .playBtn { - background: var(--bluebg); -} - -a { - color: rgb(var(--red)); - text-decoration: none; - transition: filter 0.2s ease-in; -} - -a:hover { - filter: brightness(80%); -} - -#nsContent .contentMenu { - margin-bottom: 0; -} - -.contentBody .img img { - transform: scale(0.85); -} - -.contentBody .img { - width: 100%; - text-align: center; -} - -#nsRelease, #vpReleaseNotes { - height: 80vh; - overflow-y: scroll; - flex-direction: column; - margin-top: 10px !important; -} - -.inline * { - display: inline-block; -} - -#vpMain { - margin-top: 140px; - text-align: center; -} - -#vpMain img { - width: 20%; -} - -#vpVersion { - font-size: 16px; -} - -.simplebar-scrollbar:before { - background: rgb(var(--red)) !important; -} - -#installmod {background: rgb(var(--blue))} -#togglemod, #toggleall {background: rgb(var(--orange))} -#removeall, #removemod {background: rgb(var(--red))} -button:disabled { - opacity: 0.5; - pointer-events: none; -} - -button.visual { - opacity: 1.0; - pointer-events: none; - background: transparent !important; -} - -code { - font-size: 16px; - padding: 2px 5px; - border-radius: 3px; - background-color: #00000070; -} - -#nsMods .line { - display: flex; - align-items: center; - margin-top: calc(var(--padding) / 2); -} - -#modsdiv { - height: 50vh; - overflow-y: scroll; - border-radius: 5px; - background: var(--bg); - backdrop-filter: blur(15px); - padding: calc(var(--padding) / 4); -} - -#modsdiv .mod { - display: flex; - border-radius: 5px; - transition: 0.1s ease-in-out; - margin: calc(var(--padding) / 3); - padding: calc(var(--padding) / 3); -} - -#modsdiv .mod.selected { - background: var(--selbg); -} - -#modsdiv .mod .disabled, .modbtns { - margin-left: auto; -} - -.modbtns button { - margin-left: var(--spacing); - --spacing: calc(var(--padding) / 3); - margin-top: calc(var(--spacing) / 2); - margin-bottom: calc(var(--spacing) / 2); -} - -#toasts { - position: fixed; - z-index: 100000; - right: calc(var(--padding) * 1.5); - bottom: calc(var(--padding) * 1.5); -} - -@keyframes bodyfadeaway { - 0% {opacity: 0.0; transform: scale(0.95)} - 100% {opacity: 1.0; transform: scale(1.0)} -} - -#toasts .toast { - width: 300px; - opacity: 0.0; - cursor: pointer; - overflow: hidden; - max-height: 100vh; - background: #FFFFFF; - transform: scale(0.95); - transition: 0.2s ease-in-out; - padding: calc(var(--padding) / 2); - margin-top: calc(var(--padding) / 2); - border-radius: calc(var(--padding) / 2.5); - box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.2); - - animation-duration: 0.2s; - animation-iteration-count: 1; - animation-name: bodyfadeaway; - animation-fill-mode: forwards; - animation-timing-function: ease-in-out; -} - -#toasts .toast.hidden { - margin-top: 0px; - max-height: 0px; - padding-top: 0px; - padding-bottom: 0px; - filter: opacity(0.0); - transform: scale(0.95); -} - -#toasts .toast:not(.hidden):hover {filter: opacity(0.9)} -#toasts .toast:not(.hidden):active {filter: opacity(0.8)} - -.toast .title { -} - -.toast .description { - opacity: 0.8; - font-size: 0.8em; - font-weight: 600; -} - -#dragUI { - top: 0; - left: 0; - right: 0; - bottom: 0; - color: white; - opacity: 0.0; - position: fixed; - z-index: 1000000; - pointer-events: none; - background: var(--bg); - backdrop-filter: blur(15px); - transition: 0.1s ease-in-out; -} - -#dragUI.shown { - opacity: 1.0; - pointer-events: all; -} - -#dragUI #dragitems { - --size: 25vw; - top: 50%; - left: 50%; - opacity: 0.6; - position: absolute; - text-align: center; - width: var(--size); - height: var(--size); - margin-top: calc(var(--size) / 2 * -1); - margin-left: calc(var(--size) / 2 * -1); -} - -#dragUI #dragitems #icon { - width: 100%; - height: 100%; - filter: invert(1); - transform: scale(0.45); - background-size: cover; - background-image: url("icons/download.png"); - transition: 0.1s ease-in-out; -} - -#dragUI.shown #dragitems #icon { - transform: scale(0.5); -} - -#dragUI #dragitems #text { - top: -5vw; - position: relative; -} - /* drag control */ - #bgHolder, .contentContainer, .gamesContainer { @@ -804,6 +101,7 @@ code { -webkit-app-region: no-drag; } -a, button, #close, #nsRelease, #vpReleaseNotes, .mod, #overlay, #modsdiv, #winbtns, .contentMenu { +a, button, #close, #nsRelease, #vpReleaseNotes, +.mod, #overlay, #modsdiv, #winbtns, .contentMenu { -webkit-app-region: no-drag; } diff --git a/src/app/main.js b/src/app/main.js index 4918777..ad1a44f 100644 --- a/src/app/main.js +++ b/src/app/main.js @@ -1,6 +1,6 @@ const fs = require("fs"); const path = require("path"); -const { ipcRenderer, shell } = require("electron"); +const { ipcRenderer, shell, ipcMain } = require("electron"); const lang = require("../lang"); var modsobj = {}; @@ -15,6 +15,7 @@ var settings = { autolang: true, forcedlang: "en", autoupdate: true, + originkill: false, zip: "/northstar.zip", lang: navigator.language, winebin: "/usr/bin/wine64", @@ -29,7 +30,24 @@ ipcRenderer.send("setlang", settings.lang); // Loads the settings if (fs.existsSync("viper.json")) { - settings = {...settings, ...JSON.parse(fs.readFileSync("viper.json", "utf8"))}; + let conf = fs.readFileSync("viper.json", "utf8"); + let json = {}; + + // Validates viper.json + try { + json = JSON.parse(conf); + }catch (e) { + let reset = confirm(lang("general.invalidconfig", navigator.language) + e); + if (! reset) { + ipcRenderer.send("exit"); + } else { + fs.writeFileSync("viper.json", "{}"); + ipcRenderer.send("relaunch"); + } + + } + + settings = {...settings, ...json}; settings.zip = path.join(settings.gamepath + "/northstar.zip"); if (settings.gamepath.length === 0) { @@ -46,8 +64,14 @@ if (fs.existsSync("viper.json")) { setpath(); } + +// Show a toast message if no Internet connection has been detected. +if (!navigator.onLine) { + ipcRenderer.send("no-internet"); +} + function exit() {ipcRenderer.send("exit")} -function update() {ipcRenderer.send("update")} +function updateNorthstar() {ipcRenderer.send("update-northstar")} // Reports to the main process about game path status. // @param {boolean} value is game path loaded @@ -58,15 +82,15 @@ function setpath(value = false) { // Tells the main process to launch or install Northstar function launch() { if (shouldInstallNorthstar) { - update(); + updateNorthstar(); shouldInstallNorthstar = false; } else { - ipcRenderer.send("launch"); + ipcRenderer.send("launch-ns"); } } // Tells the main process to launch the vanilla game -function launchVanilla() {ipcRenderer.send("launchVanilla")} +function launchVanilla() {ipcRenderer.send("launch-vanilla")} // In conjunction with utils.js' winLog(), it'll send log messages in // the devTools from utils.js @@ -85,13 +109,11 @@ function setButtons(state) { } } - disablearray(document.querySelectorAll(".playBtnContainer .playBtn")) - disablearray(document.querySelectorAll("#nsMods .buttons.modbtns button")) - disablearray(document.querySelectorAll("#browser #browserEntries .text button")) + disablearray(document.querySelectorAll(".playBtnContainer .playBtn")); + disablearray(document.querySelectorAll("#nsMods .buttons.modbtns button")); + disablearray(document.querySelectorAll("#browser #browserEntries .text button")); } -ipcRenderer.on("setbuttons", (event, state) => {setButtons(state)}) - ipcRenderer.on("gamestate", (event, state) => { setButtons(! state); @@ -105,7 +127,11 @@ ipcRenderer.on("gamestate", (event, state) => { btns[1].innerHTML = string; }) -ipcRenderer.on("gamepathlost", (event, state) => { +ipcRenderer.on("set-buttons", (event, state) => { + setButtons(state); +}) + +ipcRenderer.on("gamepath-lost", (event, state) => { page(0); setButtons(false); alert(lang("gui.gamepath.lost")); @@ -117,6 +143,7 @@ ipcRenderer.on("ns-update-event", (event, key) => { console.log(lang(key)); switch(key) { case "cli.update.uptodate.short": + case "cli.update.noInternet": setButtons(true); playNsBtn.innerText = lang("gui.launch"); break; @@ -171,7 +198,7 @@ function selected(all) { } } - ipcRenderer.send("removemod", selected) + ipcRenderer.send("remove-mod", selected); }, toggle: () => { if (selected.match(/^Northstar\./)) { @@ -184,33 +211,82 @@ function selected(all) { } } - ipcRenderer.send("togglemod", selected) + ipcRenderer.send("toggle-mod", selected); } } } +let installqueue = []; + // Tells the main process to install a mod through the file selector function installmod() { setButtons(false); - ipcRenderer.send("installmod") + ipcRenderer.send("install-mod"); } // Tells the main process to directly install a mod from this path function installFromPath(path) { setButtons(false); - ipcRenderer.send("installfrompath", path) + ipcRenderer.send("install-from-path", path); } // Tells the main process to install a mod from a URL -function installFromURL(url) { +function installFromURL(url, dependencies, clearqueue) { + if (clearqueue) {installqueue = []}; + + let prettydepends = []; + + if (dependencies) { + let newdepends = []; + for (let i = 0; i < dependencies.length; i++) { + let depend = dependencies[i].toLowerCase(); + if (! depend.match(/northstar-northstar-.*/)) { + depend = dependencies[i].replaceAll("-", "/"); + let pkg = depend.split("/"); + if (! isModInstalled(pkg[1])) { + newdepends.push(depend); + prettydepends.push(`${pkg[1]} v${pkg[2]} - ${lang("gui.browser.madeby")} ${pkg[0]}`); + } + } + } + + dependencies = newdepends; + } + + if (dependencies && dependencies.length != 0) { + let confirminstall = confirm(lang("gui.mods.confirmdependencies") + prettydepends.join("\n")); + if (! confirminstall) { + return + } + } + setButtons(false); - ipcRenderer.send("installfromurl", url) + ipcRenderer.send("install-from-url", url, dependencies); + + if (dependencies) { + installqueue = dependencies; + } +} + +function isModInstalled(modname) { + for (let i = 0; i < modsobj.all.length; i++) { + let mod = modsobj.all[i]; + if (mod.ManifestName) { + if (mod.ManifestName.match(modname)) { + return true; + } + } else if (mod.Name.match(modname)) { + return true; + } + } + + return false; } // Frontend part of settings a new game path ipcRenderer.on("newpath", (event, newpath) => { settings.gamepath = newpath; - ipcRenderer.send("guigetmods"); + ipcRenderer.send("gui-getmods"); }) // Continuation of log() @@ -258,23 +334,23 @@ ipcRenderer.on("version", (event, versions) => { shouldInstallNorthstar = true; playNsBtn.innerText = lang("gui.installnorthstar"); } -}); ipcRenderer.send("getversion"); +}); ipcRenderer.send("get-version"); // When an update is available it'll ask the user about it -ipcRenderer.on("updateavailable", () => { +ipcRenderer.on("update-available", () => { if (confirm(lang("gui.update.available"))) { - ipcRenderer.send("updatenow"); + ipcRenderer.send("update-now"); } }) // Error out when no game path is set -ipcRenderer.on("nopathselected", () => { +ipcRenderer.on("no-path-selected", () => { alert(lang("gui.gamepath.must")); exit(); }); // Error out when game path is wrong -ipcRenderer.on("wrongpath", () => { +ipcRenderer.on("wrong-path", () => { alert(lang("gui.gamepath.wrong")); setpath(false); }); @@ -303,7 +379,7 @@ document.addEventListener("drop", (e) => { event.stopPropagation(); dragUI.classList.remove("shown"); - installFromPath(event.dataTransfer.files[0].path) + installFromPath(event.dataTransfer.files[0].path); }); document.body.addEventListener("keyup", (e) => { diff --git a/src/app/settings.js b/src/app/settings.js index 96d9379..34975fe 100644 --- a/src/app/settings.js +++ b/src/app/settings.js @@ -3,26 +3,26 @@ var Settings = { if (state) { Settings.load(); options.scrollTo(0, 0); - overlay.classList.add("shown") - options.classList.add("shown") + overlay.classList.add("shown"); + options.classList.add("shown"); return } else if (! state) { if (state != undefined) { - overlay.classList.remove("shown") - options.classList.remove("shown") + overlay.classList.remove("shown"); + options.classList.remove("shown"); return } } Settings.load(); options.scrollTo(0, 0); - overlay.classList.toggle("shown") - options.classList.toggle("shown") + overlay.classList.toggle("shown"); + options.classList.toggle("shown"); }, apply: () => { settings = {...settings, ...Settings.get()}; - ipcRenderer.send("savesettings", Settings.get()); + ipcRenderer.send("save-settings", Settings.get()); }, reloadSwitches: () => { let switches = document.querySelectorAll(".switch"); |