diff options
Diffstat (limited to 'src/app')
-rw-r--r-- | src/app/browser.js | 264 | ||||
-rw-r--r-- | src/app/icons/no-image.png | bin | 0 -> 1959 bytes | |||
-rw-r--r-- | src/app/icons/viper.png | bin | 137107 -> 59028 bytes | |||
-rw-r--r-- | src/app/index.html | 18 | ||||
-rw-r--r-- | src/app/main.css | 298 | ||||
-rw-r--r-- | src/app/main.js | 23 | ||||
-rw-r--r-- | src/app/toast.js | 59 |
7 files changed, 627 insertions, 35 deletions
diff --git a/src/app/browser.js b/src/app/browser.js new file mode 100644 index 0000000..ded12fa --- /dev/null +++ b/src/app/browser.js @@ -0,0 +1,264 @@ +const Fuse = require("fuse.js"); +var fuse; +var packages = []; + +var Browser = { + maxentries: 50, + toggle: (state) => { + if (state) { + browser.scrollTo(0, 0); + overlay.classList.add("shown") + browser.classList.add("shown") + + if (browserEntries.querySelectorAll(".el").length == 0) { + Browser.loadfront(); + } + return + } else if (! state) { + if (state != undefined) { + overlay.classList.remove("shown") + browser.classList.remove("shown") + return + } + } + + browser.scrollTo(0, 0); + overlay.classList.toggle("shown") + browser.classList.toggle("shown") + }, + loadfront: async () => { + Browser.loading(); + + if (packages.length < 1) { + packages = await (await fetch("https://northstar.thunderstore.io/api/v1/package/")).json(); + + fuse = new Fuse(packages, { + keys: ["full_name"] + }) + } + + for (let i in packages) { + if (i == Browser.maxentries) {Browser.endoflist();break} + new BrowserElFromObj(packages[i]); + } + }, + loading: (string) => { + if (string) { + browserEntries.innerHTML = `<div class="loading">${string}</div>`; + } + + if (! browserEntries.querySelector(".loading")) { + browserEntries.innerHTML = `<div class="loading">${lang('gui.browser.loading')}</div>`; + } + }, + endoflist: () => { + browserEntries.innerHTML += `<div class="message">${lang('gui.browser.endoflist')}</div>` + }, + search: (string) => { + Browser.loading(); + let res = fuse.search(string); + + if (res.length < 1) { + Browser.loading("No results...") + return + } + + for (let i = 0; i < res.length; i++) { + if (i == Browser.maxentries) {Browser.endoflist();break} + new BrowserElFromObj(res[i].item); + } + }, + setbutton: (mod, string) => { + mod = normalize(mod); + if (document.getElementById(mod)) { + let elems = document.querySelectorAll(`#${mod}`); + + for (let i = 0; i < elems.length; i++) { + elems[i].querySelector(".text button").innerHTML = string; + } + } else { + let make = (str) => { + if (document.getElementById(str)) { + return Browser.setbutton(str, string); + } 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].FolderName); + + if (mod.includes(modname)) { + if (! make(modname)) { + if (modsobj.all[i].ManifestName) { + make(normalize(modsobj.all[i].ManifestName)); + } + } + } + else if (mod.includes(modfolder)) {make(modfolder);break} + } + }, 1501) + } + } +} + +document.body.addEventListener("keyup", (e) => { + if (e.key == "Escape") {Browser.toggle(false)} +}) + +function BrowserElFromObj(obj) { + let pkg = {...obj, ...obj.versions[0]}; + + new BrowserEl({ + title: pkg.name, + image: pkg.icon, + author: pkg.owner, + url: pkg.package_url, + download: pkg.download_url, + version: pkg.version_number, + description: pkg.description + }) +} + +function BrowserEl(properties) { + properties = { + title: "No name", + version: "1.0.0", + image: "icons/no-image.png", + author: "Unnamed Pilot", + description: "No description", + ...properties + } + + if (properties.version[0] != "v") { + properties.version = "v" + properties.version; + } + + if (browserEntries.querySelector(".loading")) { + browserEntries.innerHTML = ""; + } + + let installstr = lang("gui.browser.install"); + + if (normalize(modsdiv.innerText.split("\n")).includes(normalize(properties.title))) { + installstr = lang("gui.browser.reinstall"); + + for (let i = 0; i < modsobj.all.length; i++) { + if (normalize(modsobj.all[i].Name) == normalize(properties.title) + && "v" + modsobj.all[i].Version != properties.version) { + + installstr = lang("gui.browser.update"); + } + } + } else { + for (let i = 0; i < modsobj.all.length; i++) { + let title = normalize(properties.title); + let folder = normalize(modsobj.all[i].FolderName); + let manifestname = null; + if (modsobj.all[i].ManifestName) { + manifestname = normalize(modsobj.all[i].ManifestName); + } + + if (title.includes(folder) || title.includes(manifestname)) { + installstr = lang("gui.browser.reinstall"); + + if (folder == title + && "v" + modsobj.all[i].Version != properties.version) { + + installstr = lang("gui.browser.update"); + } + } + } + } + + browserEntries.innerHTML += ` + <div class="el" id="${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> + </div> + ` +} + +ipcRenderer.on("removedmod", (event, mod) => { + setButtons(true); + Browser.setbutton(mod.name, lang("gui.browser.install")); + if (mod.manifestname) { + Browser.setbutton(mod.manifestname, lang("gui.browser.install")); + } +}) + +ipcRenderer.on("failedmod", (event, modname) => { + setButtons(true); + new Toast({ + timeout: 10000, + scheme: "error", + title: lang("gui.toast.title.failed"), + description: lang("gui.toast.desc.failed") + }) +}) + +ipcRenderer.on("installedmod", (event, mod) => { + setButtons(true); + Browser.setbutton(mod.name, lang("gui.browser.reinstall")); + + if (mod.malformed) { + new Toast({ + timeout: 8000, + scheme: "warning", + title: lang("gui.toast.title.malformed"), + description: mod.name + " " + lang("gui.toast.desc.malformed") + }) + } + + new Toast({ + scheme: "success", + title: lang("gui.toast.title.installed"), + description: mod.name + " " + lang("gui.toast.desc.installed") + }) +}) + +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 = ""; +search.addEventListener("keyup", () => { + clearTimeout(searchtimeout); + + if (searchstr != search.value) { + if (search.value.replaceAll(" ", "") == "") { + searchstr = ""; + Browser.loadfront(); + return + } + + searchtimeout = setTimeout(() => { + Browser.search(search.value); + searchstr = search.value; + }, 500) + } +}) diff --git a/src/app/icons/no-image.png b/src/app/icons/no-image.png Binary files differnew file mode 100644 index 0000000..43265d1 --- /dev/null +++ b/src/app/icons/no-image.png diff --git a/src/app/icons/viper.png b/src/app/icons/viper.png Binary files differindex 281f3dd..69b1cce 100644 --- a/src/app/icons/viper.png +++ b/src/app/icons/viper.png diff --git a/src/app/index.html b/src/app/index.html index fe383a0..8222416 100644 --- a/src/app/index.html +++ b/src/app/index.html @@ -7,11 +7,26 @@ </head> <body> <div id="bgHolder"></div> + <div id="toasts"></div> <div id="winbtns"> <div id="minimize" onclick="ipcRenderer.send('minimize')"></div> <div id="close" onclick="ipcRenderer.send('exit')"></div> </div> + + + <div id="overlay" onclick="Browser.toggle(false)"></div> + <div id="browser"> + <div id="misc"> + <input id="search" placeholder="%%gui.browser.search%%"> + <button id="close" onclick="Browser.toggle(false)"> + <img src="icons/close.png"> + </button> + </div> + <div id="browserEntries"> + <div class="loading">%%gui.browser.loading%%</div> + </div> + </div> <nav class="gamesContainer"> <button id="vpBtn" onclick="page(0)"></button> @@ -79,6 +94,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> </div> </div> </div> @@ -102,6 +118,8 @@ <script src="lang.js"></script> <script src="main.js"></script> + <script src="toast.js"></script> + <script src="browser.js"></script> <script src="launcher.js"></script> </body> </html> diff --git a/src/app/main.css b/src/app/main.css index de0db46..a532490 100644 --- a/src/app/main.css +++ b/src/app/main.css @@ -9,7 +9,11 @@ --selbg: rgba(80, 80, 80, 0.5); --redbg: linear-gradient(45deg, var(--red), #FA4343); --bluebg: linear-gradient(45deg, var(--blue), #7380ED); +} +#browser, #modsdiv { + outline: 1px solid #444444; + border: 3px solid var(--bg); } ::-webkit-scrollbar { @@ -26,12 +30,191 @@ background: var(--red); } +::selection { + color: black; + background: var(--red); +} + +body { + margin: 0; + overflow: hidden; + user-select: none; +} + +body, button, input {font-family: "Roboto", sans-serif} + +button {outline: none} +b, strong {font-weight: 700} +body, input, button {font-weight: 500} + +button { + border: none; + color: white; + outline: none; + cursor: pointer; + font-weight: 700; + padding: 5px 10px; + border-radius: 5px; + transition: 0.2s ease-in-out; +} + .playBtn, .gamesContainer button, #winbtns div { cursor: pointer; } -#winbtns { +#browser { + --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; +} + +#browser.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} +} + +#browser .el, #browser #misc, #browser .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; +} + +#browser .el, #browser #search, #browser #close { + 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); +} + +#browser #misc, #browser #search { + --height: var(--mischeight); +} + +#browser #misc { + display: flex; +} + +#browser #search { + border: none; + outline: none; + transition: filter 0.15s ease-in-out; + width: calc(100% - var(--spacing) * 2); +} + +#browser #search:focus { + filter: brightness(1.5); +} + +#browser #close { + --height: calc(var(--padding) * 1.5); + + padding: 0px; + margin-left: 0px; + width: var(--height); +} + +#browser #close img { + opacity: 0.6; + width: var(--height); + height: var(--height); + transform: scale(0.6); +} + +#browser .loading { + width: 100%; + color: white; + display: flex; + text-align: center; + align-items: center; + justify-content: center; + height: calc(100% - var(--mischeight) - var(--height)); +} + +#browser .message { + color: white; + text-align: center; + margin: var(--padding); + width: calc(100% - var(--padding)); +} + +#browser .el .image, #browser .el .image img { + width: var(--height); + height: var(--height); + margin-right: var(--spacing); + border-radius: var(--spacing); +} + +#browser .el .text { + overflow: hidden; +} + +#browser .el .title, #browser .el .description { + height: 1.2em; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +#browser .el .title {font-size: 1.2em} +#browser .el .description {font-size: 0.8em} +#browser .el button { + background: var(--blue); + margin-top: var(--spacing); +} + +#winbtns { + z-index: 2; display: flex; position: fixed; top: var(--padding); @@ -54,40 +237,18 @@ #winbtns div:hover {opacity: 1.0} #winbtns div:active {transform: scale(0.98)} -body { - margin: 0; - overflow: hidden; - user-select: none; - font-family: "Roboto", sans-serif; -} - -button {outline: none} -b, strong {font-weight: 700} -body, input, button {font-weight: 500} - -button { - border: none; - color: white; - outline: none; - font-weight: 700; - padding: 5px 10px; - border-radius: 5px; - transition: 0.2s ease-in-out; -} - button:hover {filter: brightness(110%)} button:active {filter: brightness(90%)} img {pointer-events: none} #bgHolder { - top: 0; - left: 0; - right: 0; - bottom: 0; - pointer-events: none; - position: fixed; - transform: scale(1.1); + top: -5px; + left: -5px; + right: -5px; + bottom: -5px; + z-index: -1; + position: absolute; background-size: cover; background-position: center; background-repeat: no-repeat; @@ -162,7 +323,7 @@ img {pointer-events: none} } .contentContainer { - width: 90%; + width: 85%; color: white; flex-grow: 1; opacity: 1.0; @@ -346,6 +507,12 @@ button:disabled { pointer-events: none; } +button.visual { + opacity: 1.0; + pointer-events: none; + background: transparent !important; +} + code { font-size: 16px; padding: 2px 5px; @@ -385,16 +552,81 @@ code { } .modbtns button { - margin-left: calc(var(--padding) / 3); + 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; } /* drag control */ -#bgHolder { +#bgHolder, +.contentContainer, +.gamesContainer { user-select: none; -webkit-app-region: drag; } -a, button, .contentMenu, #close, #nsRelease, #vpReleaseNotes, .mod { +#overlay.shown ~ #bgHolder, +#overlay.shown ~ .contentContainer, +#overlay.shown ~ .gamesContainer { + -webkit-app-region: no-drag; +} + +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 5f0cc9a..169f86f 100644 --- a/src/app/main.js +++ b/src/app/main.js @@ -3,6 +3,7 @@ const path = require("path"); const { ipcRenderer, shell } = require("electron"); const lang = require("../lang"); +var modsobj = {}; let shouldInstallNorthstar = false; // Base settings @@ -26,13 +27,11 @@ if (fs.existsSync("viper.json")) { settings.zip = path.join(settings.gamepath + "/northstar.zip"); if (settings.gamepath.length === 0) { - alert(lang("general.missingpath")); setpath(false); } else { setpath(true); } } else { - alert(lang("general.missingpath")); setpath(); } @@ -68,6 +67,15 @@ function log(msg) { // updating/installing Northstar. function setButtons(state) { playNsBtn.disabled = !state; + + let disablearray = (array) => { + for (let i = 0; i < array.length; i++) { + array[i].disabled = !state; + } + } + + disablearray(document.querySelectorAll("#nsMods .buttons.modbtns button")) + disablearray(document.querySelectorAll("#browser #browserEntries .text button")) } // Frontend part of updating Northstar @@ -150,12 +158,20 @@ function selected(all) { // Tells the main process to install a mod function installmod() { + setButtons(false); ipcRenderer.send("installmod") } +// Tells the main process to install a mod from a URL +function installFromURL(url) { + setButtons(false); + ipcRenderer.send("installfromurl", url) +} + // Frontend part of settings a new game path ipcRenderer.on("newpath", (event, newpath) => { settings.gamepath = newpath; + ipcRenderer.send("guigetmods"); }) // Continuation of log() @@ -164,6 +180,9 @@ ipcRenderer.on("alert", (event, msg) => {alert(msg)}) // Updates the installed mods ipcRenderer.on("mods", (event, mods) => { + modsobj = mods; + if (! mods) {return} + modcount.innerHTML = `${lang("gui.mods.count")} ${mods.all.length}`; modsdiv.innerHTML = ""; diff --git a/src/app/toast.js b/src/app/toast.js new file mode 100644 index 0000000..2a8555e --- /dev/null +++ b/src/app/toast.js @@ -0,0 +1,59 @@ +function Toast(properties) { + let toast = { + fg: "#000000", + bg: "#FFFFFF", + timeout: 3000, + title: "Untitled Toast", + description: "No description provided for toast", + ...properties + } + + switch(toast.scheme) { + case "error": + toast.fg = "#FFFFFF"; + toast.bg = "var(--red)"; + break + case "success": + toast.fg = "#FFFFFF"; + toast.bg = "#60D394"; + break + case "warning": + toast.fg = "#FFFFFF"; + toast.bg = "#FF9B85"; + break + } + + + let id = Date.now(); + if (document.getElementById(id)) {id = id + 1} + let el = document.createElement("div"); + + el.classList.add("toast"); + + el.style.color = toast.fg; + el.style.background = toast.bg; + + el.id = id; + el.setAttribute("onclick", `dismissToast(${id})`); + + el.innerHTML = ` + <div class="title">${toast.title}</div> + <div class="description">${toast.description}</div> + ` + + toasts.appendChild(el); + + setTimeout(() => { + dismissToast(id); + }, toast.timeout) +} + +function dismissToast(id) { + id = document.getElementById(id); + if (id) { + id.classList.add("hidden"); + setTimeout(() => { + id.remove(); + }, 500) + } +} |