diff --git a/main.js b/main.js index e6adb93..f4709cd 100644 --- a/main.js +++ b/main.js @@ -11,9 +11,15 @@ 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"); @@ -27,11 +33,25 @@ const bepinexFiles = [".doorstop_version", "changelog.txt", "doorstop_config.ini let bepinexVersion; let bepinexBackupVersion; -const bepinexStore = new Store({ cwd: "bepinex-version" }); 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, @@ -51,7 +71,16 @@ async function createWindow() { } app.whenReady().then(() => { - createWindow(); + 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) { @@ -66,6 +95,11 @@ app.on("window-all-closed", () => { } }); +app.on("open-url", (event, url) => { + event.preventDefault(); + handleNxmUrl(url); +}); + ////////////////////////////////////////////////////// ///////////////// SAVING AND LOADING ///////////////// ipcMain.handle("save-path", (event, path) => { @@ -141,6 +175,31 @@ ipcMain.handle("load-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 //////////////////// @@ -197,9 +256,7 @@ ipcMain.handle("import-data", async () => { async function installBepinex() { if (await fileExists(bepinexBackupPath)) { if (await fileExists(`${bepinexBackupPath}/BepInEx`)) { - await fs.cp(`${bepinexBackupPath}/BepInEx`, bepinexFolderPath, { - recursive: true, - }); + await fs.cp(`${bepinexBackupPath}/BepInEx`, bepinexFolderPath, { recursive: true }); } 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 download = await fetch(asset.browser_download_url); - 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); + 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 () => { @@ -273,6 +325,10 @@ async function backupBepinex() { 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, @@ -331,7 +387,7 @@ async function verifyNexusAPI() { } ipcMain.handle("get-latest-mods", async () => { - if (nexus == undefined) { + if (!(await verifyNexusAPI())) { return; } @@ -339,12 +395,12 @@ ipcMain.handle("get-latest-mods", async () => { return mods; }); -ipcMain.handle("download-mod", async (event, link) => { - if (nexus == undefined) { +ipcMain.handle("open-download", async (event, link) => { + if (!(await verifyNexusAPI())) { return; } - const nexusWindow = new BrowserWindow({ + nexusWindow = new BrowserWindow({ width: 1080, height: 720, modal: true, @@ -358,6 +414,60 @@ ipcMain.handle("download-mod", async (event, 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 //////////////////// @@ -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); +} diff --git a/preload.js b/preload.js index 14873af..aa85862 100644 --- a/preload.js +++ b/preload.js @@ -23,6 +23,7 @@ contextBridge.exposeInMainWorld("files", { loadNexusAPI: () => ipcRenderer.invoke("load-nexus-api"), saveTheme: (theme, lacePinState) => ipcRenderer.invoke("save-theme", theme, lacePinState), loadTheme: () => ipcRenderer.invoke("load-theme"), + loadInstalledModsInfo: () => ipcRenderer.invoke("load-installed-mods-info"), }); contextBridge.exposeInMainWorld("electronAPI", { @@ -43,5 +44,6 @@ contextBridge.exposeInMainWorld("bepinex", { contextBridge.exposeInMainWorld("nexus", { verifyAPI: () => ipcRenderer.invoke("verify-nexus-api"), 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), }); diff --git a/renderer/index.html b/renderer/index.html index 3459167..6d1c2a2 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -106,6 +106,26 @@
+ +