mirror of
https://github.com/Gabi-Zar/Silk-Fly-Launcher.git
synced 2026-04-17 05:26:04 +02:00
549 lines
15 KiB
JavaScript
549 lines
15 KiB
JavaScript
import { app, BrowserWindow, ipcMain, dialog, shell } from "electron";
|
|
import path from "path";
|
|
import { fileURLToPath } from "url";
|
|
import Store from "electron-store";
|
|
import fs from "fs/promises";
|
|
import { createWriteStream } from "fs";
|
|
import { pipeline } from "stream/promises";
|
|
import extract from "extract-zip";
|
|
import NexusModule from "@nexusmods/nexus-api";
|
|
const Nexus = NexusModule.default;
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
const gotTheLock = app.requestSingleInstanceLock();
|
|
const isDev = !app.isPackaged;
|
|
|
|
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 modSavePath = `${userSavePath}\\mods`;
|
|
const dataPath = `${userSavePath}\\config.json`;
|
|
let silksongPath = store.get("silksong-path");
|
|
|
|
let nexusAPI = store.get("nexus-api");
|
|
let nexus = undefined;
|
|
createNexus();
|
|
|
|
let bepinexFolderPath = `${silksongPath}/BepInEx`;
|
|
let bepinexBackupPath = `${silksongPath}/BepInEx-Backup`;
|
|
const bepinexFiles = [".doorstop_version", "changelog.txt", "doorstop_config.ini", "winhttp.dll"];
|
|
|
|
let bepinexVersion;
|
|
let bepinexBackupVersion;
|
|
|
|
let mainWindow;
|
|
let nexusWindow;
|
|
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() {
|
|
mainWindow = new BrowserWindow({
|
|
width: 1280,
|
|
height: 720,
|
|
webPreferences: {
|
|
preload: path.join(__dirname, "preload.js"),
|
|
},
|
|
});
|
|
|
|
if (await fileExists(dataPath)) {
|
|
htmlFile = "index.html";
|
|
} else {
|
|
htmlFile = "welcome.html";
|
|
}
|
|
|
|
mainWindow.loadFile(`renderer/${htmlFile}`);
|
|
}
|
|
|
|
app.whenReady().then(() => {
|
|
if (isDev) {
|
|
app.setAsDefaultProtocolClient("nxm", process.execPath, [path.resolve(process.argv[1])]);
|
|
} else {
|
|
app.setAsDefaultProtocolClient("nxm");
|
|
}
|
|
|
|
if (gotTheLock) {
|
|
checkInstalledMods();
|
|
createWindow();
|
|
}
|
|
|
|
app.on("activate", () => {
|
|
if (BrowserWindow.getAllWindows().length === 0) {
|
|
createWindow();
|
|
}
|
|
});
|
|
});
|
|
|
|
app.on("window-all-closed", () => {
|
|
if (process.platform !== "darwin") {
|
|
app.quit();
|
|
}
|
|
});
|
|
|
|
app.on("open-url", (event, url) => {
|
|
event.preventDefault();
|
|
handleNxmUrl(url);
|
|
});
|
|
|
|
//////////////////////////////////////////////////////
|
|
///////////////// SAVING AND LOADING /////////////////
|
|
ipcMain.handle("save-path", (event, path) => {
|
|
saveSilksongPath(path);
|
|
});
|
|
function saveSilksongPath(path) {
|
|
silksongPath = path;
|
|
bepinexFolderPath = `${silksongPath}/BepInEx`;
|
|
bepinexBackupPath = `${silksongPath}/BepInEx-Backup`;
|
|
store.set("silksong-path", silksongPath);
|
|
}
|
|
|
|
ipcMain.handle("load-path", () => {
|
|
silksongPath = store.get("silksong-path");
|
|
if (silksongPath == undefined) {
|
|
return "";
|
|
}
|
|
return silksongPath;
|
|
});
|
|
|
|
function saveBepinexVersion(version) {
|
|
bepinexVersion = version;
|
|
if (bepinexVersion == undefined) {
|
|
bepinexStore.delete("bepinex-version");
|
|
return;
|
|
}
|
|
bepinexStore.set("bepinex-version", version);
|
|
}
|
|
|
|
ipcMain.handle("load-bepinex-version", () => {
|
|
bepinexVersion = bepinexStore.get("bepinex-version");
|
|
return bepinexVersion;
|
|
});
|
|
|
|
function saveBepinexBackupVersion(version) {
|
|
bepinexBackupVersion = version;
|
|
if (bepinexBackupVersion == undefined) {
|
|
bepinexStore.delete("bepinex-backup-version");
|
|
return;
|
|
}
|
|
bepinexStore.set("bepinex-backup-version", version);
|
|
}
|
|
|
|
ipcMain.handle("load-bepinex-backup-version", () => {
|
|
bepinexBackupVersion = bepinexStore.get("bepinex-backup-version");
|
|
return bepinexBackupVersion;
|
|
});
|
|
|
|
ipcMain.handle("save-nexus-api", (event, api) => {
|
|
nexusAPI = api;
|
|
createNexus();
|
|
store.set("nexus-api", nexusAPI);
|
|
});
|
|
|
|
ipcMain.handle("load-nexus-api", () => {
|
|
nexusAPI = store.get("nexus-api");
|
|
if (nexusAPI == undefined) {
|
|
return "";
|
|
}
|
|
return nexusAPI;
|
|
});
|
|
|
|
ipcMain.handle("save-theme", (event, theme, lacePinState) => {
|
|
store.set("theme.theme", theme);
|
|
store.set("theme.lacePinState", lacePinState);
|
|
});
|
|
|
|
ipcMain.handle("load-theme", () => {
|
|
const theme = [store.get("theme.theme"), store.get("theme.lacePinState")];
|
|
if (theme[0] == undefined) {
|
|
return ["Silksong", false];
|
|
}
|
|
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 ////////////////////
|
|
|
|
async function fileExists(filePath) {
|
|
try {
|
|
await fs.access(filePath);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
ipcMain.handle("delete-data", async () => {
|
|
if (await fileExists(dataPath)) {
|
|
await fs.unlink(dataPath);
|
|
}
|
|
});
|
|
|
|
ipcMain.handle("export-data", async () => {
|
|
if (!(await fileExists(dataPath))) {
|
|
return;
|
|
}
|
|
|
|
const { canceled, filePath } = await dialog.showSaveDialog({
|
|
title: "Export Data",
|
|
defaultPath: "config.json",
|
|
filters: [{ name: "JSON", extensions: ["json"] }],
|
|
});
|
|
|
|
if (canceled || !filePath) return;
|
|
|
|
await fs.copyFile(dataPath, filePath);
|
|
});
|
|
|
|
ipcMain.handle("import-data", async () => {
|
|
const { canceled, filePaths } = await dialog.showOpenDialog({
|
|
title: "Import Data",
|
|
properties: ["openFile"],
|
|
filters: [{ name: "JSON", extensions: ["json"] }],
|
|
});
|
|
|
|
if (canceled || !filePaths) return false;
|
|
|
|
if (await fileExists(dataPath)) {
|
|
await fs.unlink(dataPath);
|
|
}
|
|
await fs.copyFile(filePaths[0], dataPath, fs.constants.COPYFILE_EXCL);
|
|
return true;
|
|
});
|
|
|
|
//////////////////////////////////////////////////////
|
|
////////////////////// BEPINEX ///////////////////////
|
|
|
|
async function installBepinex() {
|
|
if (await fileExists(bepinexBackupPath)) {
|
|
if (await fileExists(`${bepinexBackupPath}/BepInEx`)) {
|
|
await fs.cp(`${bepinexBackupPath}/BepInEx`, bepinexFolderPath, { recursive: true });
|
|
}
|
|
|
|
for (const file of bepinexFiles) {
|
|
const filePath = `${silksongPath}/${file}`;
|
|
if (await fileExists(`${bepinexBackupPath}/${file}`)) {
|
|
await fs.copyFile(`${bepinexBackupPath}/${file}`, filePath);
|
|
}
|
|
}
|
|
await fs.rm(bepinexBackupPath, { recursive: true });
|
|
|
|
bepinexBackupVersion = bepinexStore.get("bepinex-backup-version");
|
|
saveBepinexVersion(bepinexBackupVersion);
|
|
saveBepinexBackupVersion(undefined);
|
|
} else {
|
|
const GITHUB_URL = "https://api.github.com/repos/bepinex/bepinex/releases/latest";
|
|
|
|
const res = await fetch(GITHUB_URL, {
|
|
headers: {
|
|
"User-Agent": "SilkFlyLauncher/1.0.0",
|
|
Accept: "application/vnd.github+json",
|
|
},
|
|
});
|
|
|
|
if (!res.ok) {
|
|
throw new Error(`GitHub API error: ${res.status}`);
|
|
}
|
|
|
|
const release = await res.json();
|
|
|
|
const asset = release.assets.find((a) => a.name.endsWith(".zip") && a.name.toLowerCase().includes("win_x64"));
|
|
|
|
await downloadAndUnzip(asset.browser_download_url, silksongPath);
|
|
|
|
saveBepinexVersion(release.tag_name);
|
|
}
|
|
|
|
if (await fileExists(modSavePath)) {
|
|
await fs.cp(`${modSavePath}`, `${bepinexFolderPath}/plugins`, { recursive: true });
|
|
}
|
|
}
|
|
|
|
ipcMain.handle("install-bepinex", async () => {
|
|
await installBepinex();
|
|
});
|
|
|
|
async function uninstallBepinex() {
|
|
if (await fileExists(bepinexFolderPath)) {
|
|
await fs.rm(bepinexFolderPath, { recursive: true });
|
|
}
|
|
|
|
for (const file of bepinexFiles) {
|
|
const filePath = `${silksongPath}/${file}`;
|
|
if (await fileExists(filePath)) {
|
|
await fs.unlink(filePath);
|
|
}
|
|
}
|
|
saveBepinexVersion(undefined);
|
|
}
|
|
|
|
ipcMain.handle("uninstall-bepinex", async () => {
|
|
await uninstallBepinex();
|
|
});
|
|
|
|
async function backupBepinex() {
|
|
if ((await fileExists(bepinexBackupPath)) == false) {
|
|
await fs.mkdir(bepinexBackupPath);
|
|
}
|
|
|
|
if (fileExists(`${bepinexFolderPath}/plugins`)) {
|
|
await fs.rm(`${bepinexFolderPath}/plugins`, { recursive: true });
|
|
}
|
|
|
|
if (await fileExists(bepinexFolderPath)) {
|
|
await fs.cp(bepinexFolderPath, `${bepinexBackupPath}/BepInEx`, {
|
|
recursive: true,
|
|
});
|
|
}
|
|
|
|
for (const file of bepinexFiles) {
|
|
const filePath = `${silksongPath}/${file}`;
|
|
if (await fileExists(filePath)) {
|
|
await fs.copyFile(filePath, `${bepinexBackupPath}/${file}`);
|
|
}
|
|
}
|
|
|
|
saveBepinexBackupVersion(bepinexVersion);
|
|
await uninstallBepinex();
|
|
}
|
|
|
|
ipcMain.handle("backup-bepinex", async () => {
|
|
await backupBepinex();
|
|
});
|
|
|
|
ipcMain.handle("delete-bepinex-backup", async () => {
|
|
if (await fileExists(bepinexBackupPath)) {
|
|
await fs.rm(bepinexBackupPath, { recursive: true });
|
|
saveBepinexBackupVersion(undefined);
|
|
}
|
|
});
|
|
|
|
//////////////////////////////////////////////////////
|
|
/////////////////////// NEXUS ////////////////////////
|
|
|
|
async function createNexus() {
|
|
if (nexusAPI == undefined) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
nexus = await Nexus.create(nexusAPI, "silk-fly-launcher", "1.0.0", "hollowknightsilksong");
|
|
} catch (error) {
|
|
console.log(error);
|
|
nexus = undefined;
|
|
}
|
|
}
|
|
|
|
ipcMain.handle("verify-nexus-api", async () => {
|
|
return await verifyNexusAPI();
|
|
});
|
|
|
|
async function verifyNexusAPI() {
|
|
if (nexus == undefined) {
|
|
return false;
|
|
}
|
|
if (await nexus.getValidationResult()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
ipcMain.handle("get-latest-mods", async () => {
|
|
if (!(await verifyNexusAPI())) {
|
|
return;
|
|
}
|
|
|
|
const mods = await nexus.getLatestAdded();
|
|
return mods;
|
|
});
|
|
|
|
ipcMain.handle("open-download", async (event, link) => {
|
|
if (!(await verifyNexusAPI())) {
|
|
return;
|
|
}
|
|
|
|
nexusWindow = new BrowserWindow({
|
|
width: 1080,
|
|
height: 720,
|
|
modal: true,
|
|
parent: mainWindow,
|
|
webPreferences: {
|
|
nodeIntegration: false,
|
|
contextIsolation: true,
|
|
},
|
|
});
|
|
|
|
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 ////////////////////
|
|
|
|
ipcMain.handle("auto-detect-game-path", async () => {
|
|
const defaultsSilksongPaths = [":/Program Files (x86)/Steam/steamapps/common/Hollow Knight Silksong", ":/SteamLibrary/steamapps/common/Hollow Knight Silksong"];
|
|
for (const path of defaultsSilksongPaths) {
|
|
for (let i = "A".charCodeAt(0); i <= "Z".charCodeAt(0); i++) {
|
|
const fullPath = `${String.fromCharCode(i)}${path}`;
|
|
if (await fileExists(fullPath)) {
|
|
saveSilksongPath(fullPath);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
ipcMain.handle("load-main-page", () => {
|
|
htmlFile = "index.html";
|
|
mainWindow.loadFile(`renderer/${htmlFile}`);
|
|
});
|
|
|
|
ipcMain.handle("get-page", () => {
|
|
return htmlFile;
|
|
});
|
|
|
|
ipcMain.handle("open-link", async (event, link) => {
|
|
await shell.openExternal(link);
|
|
});
|
|
|
|
ipcMain.handle("open-window", async (event, file) => {
|
|
const win = new BrowserWindow({
|
|
width: 600,
|
|
height: 720,
|
|
modal: true,
|
|
parent: mainWindow,
|
|
webPreferences: {
|
|
nodeIntegration: false,
|
|
contextIsolation: true,
|
|
},
|
|
});
|
|
|
|
win.title = file;
|
|
win.loadFile(file);
|
|
});
|
|
|
|
ipcMain.handle("launch-game", async (event, mode) => {
|
|
const silksongExecutablePath = `${silksongPath}/Hollow Knight Silksong.exe`;
|
|
if (mode === "modded") {
|
|
if (await fileExists(bepinexFolderPath)) {
|
|
await shell.openExternal(silksongExecutablePath);
|
|
} else {
|
|
await installBepinex();
|
|
await shell.openExternal(silksongExecutablePath);
|
|
}
|
|
}
|
|
if (mode === "vanilla") {
|
|
if (await fileExists(bepinexFolderPath)) {
|
|
await backupBepinex();
|
|
await shell.openExternal(silksongExecutablePath);
|
|
} else {
|
|
await shell.openExternal(silksongExecutablePath);
|
|
}
|
|
}
|
|
});
|
|
|
|
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);
|
|
}
|