mirror of
https://github.com/Gabi-Zar/Silk-Fly-Launcher.git
synced 2026-04-17 05:26:04 +02:00
Add mod search via Nexus GraphQL API and user toast feedback
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -138,4 +138,7 @@ dist
|
|||||||
# Vite files
|
# Vite files
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
.vite/
|
.vite/
|
||||||
|
|
||||||
|
#prettier
|
||||||
|
.prettierignore
|
||||||
@@ -726,4 +726,55 @@ Program, unless a warranty or assumption of liability accompanies a
|
|||||||
copy of the Program in return for a fee.
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
END OF TERMS AND CONDITIONS
|
||||||
=================================
|
=================================
|
||||||
|
=================================
|
||||||
|
|
||||||
|
GRAPHQL-REQUEST
|
||||||
|
=================================
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Jason Kuhrt
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
=================================
|
||||||
|
=================================
|
||||||
|
|
||||||
|
GRAPHQL
|
||||||
|
=================================
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) GraphQL Contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|||||||
88
main.js
88
main.js
@@ -7,7 +7,7 @@ import { createWriteStream } from "fs";
|
|||||||
import { pipeline } from "stream/promises";
|
import { pipeline } from "stream/promises";
|
||||||
import extract from "extract-zip";
|
import extract from "extract-zip";
|
||||||
import NexusModule from "@nexusmods/nexus-api";
|
import NexusModule from "@nexusmods/nexus-api";
|
||||||
const Nexus = NexusModule.default;
|
import { gql, GraphQLClient } from "graphql-request";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
@@ -23,9 +23,12 @@ 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");
|
||||||
|
|
||||||
|
const Nexus = NexusModule.default;
|
||||||
let nexusAPI = store.get("nexus-api");
|
let nexusAPI = store.get("nexus-api");
|
||||||
let nexus = undefined;
|
let nexus = undefined;
|
||||||
createNexus();
|
createNexus();
|
||||||
|
let cachedModList = undefined;
|
||||||
|
let query = "";
|
||||||
|
|
||||||
let bepinexFolderPath = `${silksongPath}/BepInEx`;
|
let bepinexFolderPath = `${silksongPath}/BepInEx`;
|
||||||
let bepinexBackupPath = `${silksongPath}/BepInEx-Backup`;
|
let bepinexBackupPath = `${silksongPath}/BepInEx-Backup`;
|
||||||
@@ -154,12 +157,16 @@ ipcMain.handle("save-nexus-api", (event, api) => {
|
|||||||
store.set("nexus-api", nexusAPI);
|
store.set("nexus-api", nexusAPI);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("load-nexus-api", () => {
|
function loadNexusApi() {
|
||||||
nexusAPI = store.get("nexus-api");
|
nexusAPI = store.get("nexus-api");
|
||||||
if (nexusAPI == undefined) {
|
if (nexusAPI == undefined) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return nexusAPI;
|
return nexusAPI;
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle("load-nexus-api", () => {
|
||||||
|
return loadNexusApi();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("save-theme", (event, theme, lacePinState) => {
|
ipcMain.handle("save-theme", (event, theme, lacePinState) => {
|
||||||
@@ -197,6 +204,10 @@ ipcMain.handle("load-installed-mods-info", () => {
|
|||||||
for (const [key, modInfo] of Object.entries(installedModsStore.store)) {
|
for (const [key, modInfo] of Object.entries(installedModsStore.store)) {
|
||||||
modsInfo.push(modInfo);
|
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;
|
return modsInfo;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -386,17 +397,21 @@ async function verifyNexusAPI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.handle("get-latest-mods", async () => {
|
ipcMain.handle("get-mods", async () => {
|
||||||
if (!(await verifyNexusAPI())) {
|
if (!cachedModList) {
|
||||||
return;
|
if (!(await verifyNexusAPI())) {
|
||||||
|
mainWindow.webContents.send("showToast", "Unable to fetch mods.", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cachedModList = await nexus.getLatestAdded();
|
||||||
}
|
}
|
||||||
|
|
||||||
const mods = await nexus.getLatestAdded();
|
return cachedModList;
|
||||||
return mods;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("open-download", async (event, link) => {
|
ipcMain.handle("open-download", async (event, link) => {
|
||||||
if (!(await verifyNexusAPI())) {
|
if (!(await verifyNexusAPI())) {
|
||||||
|
mainWindow.webContents.send("showToast", "Unable to download.", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,6 +444,7 @@ function handleNxmUrl(url) {
|
|||||||
|
|
||||||
async function startDownload(modId, fileId, key, expires) {
|
async function startDownload(modId, fileId, key, expires) {
|
||||||
if (!(await verifyNexusAPI())) {
|
if (!(await verifyNexusAPI())) {
|
||||||
|
mainWindow.webContents.send("showToast", "Unable to download.", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,6 +461,7 @@ async function startDownload(modId, fileId, key, expires) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveModInfo(modId);
|
saveModInfo(modId);
|
||||||
|
mainWindow.webContents.send("showToast", "Mod downloaded successfully.");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkInstalledMods() {
|
async function checkInstalledMods() {
|
||||||
@@ -468,6 +485,60 @@ ipcMain.handle("uninstall-mod", async (event, modId) => {
|
|||||||
saveModInfo(modId, true);
|
saveModInfo(modId, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("search-nexus-mods", async (event, keywords) => {
|
||||||
|
const count = 10;
|
||||||
|
const endpoint = "https://api.nexusmods.com/v2/graphql";
|
||||||
|
const client = new GraphQLClient(endpoint, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const query = gql`
|
||||||
|
query Mods($filter: ModsFilter, $offset: Int, $count: Int) {
|
||||||
|
mods(filter: $filter, offset: $offset, count: $count) {
|
||||||
|
nodes {
|
||||||
|
author
|
||||||
|
endorsements
|
||||||
|
modId
|
||||||
|
name
|
||||||
|
pictureUrl
|
||||||
|
summary
|
||||||
|
updatedAt
|
||||||
|
version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
filter: {
|
||||||
|
op: "AND",
|
||||||
|
gameDomainName: [{ value: "hollowknightsilksong" }],
|
||||||
|
name: [{ value: keywords, op: "WILDCARD" }],
|
||||||
|
},
|
||||||
|
offset: 0,
|
||||||
|
count: count,
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
cachedModList = data.mods.nodes;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("search-installed-mods", async (event, keywords) => {
|
||||||
|
query = keywords;
|
||||||
|
});
|
||||||
|
|
||||||
//////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////
|
||||||
//////////////////// UNCATEGORIZE ////////////////////
|
//////////////////// UNCATEGORIZE ////////////////////
|
||||||
|
|
||||||
@@ -536,7 +607,8 @@ ipcMain.handle("launch-game", async (event, mode) => {
|
|||||||
async function downloadAndUnzip(url, path) {
|
async function downloadAndUnzip(url, path) {
|
||||||
const download = await fetch(url);
|
const download = await fetch(url);
|
||||||
if (!download.ok) {
|
if (!download.ok) {
|
||||||
throw new Error("Download error");
|
mainWindow.webContents.send("showToast", "Error during download.", "error");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tempPath = `${userSavePath}\\tempZip.zip`;
|
const tempPath = `${userSavePath}\\tempZip.zip`;
|
||||||
|
|||||||
3490
package-lock.json
generated
3490
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nexusmods/nexus-api": "^1.1.5",
|
"@nexusmods/nexus-api": "^1.1.5",
|
||||||
"electron-store": "^11.0.2",
|
"electron-store": "^11.0.2",
|
||||||
"extract-zip": "^2.0.1"
|
"extract-zip": "^2.0.1",
|
||||||
|
"graphql": "^16.12.0",
|
||||||
|
"graphql-request": "^7.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||||||
launchGame: (mode) => ipcRenderer.invoke("launch-game", mode),
|
launchGame: (mode) => ipcRenderer.invoke("launch-game", mode),
|
||||||
loadMainPage: () => ipcRenderer.invoke("load-main-page"),
|
loadMainPage: () => ipcRenderer.invoke("load-main-page"),
|
||||||
getPage: () => ipcRenderer.invoke("get-page"),
|
getPage: () => ipcRenderer.invoke("get-page"),
|
||||||
|
onShowToast: (callback) => {
|
||||||
|
ipcRenderer.on("showToast", (event, message, type, duration) => {
|
||||||
|
callback(message, type, duration);
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("bepinex", {
|
contextBridge.exposeInMainWorld("bepinex", {
|
||||||
@@ -43,7 +48,9 @@ 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"),
|
getMods: () => ipcRenderer.invoke("get-mods"),
|
||||||
download: (link) => ipcRenderer.invoke("open-download", link),
|
download: (link) => ipcRenderer.invoke("open-download", link),
|
||||||
uninstall: (modId) => ipcRenderer.invoke("uninstall-mod", modId),
|
uninstall: (modId) => ipcRenderer.invoke("uninstall-mod", modId),
|
||||||
|
search: (keywords) => ipcRenderer.invoke("search-nexus-mods", keywords),
|
||||||
|
searchInstalled: (keywords) => ipcRenderer.invoke("search-installed-mods", keywords),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
<main class="content">
|
<main class="content">
|
||||||
<h1 id="title">Silk Fly Launcher</h1>
|
<h1 id="title">Silk Fly Launcher</h1>
|
||||||
<div class="view" id="view"></div>
|
<div class="view" id="view"></div>
|
||||||
|
<div class="toast-div" id="toast-div"></div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ async function navigate(page) {
|
|||||||
|
|
||||||
view.appendChild(onlineModsTemplateCopy);
|
view.appendChild(onlineModsTemplateCopy);
|
||||||
|
|
||||||
mods = await nexus.getLatestMods();
|
mods = await nexus.getMods();
|
||||||
if (mods == undefined) {
|
if (mods == undefined) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -172,7 +172,7 @@ async function navigate(page) {
|
|||||||
const modPicture = modTemplateCopy.getElementById("mod-icon");
|
const modPicture = modTemplateCopy.getElementById("mod-icon");
|
||||||
modPicture.src = mod.picture_url;
|
modPicture.src = mod.picture_url;
|
||||||
}
|
}
|
||||||
if (mod.version && mod.updated_timestamp) {
|
if (mod.version && mod.updated_time) {
|
||||||
const modVersionText = modTemplateCopy.getElementById("mod-version");
|
const modVersionText = modTemplateCopy.getElementById("mod-version");
|
||||||
modVersionText.innerText = `V${mod.version} last updated on ${mod.updated_time.slice(0, 10)}`;
|
modVersionText.innerText = `V${mod.version} last updated on ${mod.updated_time.slice(0, 10)}`;
|
||||||
}
|
}
|
||||||
@@ -397,7 +397,8 @@ async function setBepinexVersion() {
|
|||||||
|
|
||||||
async function searchInstalledMods() {
|
async function searchInstalledMods() {
|
||||||
const searchInput = document.getElementById("search-input");
|
const searchInput = document.getElementById("search-input");
|
||||||
console.log(searchInput.value);
|
await nexus.searchInstalled(searchInput.value);
|
||||||
|
navigate("refresh");
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////
|
||||||
@@ -420,7 +421,8 @@ async function verifyNexusAPI() {
|
|||||||
|
|
||||||
async function searchNexusMods() {
|
async function searchNexusMods() {
|
||||||
const searchInput = document.getElementById("search-input");
|
const searchInput = document.getElementById("search-input");
|
||||||
console.log(searchInput.value);
|
await nexus.search(searchInput.value);
|
||||||
|
navigate("refresh");
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////
|
||||||
@@ -501,3 +503,20 @@ async function autoDetectGamePath() {
|
|||||||
document.getElementById("silksong-path-input").value = await files.loadSilksongPath();
|
document.getElementById("silksong-path-input").value = await files.loadSilksongPath();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showToast(message, type = "info", duration = 3000) {
|
||||||
|
const toastDiv = document.getElementById("toast-div");
|
||||||
|
const toast = document.createElement("p");
|
||||||
|
|
||||||
|
toast.classList.add("toast", type);
|
||||||
|
toast.innerText = message;
|
||||||
|
toastDiv.appendChild(toast);
|
||||||
|
toast.classList.add("show");
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove("show");
|
||||||
|
toast.addEventListener("transitionend", () => toast.remove());
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
electronAPI.onShowToast(showToast);
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mods-container {
|
.mods-container {
|
||||||
height: 80%;
|
height: 70%;
|
||||||
transform: translateY(50px);
|
transform: translateY(50px);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -382,3 +382,40 @@ body {
|
|||||||
border-width: 0 2px 2px 0;
|
border-width: 0 2px 2px 0;
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toast-div {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
width: 300px;
|
||||||
|
background: var(--transparent-black);
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: 1px solid var(--secondary-color);
|
||||||
|
border-radius: 32px;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.show {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.error {
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
.toast.info {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
.toast.warning {
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user