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.config.js.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.
|
||||
|
||||
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 extract from "extract-zip";
|
||||
import NexusModule from "@nexusmods/nexus-api";
|
||||
const Nexus = NexusModule.default;
|
||||
import { gql, GraphQLClient } from "graphql-request";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -23,9 +23,12 @@ const modSavePath = `${userSavePath}\\mods`;
|
||||
const dataPath = `${userSavePath}\\config.json`;
|
||||
let silksongPath = store.get("silksong-path");
|
||||
|
||||
const Nexus = NexusModule.default;
|
||||
let nexusAPI = store.get("nexus-api");
|
||||
let nexus = undefined;
|
||||
createNexus();
|
||||
let cachedModList = undefined;
|
||||
let query = "";
|
||||
|
||||
let bepinexFolderPath = `${silksongPath}/BepInEx`;
|
||||
let bepinexBackupPath = `${silksongPath}/BepInEx-Backup`;
|
||||
@@ -154,12 +157,16 @@ ipcMain.handle("save-nexus-api", (event, api) => {
|
||||
store.set("nexus-api", nexusAPI);
|
||||
});
|
||||
|
||||
ipcMain.handle("load-nexus-api", () => {
|
||||
function loadNexusApi() {
|
||||
nexusAPI = store.get("nexus-api");
|
||||
if (nexusAPI == undefined) {
|
||||
return "";
|
||||
}
|
||||
return nexusAPI;
|
||||
}
|
||||
|
||||
ipcMain.handle("load-nexus-api", () => {
|
||||
return loadNexusApi();
|
||||
});
|
||||
|
||||
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)) {
|
||||
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;
|
||||
});
|
||||
|
||||
@@ -386,17 +397,21 @@ async function verifyNexusAPI() {
|
||||
}
|
||||
}
|
||||
|
||||
ipcMain.handle("get-latest-mods", async () => {
|
||||
if (!(await verifyNexusAPI())) {
|
||||
return;
|
||||
ipcMain.handle("get-mods", async () => {
|
||||
if (!cachedModList) {
|
||||
if (!(await verifyNexusAPI())) {
|
||||
mainWindow.webContents.send("showToast", "Unable to fetch mods.", "error");
|
||||
return;
|
||||
}
|
||||
cachedModList = await nexus.getLatestAdded();
|
||||
}
|
||||
|
||||
const mods = await nexus.getLatestAdded();
|
||||
return mods;
|
||||
return cachedModList;
|
||||
});
|
||||
|
||||
ipcMain.handle("open-download", async (event, link) => {
|
||||
if (!(await verifyNexusAPI())) {
|
||||
mainWindow.webContents.send("showToast", "Unable to download.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -429,6 +444,7 @@ function handleNxmUrl(url) {
|
||||
|
||||
async function startDownload(modId, fileId, key, expires) {
|
||||
if (!(await verifyNexusAPI())) {
|
||||
mainWindow.webContents.send("showToast", "Unable to download.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -445,6 +461,7 @@ async function startDownload(modId, fileId, key, expires) {
|
||||
}
|
||||
|
||||
saveModInfo(modId);
|
||||
mainWindow.webContents.send("showToast", "Mod downloaded successfully.");
|
||||
}
|
||||
|
||||
async function checkInstalledMods() {
|
||||
@@ -468,6 +485,60 @@ ipcMain.handle("uninstall-mod", async (event, modId) => {
|
||||
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 ////////////////////
|
||||
|
||||
@@ -536,7 +607,8 @@ 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");
|
||||
mainWindow.webContents.send("showToast", "Error during download.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
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": {
|
||||
"@nexusmods/nexus-api": "^1.1.5",
|
||||
"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),
|
||||
loadMainPage: () => ipcRenderer.invoke("load-main-page"),
|
||||
getPage: () => ipcRenderer.invoke("get-page"),
|
||||
onShowToast: (callback) => {
|
||||
ipcRenderer.on("showToast", (event, message, type, duration) => {
|
||||
callback(message, type, duration);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld("bepinex", {
|
||||
@@ -43,7 +48,9 @@ contextBridge.exposeInMainWorld("bepinex", {
|
||||
|
||||
contextBridge.exposeInMainWorld("nexus", {
|
||||
verifyAPI: () => ipcRenderer.invoke("verify-nexus-api"),
|
||||
getLatestMods: () => ipcRenderer.invoke("get-latest-mods"),
|
||||
getMods: () => ipcRenderer.invoke("get-mods"),
|
||||
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),
|
||||
});
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
<main class="content">
|
||||
<h1 id="title">Silk Fly Launcher</h1>
|
||||
<div class="view" id="view"></div>
|
||||
<div class="toast-div" id="toast-div"></div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ async function navigate(page) {
|
||||
|
||||
view.appendChild(onlineModsTemplateCopy);
|
||||
|
||||
mods = await nexus.getLatestMods();
|
||||
mods = await nexus.getMods();
|
||||
if (mods == undefined) {
|
||||
break;
|
||||
}
|
||||
@@ -172,7 +172,7 @@ async function navigate(page) {
|
||||
const modPicture = modTemplateCopy.getElementById("mod-icon");
|
||||
modPicture.src = mod.picture_url;
|
||||
}
|
||||
if (mod.version && mod.updated_timestamp) {
|
||||
if (mod.version && mod.updated_time) {
|
||||
const modVersionText = modTemplateCopy.getElementById("mod-version");
|
||||
modVersionText.innerText = `V${mod.version} last updated on ${mod.updated_time.slice(0, 10)}`;
|
||||
}
|
||||
@@ -397,7 +397,8 @@ async function setBepinexVersion() {
|
||||
|
||||
async function searchInstalledMods() {
|
||||
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() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
height: 80%;
|
||||
height: 70%;
|
||||
transform: translateY(50px);
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
@@ -382,3 +382,40 @@ body {
|
||||
border-width: 0 2px 2px 0;
|
||||
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