const Fuse = require("fuse.js");
const { ipcRenderer, shell } = require("electron");
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 browser_el = document.getElementById("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)) {
pkgs.push(packages[i]);
} else {
other.push(packages[i]);
}
}
return pkgs;
},
get: () => {
let filtered = [];
let unfiltered = [];
let checks = browser_el.querySelectorAll("#filters .check");
for (let i = 0; i < checks.length; i++) {
if (! checks[i].classList.contains("checked")) {
filtered.push(checks[i].getAttribute("value"));
} else {
unfiltered.push(checks[i].getAttribute("value"));
}
}
return {
filtered,
unfiltered
};
},
isfiltered: (categories) => {
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])) {
state = true;
continue
} else if (unfiltered.includes(categories[i])) {
state = false;
continue
}
state = true;
}
return state;
},
toggle: (state) => {
if (state == false) {
filters.classList.remove("shown");
return
}
filters.classList.toggle("shown");
let filterRect = filter.getBoundingClientRect();
let spacing = parseInt(getComputedStyle(filters).getPropertyValue("--spacing"));
filters.style.top = filterRect.bottom - (spacing + (spacing * 1.3));
filters.style.right = filterRect.right - filterRect.left + filterRect.width - (spacing / 2);
},
},
toggle: (state) => {
browser_el.scrollTo(0, 0);
popups.set("#browser", state);
if (state) {
if (browserEntries.querySelectorAll(".el").length == 0) {
browser.loadfront();
}
} else if (state === false) {
browser.filters.toggle(false);
}
},
install: async (package_obj, clear_queue = false) => {
let can_connect = await request.check_with_toasts(
"Thunderstore", "https://thunderstore.io"
)
if (! can_connect) {
return;
}
return mods.install_from_url(
package_obj.download || package_obj.versions[0].download_url,
package_obj.dependencies || package_obj.versions[0].dependencies,
clear_queue,
package_obj.author || package_obj.owner,
package_obj.name || package_obj.pkg.name,
package_obj.version || package_obj.versions[0].version_number
)
},
add_pkg_properties: () => {
for (let i = 0; i < packages.length; i++) {
let properties = packages[i];
let normalized = mods.normalize(packages[i].name);
let has_update = false;
let local_name = false;
let local_version = false;
let remote_version = packages[i].versions[0].version_number;
remote_version = version.format(remote_version);
if (mods.list()) {
for (let ii = 0; ii < mods.list().all.length; ii++) {
let mod = mods.list().all[ii];
if (mods.normalize(mod.name) !== normalized && (
! mod.package ||
mod.package.author + "-" + mod.package.package_name !==
packages[i].full_name
)) {
continue;
}
local_name = mod.name;
local_version = version.format(mod.version);
if (version.is_newer(remote_version, local_version)) {
has_update = true;
}
}
}
let install = () => {
return browser.install({...properties});
}
packages[i].unix_created = new Date(packages[i].date_created).getTime();
packages[i].unix_updated = new Date(packages[i].date_updated).getTime();
packages[i].install = install;
packages[i].has_update = has_update;
packages[i].local_version = local_version;
packages[i].downloads = 0;
for (let version of packages[i].versions) {
packages[i].downloads += version.downloads || 0;
}
if (local_version) {
browser.mod_versions[normalized] = {
install: install,
has_update: has_update,
local_name: local_name,
local_version: local_version,
package: packages[i]
}
}
}
},
// sorts `pkgs` based on `property` in package object
sort: (pkgs, property) => {
// get property from sort selector, if not specified
if (! property) {
property = sort.querySelector("select").value;
// if we somehow still don't have a property, just return
if (! property) {
return pkgs;
}
}
// if `property` doesn't even exist, just return
if (typeof pkgs[0][property] == "undefined") {
return pkgs;
}
// sort in descending order
return pkgs.sort((a, b) => {
return b[property] - a[property];
})
},
loadfront: async () => {
browser.loading();
packagecount = 0;
if (packages.length < 1) {
let host = "northstar.thunderstore.io";
let path = "/api/v1/package/";
packages = [];
// attempt to get the list of packages from Thunderstore, if
// this has been done recently, it'll simply return a cached
// version of the request
try {
packages = JSON.parse(
await request(host, path, "thunderstore-packages")
)
}catch(err) {
console.error(err)
}
browser.add_pkg_properties();
browser_fuse = new Fuse(packages, {
keys: ["full_name"]
})
}
let pkgs = browser.sort(browser.filters.getpkgs());
for (let i in pkgs) {
if (packagecount >= browser.maxentries) {
browser.endoflist();
break
}
browser.mod_el_from_obj(pkgs[i]);
packagecount++;
}
},
loading: (string) => {
if (browser.filters.get().unfiltered.length == 0) {
string = lang("gui.browser.no_results");
}
if (string) {
browserEntries.innerHTML = `
${string}
`;
}
if (! browserEntries.querySelector(".loading")) {
browserEntries.innerHTML = `${lang('gui.browser.loading')}
`;
}
},
endoflist: (is_end) => {
let pkgs = [];
let filtered = browser.filters.getpkgs();
for (let i = 0; i < filtered.length; i++) {
if (filtered[packagecount + i]) {
pkgs.push(filtered[packagecount + i]);
} else {
break
}
}
if (browserEntries.querySelector(".message")) {
browserEntries.querySelector(".message").remove();
}
if (pkgs.length == 0 || is_end) {
browser.msg(`${lang('gui.browser.endoflist')}`);
return
}
browser.msg(``);
loadmore.addEventListener("click", () => {
browser.loadpkgs(pkgs);
browser.endoflist(! pkgs.length);
})
},
search: (string) => {
browser.loading();
let res = browser_fuse.search(string);
if (res.length < 1) {
browser.loading(lang("gui.browser.no_results"));
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}
browser.mod_el_from_obj(res[i].item);
count++;
}
if (count < 1) {
browser.loading(lang("gui.browser.no_results"));
}
},
setbutton: (mod, string, icon) => {
mod = mods.normalize(mod);
if (browserEntries.querySelector(`#mod-${mod}`)) {
let elems = browserEntries.querySelectorAll(`.el#mod-${mod}`);
for (let i = 0; i < elems.length; i++) {
if (icon) {
string = `` +
`${string}`;
}
elems[i].querySelector(".text button").innerHTML = string;
}
} else {
let make = (str) => {
if (browserEntries.querySelector(`#mod-${str}`)) {
return browser.setbutton(str, string, icon);
} else {
return false;
}
}
setTimeout(() => {
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 (mods.list().all[i].manifest_name) {
make(mods.normalize(mods.list().all[i].manifest_name));
}
}
}
else if (mod.includes(modfolder)) {make(modfolder);break}
}
}, 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 {
browser.mod_el_from_obj(pkgs[i]);
}catch(e) {}
count++;
packagecount++;
}
},
msg: (html) => {
let msg = document.createElement("div");
msg.classList.add("message");
msg.innerHTML = html;
browserEntries.appendChild(msg);
}
}
sort.querySelector("select").addEventListener("change", () => {
browser.loadfront();
})
setInterval(browser.add_pkg_properties, 1500);
if (navigator.onLine) {
browser.loadfront();
}
var view = document.querySelector(".popup#preview webview");
browser.preview = {
show: () => {
popups.show(preview, false);
},
hide: () => {
popups.hide(preview, false);
},
set: (url, autoshow) => {
if (autoshow != false) {browser.preview.show()}
view.src = url;
document.querySelector("#preview #external").setAttribute(
"onclick",
`require("electron").shell.openExternal("${url}")`
)
}
}
browser.mod_el_from_obj = (obj) => {
let pkg = {...obj, ...obj.versions[0]};
browser.mod_el({
pkg: pkg,
title: pkg.name,
image: pkg.icon,
author: pkg.owner,
url: pkg.package_url,
download: pkg.download_url,
version: pkg.version_number,
categories: pkg.categories,
description: pkg.description,
dependencies: pkg.dependencies,
})
}
browser.mod_el = (properties) => {
if (browser.filters.isfiltered(properties.categories)) {return}
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 installicon = "downloads";
let installstr = lang("gui.browser.install");
let normalized_title = mods.normalize(properties.title)
let installcallback = () => {
browser.install(properties);
}
let nondefault_install = {
"vanillaplus": "https://github.com/Zayveeo5e/NP.VanillaPlus/blob/main/README.md"
}
if (normalized_title in nondefault_install) {
installicon = "open";
installstr = lang("gui.browser.guide");
installcallback = () => {
shell.openExternal(nondefault_install[normalized_title])
}
}
else if (properties.pkg.local_version) {
installicon = "redo";
installstr = lang("gui.browser.reinstall");
if (properties.pkg.has_update) {
installicon = "downloads";
installstr = lang("gui.browser.update");
}
}
let entry = document.createElement("div");
entry.classList.add("el");
entry.id = `mod-${normalized_title}`;
entry.innerHTML = `
${properties.title}
${properties.description}
`
entry.querySelector("button.install").addEventListener("click", installcallback)
browserEntries.appendChild(entry);
}
browser.packages = async () => {
await browser.loadfront();
return packages;
}
let recent_toasts = {};
function add_recent_toast(name, timeout = 3000) {
if (recent_toasts[name]) {return}
recent_toasts[name] = true;
setTimeout(() => {
delete recent_toasts[name];
}, timeout)
}
ipcRenderer.on("removed-mod", (_, 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");
}
})
ipcRenderer.on("failed-mod", (_, modname) => {
if (recent_toasts["failed" + modname]) {return}
add_recent_toast("failed" + modname);
set_buttons(true);
toasts.show({
timeout: 10000,
scheme: "error",
title: lang("gui.toast.title.failed"),
description: lang("gui.toast.desc.failed")
})
})
ipcRenderer.on("legacy-duped-mod", (_, modname) => {
if (recent_toasts["duped" + modname]) {return}
add_recent_toast("duped" + modname);
set_buttons(true);
toasts.show({
timeout: 10000,
scheme: "warning",
title: lang("gui.toast.title.duped"),
description: modname + " " + lang("gui.toast.desc.duped")
})
})
ipcRenderer.on("no-internet", () => {
toasts.show({
timeout: 10000,
scheme: "error",
title: lang("gui.toast.title.no_internet"),
description: lang("gui.toast.desc.no_internet")
})
})
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;
set_buttons(true);
browser.setbutton(name, lang("gui.browser.reinstall"), "redo");
if (mod.malformed) {
toasts.show({
timeout: 8000,
scheme: "warning",
title: lang("gui.toast.title.malformed"),
description: name + " " + lang("gui.toast.desc.malformed")
})
}
toasts.show({
scheme: "success",
title: lang("gui.toast.title.installed"),
description: name + " " + lang("gui.toast.desc.installed")
})
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
)
mods.install_queue.shift();
}
})
let searchtimeout;
let searchstr = "";
let search = document.querySelector("#browser .search");
search.addEventListener("keyup", () => {
browser.filters.toggle(false);
clearTimeout(searchtimeout);
if (searchstr != search.value) {
if (search.value.replaceAll(" ", "") == "") {
searchstr = "";
browser.loadfront();
return
}
searchtimeout = setTimeout(() => {
browser.search(search.value);
searchstr = search.value;
}, 500)
}
})
let mouse_events = ["scroll", "mousedown", "touchdown"];
mouse_events.forEach((event) => {
document.body.addEventListener(event, () => {
let mouse_at = document.elementsFromPoint(mouseX, mouseY);
if (! mouse_at.includes(document.querySelector("#preview"))) {
browser.preview.hide();
}
if (! mouse_at.includes(document.querySelector("#filter"))
&& ! mouse_at.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_el.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 = ''"
)
}
module.exports = browser;