mirror of
https://github.com/Gabi-Zar/Silk-Fly-Launcher.git
synced 2026-04-17 05:26:04 +02:00
Add the ability to download mods from Nexus, add mod data saving, and allow mods to be saved even if BepInEx is not installed.
This commit is contained in:
160
main.js
160
main.js
@@ -11,9 +11,15 @@ const Nexus = NexusModule.default;
|
|||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
|
const isDev = !app.isPackaged;
|
||||||
|
|
||||||
const store = new Store();
|
const store = new Store();
|
||||||
|
const bepinexStore = new Store({ cwd: "bepinex-version" });
|
||||||
|
const installedModsStore = new Store({ cwd: "installed-mods-version" });
|
||||||
|
|
||||||
const userSavePath = app.getPath("userData");
|
const userSavePath = app.getPath("userData");
|
||||||
|
const modSavePath = `${userSavePath}\\mods`;
|
||||||
const dataPath = `${userSavePath}\\config.json`;
|
const dataPath = `${userSavePath}\\config.json`;
|
||||||
let silksongPath = store.get("silksong-path");
|
let silksongPath = store.get("silksong-path");
|
||||||
|
|
||||||
@@ -27,11 +33,25 @@ const bepinexFiles = [".doorstop_version", "changelog.txt", "doorstop_config.ini
|
|||||||
|
|
||||||
let bepinexVersion;
|
let bepinexVersion;
|
||||||
let bepinexBackupVersion;
|
let bepinexBackupVersion;
|
||||||
const bepinexStore = new Store({ cwd: "bepinex-version" });
|
|
||||||
|
|
||||||
let mainWindow;
|
let mainWindow;
|
||||||
|
let nexusWindow;
|
||||||
let htmlFile;
|
let htmlFile;
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
////////////////////// STARTUP ///////////////////////
|
||||||
|
|
||||||
|
if (!gotTheLock) {
|
||||||
|
app.quit();
|
||||||
|
} else {
|
||||||
|
app.on("second-instance", (event, argv) => {
|
||||||
|
const nxmUrl = argv.find((arg) => arg.startsWith("nxm://"));
|
||||||
|
if (nxmUrl) {
|
||||||
|
handleNxmUrl(nxmUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function createWindow() {
|
async function createWindow() {
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 1280,
|
width: 1280,
|
||||||
@@ -51,7 +71,16 @@ async function createWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
|
if (isDev) {
|
||||||
|
app.setAsDefaultProtocolClient("nxm", process.execPath, [path.resolve(process.argv[1])]);
|
||||||
|
} else {
|
||||||
|
app.setAsDefaultProtocolClient("nxm");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gotTheLock) {
|
||||||
|
checkInstalledMods();
|
||||||
createWindow();
|
createWindow();
|
||||||
|
}
|
||||||
|
|
||||||
app.on("activate", () => {
|
app.on("activate", () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
@@ -66,6 +95,11 @@ app.on("window-all-closed", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.on("open-url", (event, url) => {
|
||||||
|
event.preventDefault();
|
||||||
|
handleNxmUrl(url);
|
||||||
|
});
|
||||||
|
|
||||||
//////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////
|
||||||
///////////////// SAVING AND LOADING /////////////////
|
///////////////// SAVING AND LOADING /////////////////
|
||||||
ipcMain.handle("save-path", (event, path) => {
|
ipcMain.handle("save-path", (event, path) => {
|
||||||
@@ -141,6 +175,31 @@ ipcMain.handle("load-theme", () => {
|
|||||||
return theme;
|
return theme;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function saveModInfo(modId, suppr = false) {
|
||||||
|
if (suppr == true) {
|
||||||
|
installedModsStore.delete(String(modId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modInfo = await nexus.getModInfo(modId);
|
||||||
|
|
||||||
|
installedModsStore.set(`${modId}.mod_id`, modInfo.mod_id);
|
||||||
|
installedModsStore.set(`${modId}.name`, modInfo.name);
|
||||||
|
installedModsStore.set(`${modId}.summary`, modInfo.summary);
|
||||||
|
installedModsStore.set(`${modId}.picture_url`, modInfo.picture_url);
|
||||||
|
installedModsStore.set(`${modId}.version`, modInfo.version);
|
||||||
|
installedModsStore.set(`${modId}.updated_time`, modInfo.updated_time);
|
||||||
|
installedModsStore.set(`${modId}.author`, modInfo.author);
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle("load-installed-mods-info", () => {
|
||||||
|
let modsInfo = [];
|
||||||
|
for (const [key, modInfo] of Object.entries(installedModsStore.store)) {
|
||||||
|
modsInfo.push(modInfo);
|
||||||
|
}
|
||||||
|
return modsInfo;
|
||||||
|
});
|
||||||
|
|
||||||
//////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////
|
||||||
/////////////////// DATA HANDLING ////////////////////
|
/////////////////// DATA HANDLING ////////////////////
|
||||||
|
|
||||||
@@ -197,9 +256,7 @@ ipcMain.handle("import-data", async () => {
|
|||||||
async function installBepinex() {
|
async function installBepinex() {
|
||||||
if (await fileExists(bepinexBackupPath)) {
|
if (await fileExists(bepinexBackupPath)) {
|
||||||
if (await fileExists(`${bepinexBackupPath}/BepInEx`)) {
|
if (await fileExists(`${bepinexBackupPath}/BepInEx`)) {
|
||||||
await fs.cp(`${bepinexBackupPath}/BepInEx`, bepinexFolderPath, {
|
await fs.cp(`${bepinexBackupPath}/BepInEx`, bepinexFolderPath, { recursive: true });
|
||||||
recursive: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const file of bepinexFiles) {
|
for (const file of bepinexFiles) {
|
||||||
@@ -231,19 +288,14 @@ async function installBepinex() {
|
|||||||
|
|
||||||
const asset = release.assets.find((a) => a.name.endsWith(".zip") && a.name.toLowerCase().includes("win_x64"));
|
const asset = release.assets.find((a) => a.name.endsWith(".zip") && a.name.toLowerCase().includes("win_x64"));
|
||||||
|
|
||||||
const download = await fetch(asset.browser_download_url);
|
await downloadAndUnzip(asset.browser_download_url, silksongPath);
|
||||||
if (!download.ok) {
|
|
||||||
throw new Error("Download error");
|
|
||||||
}
|
|
||||||
const filePath = `${userSavePath}\\bepinex.zip`;
|
|
||||||
|
|
||||||
await pipeline(download.body, createWriteStream(filePath));
|
|
||||||
|
|
||||||
await extract(filePath, { dir: silksongPath });
|
|
||||||
await fs.unlink(filePath);
|
|
||||||
|
|
||||||
saveBepinexVersion(release.tag_name);
|
saveBepinexVersion(release.tag_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (await fileExists(modSavePath)) {
|
||||||
|
await fs.cp(`${modSavePath}`, `${bepinexFolderPath}/plugins`, { recursive: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.handle("install-bepinex", async () => {
|
ipcMain.handle("install-bepinex", async () => {
|
||||||
@@ -273,6 +325,10 @@ async function backupBepinex() {
|
|||||||
await fs.mkdir(bepinexBackupPath);
|
await fs.mkdir(bepinexBackupPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fileExists(`${bepinexFolderPath}/plugins`)) {
|
||||||
|
await fs.rm(`${bepinexFolderPath}/plugins`, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
if (await fileExists(bepinexFolderPath)) {
|
if (await fileExists(bepinexFolderPath)) {
|
||||||
await fs.cp(bepinexFolderPath, `${bepinexBackupPath}/BepInEx`, {
|
await fs.cp(bepinexFolderPath, `${bepinexBackupPath}/BepInEx`, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
@@ -331,7 +387,7 @@ async function verifyNexusAPI() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.handle("get-latest-mods", async () => {
|
ipcMain.handle("get-latest-mods", async () => {
|
||||||
if (nexus == undefined) {
|
if (!(await verifyNexusAPI())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,12 +395,12 @@ ipcMain.handle("get-latest-mods", async () => {
|
|||||||
return mods;
|
return mods;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("download-mod", async (event, link) => {
|
ipcMain.handle("open-download", async (event, link) => {
|
||||||
if (nexus == undefined) {
|
if (!(await verifyNexusAPI())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nexusWindow = new BrowserWindow({
|
nexusWindow = new BrowserWindow({
|
||||||
width: 1080,
|
width: 1080,
|
||||||
height: 720,
|
height: 720,
|
||||||
modal: true,
|
modal: true,
|
||||||
@@ -358,6 +414,60 @@ ipcMain.handle("download-mod", async (event, link) => {
|
|||||||
nexusWindow.loadURL(link);
|
nexusWindow.loadURL(link);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function handleNxmUrl(url) {
|
||||||
|
nexusWindow.close();
|
||||||
|
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
|
||||||
|
const key = parsedUrl.searchParams.get("key");
|
||||||
|
const expires = Number(parsedUrl.searchParams.get("expires"));
|
||||||
|
|
||||||
|
const [, , modId, , fileId] = parsedUrl.pathname.split("/");
|
||||||
|
|
||||||
|
startDownload(Number(modId), Number(fileId), key, expires);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startDownload(modId, fileId, key, expires) {
|
||||||
|
if (!(await verifyNexusAPI())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = await nexus.getDownloadURLs(modId, fileId, key, expires);
|
||||||
|
const download_url = url[0].URI;
|
||||||
|
|
||||||
|
if (!(await fileExists(modSavePath))) {
|
||||||
|
await fs.mkdir(modSavePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
await downloadAndUnzip(download_url, `${modSavePath}/${modId}`);
|
||||||
|
if (await fileExists(bepinexFolderPath)) {
|
||||||
|
await fs.cp(`${modSavePath}/${modId}`, `${bepinexFolderPath}/plugins/${modId}`, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
saveModInfo(modId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkInstalledMods() {
|
||||||
|
for (const [key, modInfo] of Object.entries(installedModsStore.store)) {
|
||||||
|
if (!(await fileExists(`${modSavePath}/${modInfo.mod_id}`))) {
|
||||||
|
saveModInfo(key, true);
|
||||||
|
await fs.rm(`${bepinexFolderPath}/plugins/${modInfo.mod_id}`, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle("uninstall-mod", async (event, modId) => {
|
||||||
|
const modPath = `${bepinexFolderPath}/plugins/${modId}`;
|
||||||
|
if (await fileExists(`${modSavePath}/${modId}`)) {
|
||||||
|
await fs.rm(`${modSavePath}/${modId}`, { recursive: true });
|
||||||
|
}
|
||||||
|
if (await fileExists(modPath)) {
|
||||||
|
await fs.rm(modPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
saveModInfo(modId, true);
|
||||||
|
});
|
||||||
|
|
||||||
//////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////
|
||||||
//////////////////// UNCATEGORIZE ////////////////////
|
//////////////////// UNCATEGORIZE ////////////////////
|
||||||
|
|
||||||
@@ -422,3 +532,17 @@ ipcMain.handle("launch-game", async (event, mode) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function downloadAndUnzip(url, path) {
|
||||||
|
const download = await fetch(url);
|
||||||
|
if (!download.ok) {
|
||||||
|
throw new Error("Download error");
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempPath = `${userSavePath}\\tempZip.zip`;
|
||||||
|
|
||||||
|
await pipeline(download.body, createWriteStream(tempPath));
|
||||||
|
|
||||||
|
await extract(tempPath, { dir: path });
|
||||||
|
await fs.unlink(tempPath);
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ contextBridge.exposeInMainWorld("files", {
|
|||||||
loadNexusAPI: () => ipcRenderer.invoke("load-nexus-api"),
|
loadNexusAPI: () => ipcRenderer.invoke("load-nexus-api"),
|
||||||
saveTheme: (theme, lacePinState) => ipcRenderer.invoke("save-theme", theme, lacePinState),
|
saveTheme: (theme, lacePinState) => ipcRenderer.invoke("save-theme", theme, lacePinState),
|
||||||
loadTheme: () => ipcRenderer.invoke("load-theme"),
|
loadTheme: () => ipcRenderer.invoke("load-theme"),
|
||||||
|
loadInstalledModsInfo: () => ipcRenderer.invoke("load-installed-mods-info"),
|
||||||
});
|
});
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("electronAPI", {
|
contextBridge.exposeInMainWorld("electronAPI", {
|
||||||
@@ -43,5 +44,6 @@ contextBridge.exposeInMainWorld("bepinex", {
|
|||||||
contextBridge.exposeInMainWorld("nexus", {
|
contextBridge.exposeInMainWorld("nexus", {
|
||||||
verifyAPI: () => ipcRenderer.invoke("verify-nexus-api"),
|
verifyAPI: () => ipcRenderer.invoke("verify-nexus-api"),
|
||||||
getLatestMods: () => ipcRenderer.invoke("get-latest-mods"),
|
getLatestMods: () => ipcRenderer.invoke("get-latest-mods"),
|
||||||
download: (link) => ipcRenderer.invoke("download-mod", link),
|
download: (link) => ipcRenderer.invoke("open-download", link),
|
||||||
|
uninstall: (modId) => ipcRenderer.invoke("uninstall-mod", modId),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -106,6 +106,26 @@
|
|||||||
<div class="mods-container" id="mods-container"></div>
|
<div class="mods-container" id="mods-container"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template id="installed-mod-template">
|
||||||
|
<div class="mod-container">
|
||||||
|
<div class="mod-text">
|
||||||
|
<div class="horizontal-div">
|
||||||
|
<h3 id="mod-title">Unknown Title</h3>
|
||||||
|
<p id="mod-author">Unknown author</p>
|
||||||
|
</div>
|
||||||
|
<p id="mod-description">No description provided</p>
|
||||||
|
<p class="transparent-text" id="mod-version">V1.0.0 last update on 01/01/2026</p>
|
||||||
|
|
||||||
|
<div class="horizontal-div">
|
||||||
|
<a href="www.nexusmods.com/hollowknightsilksong/mods" class="default-button" id="uninstall-mod-button">Uninstall</a>
|
||||||
|
<a href="www.nexusmods.com/hollowknightsilksong/mods" class="default-button" id="external-link">Website</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<img class="mod-icon" src="assets/placeholder_icon.png" alt="mod icon" id="mod-icon" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template id="mod-template">
|
<template id="mod-template">
|
||||||
<div class="mod-container">
|
<div class="mod-container">
|
||||||
<div class="mod-text">
|
<div class="mod-text">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const HomeTemplate = document.getElementById("home-template");
|
|||||||
const installedModsTemplate = document.getElementById("installed-mods-template");
|
const installedModsTemplate = document.getElementById("installed-mods-template");
|
||||||
const onlineModsTemplate = document.getElementById("online-mods-template");
|
const onlineModsTemplate = document.getElementById("online-mods-template");
|
||||||
const settingsTemplate = document.getElementById("settings-template");
|
const settingsTemplate = document.getElementById("settings-template");
|
||||||
|
const installedModTemplate = document.getElementById("installed-mod-template");
|
||||||
const modTemplate = document.getElementById("mod-template");
|
const modTemplate = document.getElementById("mod-template");
|
||||||
|
|
||||||
let oldPage;
|
let oldPage;
|
||||||
@@ -50,6 +51,9 @@ async function navigate(page) {
|
|||||||
if (oldPage == page) {
|
if (oldPage == page) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (page == "refresh") {
|
||||||
|
page = oldPage;
|
||||||
|
}
|
||||||
oldPage = page;
|
oldPage = page;
|
||||||
|
|
||||||
view.replaceChildren();
|
view.replaceChildren();
|
||||||
@@ -63,6 +67,7 @@ async function navigate(page) {
|
|||||||
case "mods-installed":
|
case "mods-installed":
|
||||||
title.innerText = "Installed Mods";
|
title.innerText = "Installed Mods";
|
||||||
const installedModsTemplateCopy = installedModsTemplate.content.cloneNode(true);
|
const installedModsTemplateCopy = installedModsTemplate.content.cloneNode(true);
|
||||||
|
const installedModsContainer = installedModsTemplateCopy.getElementById("mods-container");
|
||||||
const searchFormInstalled = installedModsTemplateCopy.getElementById("search-form");
|
const searchFormInstalled = installedModsTemplateCopy.getElementById("search-form");
|
||||||
|
|
||||||
searchFormInstalled.addEventListener("submit", async function (event) {
|
searchFormInstalled.addEventListener("submit", async function (event) {
|
||||||
@@ -70,6 +75,56 @@ async function navigate(page) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
view.appendChild(installedModsTemplateCopy);
|
view.appendChild(installedModsTemplateCopy);
|
||||||
|
|
||||||
|
const modsInfo = await files.loadInstalledModsInfo();
|
||||||
|
if (modsInfo == []) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const modInfo of modsInfo) {
|
||||||
|
const installedModTemplateCopy = installedModTemplate.content.cloneNode(true);
|
||||||
|
if (modInfo.name) {
|
||||||
|
const modTitleText = installedModTemplateCopy.getElementById("mod-title");
|
||||||
|
modTitleText.innerText = modInfo.name;
|
||||||
|
}
|
||||||
|
if (modInfo.author) {
|
||||||
|
const modAuthorText = installedModTemplateCopy.getElementById("mod-author");
|
||||||
|
modAuthorText.innerText = `by ${modInfo.author}`;
|
||||||
|
}
|
||||||
|
if (modInfo.summary) {
|
||||||
|
const modDescriptionText = installedModTemplateCopy.getElementById("mod-description");
|
||||||
|
modDescriptionText.innerText = modInfo.summary;
|
||||||
|
}
|
||||||
|
if (modInfo.picture_url) {
|
||||||
|
const modPicture = installedModTemplateCopy.getElementById("mod-icon");
|
||||||
|
modPicture.src = modInfo.picture_url;
|
||||||
|
}
|
||||||
|
if (modInfo.version && modInfo.updated_time) {
|
||||||
|
const modVersionText = installedModTemplateCopy.getElementById("mod-version");
|
||||||
|
modVersionText.innerText = `V${modInfo.version} last updated on ${modInfo.updated_time.slice(0, 10)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modUrl = `https://www.nexusmods.com/hollowknightsilksong/mods/${modInfo.mod_id}`;
|
||||||
|
|
||||||
|
const modLinkButton = installedModTemplateCopy.getElementById("external-link");
|
||||||
|
modLinkButton.href = modUrl;
|
||||||
|
modLinkButton.addEventListener("click", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const modLink = modLinkButton.href;
|
||||||
|
electronAPI.openExternalLink(modLink);
|
||||||
|
});
|
||||||
|
|
||||||
|
modDownloadButton = installedModTemplateCopy.getElementById("uninstall-mod-button");
|
||||||
|
modDownloadButton.addEventListener("click", async function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
await nexus.uninstall(modInfo.mod_id);
|
||||||
|
|
||||||
|
navigate("refresh");
|
||||||
|
});
|
||||||
|
|
||||||
|
installedModsContainer.appendChild(installedModTemplateCopy);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "mods-online":
|
case "mods-online":
|
||||||
|
|||||||
Reference in New Issue
Block a user