mirror of
https://github.com/Gabi-Zar/Silk-Fly-Launcher.git
synced 2026-04-17 05:26:04 +02:00
Add support for local mods and fix inaccurate mod sizes from Nexus Mods
This commit is contained in:
85
main.js
85
main.js
@@ -12,6 +12,8 @@ import node7z from "node-7z";
|
|||||||
const { extractFull } = node7z;
|
const { extractFull } = node7z;
|
||||||
import packageJson from "./package.json" with { type: "json" };
|
import packageJson from "./package.json" with { type: "json" };
|
||||||
import semverGt from "semver/functions/gt.js";
|
import semverGt from "semver/functions/gt.js";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
import { unzip } from "zlib";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
@@ -172,6 +174,7 @@ ipcMain.handle("save-path", (event, path) => {
|
|||||||
|
|
||||||
function saveSilksongPath(path) {
|
function saveSilksongPath(path) {
|
||||||
store.set("silksong-path", path);
|
store.set("silksong-path", path);
|
||||||
|
checkInstalledMods();
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadSilksongPath() {
|
function loadSilksongPath() {
|
||||||
@@ -251,14 +254,29 @@ ipcMain.handle("load-theme", () => {
|
|||||||
return theme;
|
return theme;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function saveModInfo(modId, suppr = false) {
|
async function saveModInfo(modId, suppr = false, optionalModInfo = {}) {
|
||||||
if (suppr == true) {
|
if (suppr == true) {
|
||||||
installedModsStore.delete(String(modId));
|
installedModsStore.delete(String(modId));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const modInfo = onlineCachedModList.find((mod) => mod.modId == modId);
|
let modInfo;
|
||||||
|
if (onlineCachedModList) {
|
||||||
|
modInfo = onlineCachedModList.find((mod) => mod.modId == modId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!modInfo) {
|
||||||
|
modInfo = optionalModInfo;
|
||||||
|
}
|
||||||
modInfo.activated = true;
|
modInfo.activated = true;
|
||||||
|
|
||||||
|
const modFiles = await fs.readdir(path.join(modSavePath, modId));
|
||||||
|
modInfo.fileSize = 0;
|
||||||
|
for (const file of modFiles) {
|
||||||
|
const fileStats = await fs.stat(path.join(modSavePath, modId, file));
|
||||||
|
modInfo.fileSize += fileStats.size;
|
||||||
|
}
|
||||||
|
|
||||||
installedModsStore.set(String(modId), modInfo);
|
installedModsStore.set(String(modId), modInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,6 +636,7 @@ async function searchNexusMods(keywords, offset = 0, count = 10, sortFilter = "d
|
|||||||
onlineCachedModList = data.mods.nodes;
|
onlineCachedModList = data.mods.nodes;
|
||||||
|
|
||||||
for (let i = 0; i < onlineCachedModList.length; i++) {
|
for (let i = 0; i < onlineCachedModList.length; i++) {
|
||||||
|
onlineCachedModList[i].source = "nexusmods";
|
||||||
if (onlineCachedModList[i].modId == 26) {
|
if (onlineCachedModList[i].modId == 26) {
|
||||||
onlineCachedModList.splice(i, 1);
|
onlineCachedModList.splice(i, 1);
|
||||||
}
|
}
|
||||||
@@ -695,18 +714,26 @@ ipcMain.handle("uninstall-mod", async (event, modId) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("activate-mod", async (event, modId) => {
|
ipcMain.handle("activate-mod", async (event, modId) => {
|
||||||
|
await activateMod(modId);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function activateMod(modId) {
|
||||||
|
if (!loadSilksongPath()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const BepinexPluginsPath = path.join(loadSilksongPath(), "BepInEx", "plugins");
|
const BepinexPluginsPath = path.join(loadSilksongPath(), "BepInEx", "plugins");
|
||||||
|
|
||||||
if (!installedModsStore.get(`${modId}.activated`)) {
|
if (!installedModsStore.get(`${modId}.activated`)) {
|
||||||
installedModsStore.set(`${modId}.activated`, true);
|
installedModsStore.set(`${modId}.activated`, true);
|
||||||
|
}
|
||||||
|
|
||||||
if (bepinexVersion) {
|
if (bepinexVersion) {
|
||||||
if (!(await fileExists(path.join(BepinexPluginsPath, String(modId))))) {
|
if (!(await fileExists(path.join(BepinexPluginsPath, String(modId))))) {
|
||||||
await fs.cp(path.join(modSavePath, String(modId)), path.join(BepinexPluginsPath, String(modId)), { recursive: true });
|
await fs.cp(path.join(modSavePath, String(modId)), path.join(BepinexPluginsPath, String(modId)), { recursive: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle("deactivate-mod", async (event, modId) => {
|
ipcMain.handle("deactivate-mod", async (event, modId) => {
|
||||||
const BepinexPluginsPath = path.join(loadSilksongPath(), "BepInEx", "plugins");
|
const BepinexPluginsPath = path.join(loadSilksongPath(), "BepInEx", "plugins");
|
||||||
@@ -739,6 +766,56 @@ function sortAndFilterModsList(list, keywords, offset, count, sortFilter, sortOr
|
|||||||
return { list: listSorted.slice(offset, offset + count), totalCount: listSorted.length };
|
return { list: listSorted.slice(offset, offset + count), totalCount: listSorted.length };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ipcMain.handle("add-offline-mod", async () => {
|
||||||
|
const { canceled, filePaths } = await dialog.showOpenDialog({
|
||||||
|
title: "Add mod",
|
||||||
|
properties: ["openFile"],
|
||||||
|
filters: [{ name: "mod", extensions: ["dll", "zip", "7z"] }],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (canceled || !filePaths) return false;
|
||||||
|
|
||||||
|
const fileName = path.basename(filePaths[0]);
|
||||||
|
const extension = path.extname(fileName).toLowerCase();
|
||||||
|
let uuid;
|
||||||
|
do {
|
||||||
|
uuid = randomUUID();
|
||||||
|
} while (installedModsStore.get(uuid));
|
||||||
|
|
||||||
|
if (extension === ".dll") {
|
||||||
|
await fs.mkdir(path.join(modSavePath, uuid));
|
||||||
|
await fs.copyFile(filePaths[0], path.join(modSavePath, uuid, fileName));
|
||||||
|
} else if ([".zip", ".7z"].includes(extension)) {
|
||||||
|
await extractArchive(filePaths[0], path.join(modSavePath, uuid));
|
||||||
|
} else {
|
||||||
|
mainWindow.webContents.send("showToast", `Unsupported file type: ${extension}`, "error");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
activateMod(uuid);
|
||||||
|
|
||||||
|
const time = new Date().toLocaleDateString();
|
||||||
|
const splitedTime = time.split("/");
|
||||||
|
let newTime = "";
|
||||||
|
for (let i = 2; i >= 0; i--) {
|
||||||
|
newTime = newTime.concat(splitedTime[i]);
|
||||||
|
if (i > 0) {
|
||||||
|
newTime = newTime.concat("-");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveModInfo(uuid, false, {
|
||||||
|
modId: uuid,
|
||||||
|
name: fileName.split(".").shift(),
|
||||||
|
summary: "Local mod",
|
||||||
|
updatedAt: newTime,
|
||||||
|
createdAt: newTime,
|
||||||
|
source: "local",
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
//////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////
|
||||||
//////////////////// UNCATEGORIZE ////////////////////
|
//////////////////// UNCATEGORIZE ////////////////////
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ contextBridge.exposeInMainWorld("mods", {
|
|||||||
getMods: (type) => ipcRenderer.invoke("get-mods", type),
|
getMods: (type) => ipcRenderer.invoke("get-mods", type),
|
||||||
activateMods: (modId) => ipcRenderer.invoke("activate-mod", modId),
|
activateMods: (modId) => ipcRenderer.invoke("activate-mod", modId),
|
||||||
deactivateMods: (modId) => ipcRenderer.invoke("deactivate-mod", modId),
|
deactivateMods: (modId) => ipcRenderer.invoke("deactivate-mod", modId),
|
||||||
|
add: () => ipcRenderer.invoke("add-offline-mod"),
|
||||||
});
|
});
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("thunderstore", {
|
contextBridge.exposeInMainWorld("thunderstore", {
|
||||||
|
|||||||
@@ -117,6 +117,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="default-button square-button" onclick="inverseSort()"><img class="icons" id="sort-order-image" src="assets/icons/sort-order-1.svg" /></button>
|
<button class="default-button square-button" onclick="inverseSort()"><img class="icons" id="sort-order-image" src="assets/icons/sort-order-1.svg" /></button>
|
||||||
|
<button class="default-button square-button" onclick="addOfflineMod()"><img class="icons" src="assets/icons/plus.svg" /></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mods-container" id="mods-container"></div>
|
<div class="mods-container" id="mods-container"></div>
|
||||||
<div class="separated-div">
|
<div class="separated-div">
|
||||||
@@ -202,15 +203,16 @@
|
|||||||
<div class="horizontal-div">
|
<div class="horizontal-div">
|
||||||
<h3 id="mod-title">Unknown Title</h3>
|
<h3 id="mod-title">Unknown Title</h3>
|
||||||
<p id="mod-author">Unknown author</p>
|
<p id="mod-author">Unknown author</p>
|
||||||
<p id="mod-endorsements-number">? likes</p>
|
<p id="mod-endorsements-number"></p>
|
||||||
<p id="mod-downloads-number">? download</p>
|
<p id="mod-downloads-number"></p>
|
||||||
|
<p id="mod-source"></p>
|
||||||
</div>
|
</div>
|
||||||
<p id="mod-description" class="long-text">No description provided</p>
|
<p id="mod-description" class="long-text">No description provided</p>
|
||||||
<p class="transparent-text" id="mod-version">V1.0.0 last update on 01/01/2026</p>
|
<p class="transparent-text" id="mod-version"></p>
|
||||||
<br />
|
<br />
|
||||||
<div class="horizontal-div">
|
<div class="horizontal-div">
|
||||||
<a href="www.nexusmods.com/hollowknightsilksong/mods" class="default-button" id="uninstall-mod-button">Uninstall</a>
|
<a href="" class="default-button" id="uninstall-mod-button">Uninstall</a>
|
||||||
<a href="www.nexusmods.com/hollowknightsilksong/mods" class="default-button" id="external-link">Website</a>
|
<a href="" class="default-button" id="external-link">Website</a>
|
||||||
<p>Activated:</p>
|
<p>Activated:</p>
|
||||||
<label class="checkbox-container">
|
<label class="checkbox-container">
|
||||||
<input type="checkbox" name="activated-mod" id="activated-mod" />
|
<input type="checkbox" name="activated-mod" id="activated-mod" />
|
||||||
|
|||||||
@@ -152,9 +152,16 @@ async function navigate(page) {
|
|||||||
const modPicture = installedModTemplateCopy.getElementById("mod-icon");
|
const modPicture = installedModTemplateCopy.getElementById("mod-icon");
|
||||||
modPicture.src = modInfo.pictureUrl;
|
modPicture.src = modInfo.pictureUrl;
|
||||||
}
|
}
|
||||||
if (modInfo.version && modInfo.updatedAt) {
|
if (modInfo.version || modInfo.updatedAt) {
|
||||||
|
let text = "";
|
||||||
|
if (modInfo.version) {
|
||||||
|
text = text.concat(`V${modInfo.version} `);
|
||||||
|
}
|
||||||
|
if (modInfo.updatedAt) {
|
||||||
|
text = text.concat(`last updated on ${modInfo.updatedAt.slice(0, 10)}`);
|
||||||
|
}
|
||||||
const modVersionText = installedModTemplateCopy.getElementById("mod-version");
|
const modVersionText = installedModTemplateCopy.getElementById("mod-version");
|
||||||
modVersionText.innerText = `V${modInfo.version} last updated on ${modInfo.updatedAt.slice(0, 10)}`;
|
modVersionText.innerText = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isActivatedCheckbox = installedModTemplateCopy.getElementById("activated-mod");
|
const isActivatedCheckbox = installedModTemplateCopy.getElementById("activated-mod");
|
||||||
@@ -172,15 +179,26 @@ async function navigate(page) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const modUrl = `https://www.nexusmods.com/hollowknightsilksong/mods/${modInfo.modId}`;
|
if (modInfo.source && modInfo.source != "local") {
|
||||||
|
const modSource = installedModTemplateCopy.getElementById("mod-source");
|
||||||
|
modSource.innerText = `From ${modInfo.source}`;
|
||||||
|
|
||||||
|
const modUrls = {
|
||||||
|
nexusmods: `https://www.nexusmods.com/hollowknightsilksong/mods/${modInfo.modId}`,
|
||||||
|
thunderstore: `https://new.thunderstore.io/c/hollow-knight-silksong/p/${modInfo.author}/${modInfo.name}`,
|
||||||
|
};
|
||||||
|
|
||||||
const modLinkButton = installedModTemplateCopy.getElementById("external-link");
|
const modLinkButton = installedModTemplateCopy.getElementById("external-link");
|
||||||
modLinkButton.href = modUrl;
|
modLinkButton.href = modUrls[modInfo.source];
|
||||||
modLinkButton.addEventListener("click", function (event) {
|
modLinkButton.addEventListener("click", function (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const modLink = modLinkButton.href;
|
const modLink = modLinkButton.href;
|
||||||
electronAPI.openExternalLink(modLink);
|
electronAPI.openExternalLink(modLink);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
const modLinkButton = installedModTemplateCopy.getElementById("external-link");
|
||||||
|
modLinkButton.remove();
|
||||||
|
}
|
||||||
|
|
||||||
const uninstallModButton = installedModTemplateCopy.getElementById("uninstall-mod-button");
|
const uninstallModButton = installedModTemplateCopy.getElementById("uninstall-mod-button");
|
||||||
uninstallModButton.addEventListener("click", async function (event) {
|
uninstallModButton.addEventListener("click", async function (event) {
|
||||||
@@ -555,6 +573,11 @@ async function searchThunderstoreMods() {
|
|||||||
searchInput.value = searchValueThunderstore;
|
searchInput.value = searchValueThunderstore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addOfflineMod() {
|
||||||
|
await mods.add();
|
||||||
|
searchInstalledMods();
|
||||||
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////
|
||||||
//////////////// THEMES / SORT / LIST ////////////////
|
//////////////// THEMES / SORT / LIST ////////////////
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user