From 729d51044d6a072bdbaae84b399c2685a9d3a41e Mon Sep 17 00:00:00 2001 From: GabiZar Date: Sat, 21 Feb 2026 13:28:29 +0100 Subject: [PATCH] Add sorting for Nexus Mods search results and installed mods. Update list menu to highlight the selected item. --- main.js | 140 ++++++++++++--------- preload.js | 11 +- renderer/assets/icons/sort-order-1.svg | 11 ++ renderer/assets/icons/sort-order-2.svg | 11 ++ renderer/index.html | 75 ++++++++---- renderer/renderer.js | 162 +++++++++++++++++++++---- renderer/style.css | 37 +++++- renderer/welcome.html | 20 +-- 8 files changed, 346 insertions(+), 121 deletions(-) create mode 100644 renderer/assets/icons/sort-order-1.svg create mode 100644 renderer/assets/icons/sort-order-2.svg diff --git a/main.js b/main.js index 31ba118..f44b1e0 100644 --- a/main.js +++ b/main.js @@ -13,6 +13,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const gotTheLock = app.requestSingleInstanceLock(); const isDev = !app.isPackaged; +const VERSION = "1.0.0"; const store = new Store(); const bepinexStore = new Store({ cwd: "bepinex-version" }); @@ -27,8 +28,8 @@ const Nexus = NexusModule.default; let nexusAPI = store.get("nexus-api"); let nexus = undefined; createNexus(); -let cachedModList = undefined; -let query = ""; +let installedCachedModList = undefined; +let onlineCachedModList = undefined; let bepinexFolderPath = `${silksongPath}/BepInEx`; let bepinexBackupPath = `${silksongPath}/BepInEx-Backup`; @@ -188,29 +189,10 @@ async function saveModInfo(modId, suppr = false) { 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); + const modInfo = onlineCachedModList.find((mod) => mod.modId == modId); + installedModsStore.set(String(modId), modInfo); } -ipcMain.handle("load-installed-mods-info", () => { - let modsInfo = []; - for (const [key, modInfo] of Object.entries(installedModsStore.store)) { - modsInfo.push(modInfo); - } - - modsInfo.sort((a, b) => a.name.localeCompare(b.name)); - modsInfo = modsInfo.filter((mod) => mod.name.toLowerCase().includes(query.toLowerCase())).sort((a, b) => a.name.localeCompare(b.name)); - - return modsInfo; -}); - ////////////////////////////////////////////////////// /////////////////// DATA HANDLING //////////////////// @@ -286,7 +268,7 @@ async function installBepinex() { const res = await fetch(GITHUB_URL, { headers: { - "User-Agent": "SilkFlyLauncher/1.0.0", + "User-Agent": `SilkFlyLauncher/${VERSION}`, Accept: "application/vnd.github+json", }, }); @@ -377,7 +359,7 @@ async function createNexus() { } try { - nexus = await Nexus.create(nexusAPI, "silk-fly-launcher", "1.0.0", "hollowknightsilksong"); + nexus = await Nexus.create(nexusAPI, "silk-fly-launcher", VERSION, "hollowknightsilksong"); } catch (error) { console.log(error); nexus = undefined; @@ -397,16 +379,18 @@ async function verifyNexusAPI() { } } -ipcMain.handle("get-mods", async () => { - if (!cachedModList) { - if (!(await verifyNexusAPI())) { - mainWindow.webContents.send("showToast", "Unable to fetch mods.", "error"); - return; +ipcMain.handle("get-mods", async (event, type) => { + if (type == "mods-installed") { + if (!installedCachedModList) { + await searchInstalledMods(""); } - cachedModList = await nexus.getLatestAdded(); + return installedCachedModList; + } else if (type == "mods-online") { + if (!onlineCachedModList) { + await searchNexusMods(""); + } + return onlineCachedModList; } - - return cachedModList; }); ipcMain.handle("open-download", async (event, link) => { @@ -462,13 +446,14 @@ async function startDownload(modId, fileId, key, expires) { saveModInfo(modId); mainWindow.webContents.send("showToast", "Mod downloaded successfully."); + installedCachedModList = undefined; } async function checkInstalledMods() { for (const [key, modInfo] of Object.entries(installedModsStore.store)) { - if (!(await fileExists(`${modSavePath}/${modInfo.mod_id}`))) { + if (!(await fileExists(`${modSavePath}/${modInfo.modId}`))) { saveModInfo(key, true); - await fs.rm(`${bepinexFolderPath}/plugins/${modInfo.mod_id}`, { recursive: true }); + await fs.rm(`${bepinexFolderPath}/plugins/${modInfo.modId}`, { recursive: true }); } } } @@ -482,21 +467,36 @@ ipcMain.handle("uninstall-mod", async (event, modId) => { await fs.rm(modPath, { recursive: true }); } + for (let i = 0; i < installedCachedModList.length; i++) { + if (installedCachedModList[i].modId == modId) { + installedCachedModList.splice(i, 1); + } + } + saveModInfo(modId, true); }); -ipcMain.handle("search-nexus-mods", async (event, keywords) => { - const count = 10; +ipcMain.handle("search-nexus-mods", async (event, keywords, offset, count, sortFilter, sortOrder) => { + await searchNexusMods(keywords, offset, count, sortFilter, sortOrder); +}); + +async function searchNexusMods(keywords, offset = 0, count = 10, sortFilter = "downloads", sortOrder = "DESC") { + if (keywords.length == 1) { + mainWindow.webContents.send("showToast", "Your query must contain at least 2 characters.", "warning"); + return; + } + const endpoint = "https://api.nexusmods.com/v2/graphql"; const client = new GraphQLClient(endpoint, { headers: { + "User-Agent": `SilkFlyLauncher/${VERSION}`, "Content-Type": "application/json", }, }); const query = gql` - query Mods($filter: ModsFilter, $offset: Int, $count: Int) { - mods(filter: $filter, offset: $offset, count: $count) { + query Mods($filter: ModsFilter, $offset: Int, $count: Int, $sort: [ModsSort!]) { + mods(filter: $filter, offset: $offset, count: $count, sort: $sort) { nodes { author endorsements @@ -505,39 +505,67 @@ ipcMain.handle("search-nexus-mods", async (event, keywords) => { pictureUrl summary updatedAt + createdAt version + downloads + fileSize } + totalCount } } `; - const variables = { + let variables = { filter: { op: "AND", gameDomainName: [{ value: "hollowknightsilksong" }], name: [{ value: keywords, op: "WILDCARD" }], }, - offset: 0, + offset: offset, count: count, + sort: [{ [sortFilter]: { direction: sortOrder } }], }; + if (!keywords) { + delete variables.filter.name; + } const data = await client.request(query, variables); - for (let i = 0; i < data.mods.nodes.length; i++) { - data.mods.nodes[i].mod_id = data.mods.nodes[i].modId; - delete data.mods.nodes[i].modId; - data.mods.nodes[i].picture_url = data.mods.nodes[i].pictureUrl; - delete data.mods.nodes[i].pictureUrl; - data.mods.nodes[i].endorsement_count = data.mods.nodes[i].endorsements; - delete data.mods.nodes[i].endorsements; - data.mods.nodes[i].updated_time = data.mods.nodes[i].updatedAt; - delete data.mods.nodes[i].updatedAt; + onlineCachedModList = data.mods.nodes; + + for (let i = 0; i < onlineCachedModList.length; i++) { + if (onlineCachedModList[i].modId == 26) { + onlineCachedModList.splice(i, 1); + } } - cachedModList = data.mods.nodes; + + return data.mods.totalCount; +} + +ipcMain.handle("search-installed-mods", async (event, keywords, offset, count, sortFilter, sortOrder) => { + await searchInstalledMods(keywords, offset, count, sortFilter, sortOrder); }); -ipcMain.handle("search-installed-mods", async (event, keywords) => { - query = keywords; -}); +async function searchInstalledMods(keywords, offset = 0, count = 10, sortFilter = "name", sortOrder = "ASC") { + let modsInfo = []; + for (const [key, modInfo] of Object.entries(installedModsStore.store)) { + modsInfo.push(modInfo); + } + + const modsInfoFiltered = modsInfo.filter((mod) => mod.name.toLowerCase().includes(keywords.toLowerCase())); + const sortFactor = sortOrder == "ASC" ? 1 : -1; + + let modsInfoSorted; + if (sortFilter == "name" || sortFilter == "createdAt" || sortFilter == "updatedAt") { + modsInfoSorted = modsInfoFiltered.sort((a, b) => sortFactor * a[sortFilter].localeCompare(b[sortFilter])); + } else if (sortFilter == "downloads" || sortFilter == "endorsements" || sortFilter == "size") { + if (sortFilter == "size") { + sortFilter = "fileSize"; + } + modsInfoSorted = modsInfoFiltered.sort((a, b) => sortFactor * (a[sortFilter] - b[sortFilter])); + } + + installedCachedModList = modsInfoSorted; +} ////////////////////////////////////////////////////// //////////////////// UNCATEGORIZE //////////////////// @@ -618,3 +646,7 @@ async function downloadAndUnzip(url, path) { await extract(tempPath, { dir: path }); await fs.unlink(tempPath); } + +ipcMain.handle("get-version", () => { + return VERSION; +}); diff --git a/preload.js b/preload.js index 86a813f..c33f689 100644 --- a/preload.js +++ b/preload.js @@ -1,9 +1,7 @@ const { contextBridge, ipcRenderer } = require("electron"); -const VERSION = "1.0.0"; - contextBridge.exposeInMainWorld("versions", { - silkFlyLauncher: () => VERSION, + silkFlyLauncher: () => ipcRenderer.invoke("get-version"), node: () => process.versions.node, chromium: () => process.versions.chrome, electron: () => process.versions.electron, @@ -23,7 +21,6 @@ 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", { @@ -48,9 +45,9 @@ contextBridge.exposeInMainWorld("bepinex", { contextBridge.exposeInMainWorld("nexus", { verifyAPI: () => ipcRenderer.invoke("verify-nexus-api"), - getMods: () => ipcRenderer.invoke("get-mods"), + getMods: (type) => ipcRenderer.invoke("get-mods", type), download: (link) => ipcRenderer.invoke("open-download", link), uninstall: (modId) => ipcRenderer.invoke("uninstall-mod", modId), - search: (keywords) => ipcRenderer.invoke("search-nexus-mods", keywords), - searchInstalled: (keywords) => ipcRenderer.invoke("search-installed-mods", keywords), + search: (keywords, offset, count, sortFilter, sortOrder) => ipcRenderer.invoke("search-nexus-mods", keywords, offset, count, sortFilter, sortOrder), + searchInstalled: (keywords, offset, count, sortFilter, sortOrder) => ipcRenderer.invoke("search-installed-mods", keywords, offset, count, sortFilter, sortOrder), }); diff --git a/renderer/assets/icons/sort-order-1.svg b/renderer/assets/icons/sort-order-1.svg new file mode 100644 index 0000000..fea1965 --- /dev/null +++ b/renderer/assets/icons/sort-order-1.svg @@ -0,0 +1,11 @@ + diff --git a/renderer/assets/icons/sort-order-2.svg b/renderer/assets/icons/sort-order-2.svg new file mode 100644 index 0000000..14fce7e --- /dev/null +++ b/renderer/assets/icons/sort-order-2.svg @@ -0,0 +1,11 @@ + diff --git a/renderer/index.html b/renderer/index.html index 32cedf6..987b708 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -91,19 +91,47 @@ @@ -113,10 +141,12 @@

Unknown Title

Unknown author

+

? likes

+

? download

-

No description provided

+

No description provided

V1.0.0 last update on 01/01/2026

- +
Uninstall Website @@ -134,10 +164,11 @@

Unknown Title

Unknown author

? likes

+

? download

-

No description provided

+

No description provided

V1.0.0 last update on 01/01/2026

- +
Download Website @@ -155,18 +186,19 @@
+
-
+
Silksong
-
-
  • Silksong
  • -
  • Citadel of song
  • -
  • Cradle
  • -
  • Abyss
  • -
  • Greyroot
  • -
  • Surface
  • -
  • Steel
  • +
    +
  • Silksong
  • +
  • Citadel of song
  • +
  • Cradle
  • +
  • Abyss
  • +
  • Greyroot
  • +
  • Surface
  • +
  • Steel