Add welcome page and clean a bit

This commit is contained in:
2026-02-09 21:56:34 +01:00
parent f68fd0ac3a
commit 585e50f123
8 changed files with 276 additions and 44 deletions

53
main.js
View File

@@ -9,6 +9,7 @@ const Nexus = require('@nexusmods/nexus-api').default;
const store = new Store(); const store = new Store();
const userSavePath = app.getPath('userData') const userSavePath = app.getPath('userData')
const dataPath = `${userSavePath}\\config.json`
let silksongPath = store.get('silksong-path') let silksongPath = store.get('silksong-path')
let nexusAPI = store.get('nexus-api') let nexusAPI = store.get('nexus-api')
let nexus = undefined let nexus = undefined
@@ -29,7 +30,7 @@ let bepinexBackupVersion
let mainWindow let mainWindow
const createWindow = () => { async function createWindow() {
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: 1280, width: 1280,
height: 720, height: 720,
@@ -38,7 +39,14 @@ const createWindow = () => {
} }
}) })
mainWindow.loadFile('renderer/index.html') if(await fileExists(dataPath)) {
htmlFile = "renderer/index.html"
}
else {
htmlFile = "renderer/welcome.html"
}
mainWindow.loadFile(htmlFile)
} }
app.whenReady().then(() => { app.whenReady().then(() => {
@@ -58,11 +66,15 @@ app.on('window-all-closed', () => {
}) })
ipcMain.handle('save-path', (event, path) => { ipcMain.handle('save-path', (event, path) => {
saveSilksongPath(path)
});
function saveSilksongPath(path) {
silksongPath = path; silksongPath = path;
bepinexFolderPath = `${silksongPath}/BepInEx` bepinexFolderPath = `${silksongPath}/BepInEx`
bepinexBackupPath = `${silksongPath}/BepInEx-Backup` bepinexBackupPath = `${silksongPath}/BepInEx-Backup`
store.set('silksong-path', silksongPath); store.set('silksong-path', silksongPath);
}); }
ipcMain.handle('load-path', () => { ipcMain.handle('load-path', () => {
silksongPath = store.get('silksong-path'); silksongPath = store.get('silksong-path');
@@ -127,17 +139,11 @@ ipcMain.handle('file-exists', async (_, filePath) => {
return await fileExists(filePath); return await fileExists(filePath);
}); });
ipcMain.handle('get-userSavePath', () => { ipcMain.handle('delete-data', async () => {
return userSavePath await fs.unlink(dataPath)
});
ipcMain.handle('delete-data', async (event, path) => {
await fs.unlink(path)
}); });
ipcMain.handle('export-data', async () => { ipcMain.handle('export-data', async () => {
const dataPath = `${userSavePath}\\config.json`
const { canceled, filePath } = await dialog.showSaveDialog({ const { canceled, filePath } = await dialog.showSaveDialog({
title: 'Export Data', title: 'Export Data',
defaultPath: 'config.json', defaultPath: 'config.json',
@@ -152,20 +158,19 @@ ipcMain.handle('export-data', async () => {
}) })
ipcMain.handle('import-data', async () => { ipcMain.handle('import-data', async () => {
const dataPath = `${userSavePath}\\config.json`
const { canceled, filePaths } = await dialog.showOpenDialog({ const { canceled, filePaths } = await dialog.showOpenDialog({
title: 'Import Data', title: 'Import Data',
properties: ['openFile'], properties: ['openFile'],
filters: [{ name: 'JSON', extensions: ['json'] }] filters: [{ name: 'JSON', extensions: ['json'] }]
}) })
if (canceled || !filePaths) return if (canceled || !filePaths) return false
if(await fileExists(dataPath)) { if(await fileExists(dataPath)) {
await fs.unlink(dataPath) await fs.unlink(dataPath)
} }
await fs.copyFile(filePaths[0], dataPath,fs.constants.COPYFILE_EXCL) await fs.copyFile(filePaths[0], dataPath,fs.constants.COPYFILE_EXCL)
return true
}) })
ipcMain.handle('open-link', async (event, link) => { ipcMain.handle('open-link', async (event, link) => {
@@ -359,4 +364,24 @@ ipcMain.handle('download-mod', async (event, link) => {
}) })
nexusWindow.loadURL(link) nexusWindow.loadURL(link)
})
ipcMain.handle('auto-detect-game-path', async () => {
const defaultsSilksongPaths = [
":/Program Files (x86)/Steam/steamapps/common/Hollow Knight Silksong",
":/SteamLibrary/steamapps/common/Hollow Knight Silksong"
]
for (const path of defaultsSilksongPaths) {
for (let i = 'A'.charCodeAt(0); i <= 'Z'.charCodeAt(0); i++) {
const fullPath = `${String.fromCharCode(i)}${path}`
if (await fileExists(fullPath)) {
saveSilksongPath(fullPath)
return
}
}
}
})
ipcMain.handle('load-main-page', () => {
mainWindow.loadFile("renderer/index.html")
}) })

View File

@@ -8,11 +8,11 @@ contextBridge.exposeInMainWorld('versions', {
contextBridge.exposeInMainWorld('files', { contextBridge.exposeInMainWorld('files', {
fileExists: (path) => ipcRenderer.invoke('file-exists', path), fileExists: (path) => ipcRenderer.invoke('file-exists', path),
userSavePath: () => ipcRenderer.invoke('get-userSavePath'), delete: () => ipcRenderer.invoke('delete-data'),
delete: (path) => ipcRenderer.invoke('delete-data', path),
export: () => ipcRenderer.invoke('export-data'), export: () => ipcRenderer.invoke('export-data'),
import: () => ipcRenderer.invoke('import-data'), import: () => ipcRenderer.invoke('import-data'),
autoDetectGamePath: () => ipcRenderer.invoke('auto-detect-game-path'),
saveSilksongPath: (path) => ipcRenderer.invoke('save-path', path), saveSilksongPath: (path) => ipcRenderer.invoke('save-path', path),
loadSilksongPath: () => ipcRenderer.invoke('load-path'), loadSilksongPath: () => ipcRenderer.invoke('load-path'),
loadBepinexVersion: () => ipcRenderer.invoke('load-bepinex-version'), loadBepinexVersion: () => ipcRenderer.invoke('load-bepinex-version'),
@@ -23,7 +23,8 @@ contextBridge.exposeInMainWorld('files', {
contextBridge.exposeInMainWorld('electronAPI', { contextBridge.exposeInMainWorld('electronAPI', {
openExternalLink: (url) => ipcRenderer.invoke('open-link', url), openExternalLink: (url) => ipcRenderer.invoke('open-link', url),
launchGame: (mode) => ipcRenderer.invoke('launch-game', mode) launchGame: (mode) => ipcRenderer.invoke('launch-game', mode),
loadMainPage: () => ipcRenderer.invoke('load-main-page')
}); });
contextBridge.exposeInMainWorld('bepinex', { contextBridge.exposeInMainWorld('bepinex', {

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Silk Fly Launcher</title> <title>Silk Fly Launcher</title>
@@ -55,6 +55,7 @@
<h1 id="title">Home</h1> <h1 id="title">Home</h1>
<div class="view" id="view"> <div class="view" id="view">
</div> </div>
</main>
</div> </div>
<!-- Template --> <!-- Template -->

View File

@@ -11,16 +11,6 @@ const versionText = HomeTemplate.content.getElementById("version-text");
navigate("home") navigate("home")
let savePath
files.userSavePath().then(path => {
savePath = `${path}\\config.json`
files.fileExists(savePath).then(result => {
if(!result) {
autoDetectGamePath()
}
});
});
async function navigate(page) { async function navigate(page) {
view.replaceChildren() view.replaceChildren()
switch (page) { switch (page) {
@@ -138,27 +128,16 @@ async function launch(mode) {
} }
async function autoDetectGamePath() { async function autoDetectGamePath() {
const defaultsSilksongPaths = [ await files.autoDetectGamePath()
":/Program Files (x86)/Steam/steamapps/common/Hollow Knight Silksong", if (document.getElementById("silksong-path-input")) {
":/SteamLibrary/steamapps/common/Hollow Knight Silksong" document.getElementById("silksong-path-input").value = await files.loadSilksongPath()
]
for (const path of defaultsSilksongPaths) {
for (let i = 'A'.charCodeAt(0); i <= 'Z'.charCodeAt(0); i++) {
const fullPath = `${String.fromCharCode(i)}${path}`
if (await files.fileExists(fullPath)) {
await files.saveSilksongPath(fullPath)
if (document.getElementById("silksong-path-input")) {
document.getElementById("silksong-path-input").value = await files.loadSilksongPath()
}
return
}
}
} }
} }
async function deleteData() { async function deleteData() {
await files.delete(savePath) await files.delete()
document.getElementById("silksong-path-input").value = await files.loadSilksongPath() document.getElementById("silksong-path-input").value = await files.loadSilksongPath()
document.getElementById("nexus-api-input").value = await files.loadNexusAPI()
} }
async function exportData() { async function exportData() {
@@ -168,6 +147,7 @@ async function exportData() {
async function importData() { async function importData() {
await files.import() await files.import()
document.getElementById("silksong-path-input").value = await files.loadSilksongPath() document.getElementById("silksong-path-input").value = await files.loadSilksongPath()
document.getElementById("nexus-api-input").value = await files.loadNexusAPI()
} }
async function installBepinex() { async function installBepinex() {

View File

@@ -104,6 +104,8 @@ body {
font-size: 28px; font-size: 28px;
margin-bottom: 20px; margin-bottom: 20px;
color: #ffffff; color: #ffffff;
display: flex;
justify-content: center;
} }
.content h2 { .content h2 {
@@ -199,6 +201,13 @@ body {
border-color: rgba(255, 25, 0, 0.8); border-color: rgba(255, 25, 0, 0.8);
} }
.bigger-button {
width: 240px;
height: 80px;
border-radius: 8px;
font-size: 32px;
}
.search-container { .search-container {
position: absolute; position: absolute;
left: 50%; left: 50%;
@@ -304,4 +313,13 @@ body {
width: 32px; width: 32px;
height: 32px; height: 32px;
object-fit: cover; object-fit: cover;
}
.link {
color: #ffffff;
transition: all 0.2s ease;
}
.link:hover {
color: #ff6b6b;
} }

25
renderer/welcome.css Normal file
View File

@@ -0,0 +1,25 @@
.welcome-div {
display: flex;
flex-direction: column;
height: 100%;
flex: 1;
padding: 0 60px;
}
.title {
display: flex;
flex: 1;
font-size: 60px;
margin-bottom: 60px;
color: #ffffff;
justify-content: center;
}
.button-div {
display: flex;
flex: 2;
align-items: center;
justify-content: space-between;
margin-top: 8px;
gap: 40px;
}

70
renderer/welcome.html Normal file
View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Welcome to Silk Fly Launcher</title>
<link rel="stylesheet" href="welcome.css" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="app">
<video autoplay muted loop id="menu_wallpaper" class="background_video">
<source src="assets/background.mp4" type="video/mp4">
</video>
<div class="welcome-div" id="main-div">
<br>
<div id="page">
</div>
<div class="button-div" id="button-div">
</div>
</div>
</div>
<template id="welcome-template">
<h1 class="title">Welcome to Silk Fly Launcher</h1>
</template>
<template id="one-button-template">
<button class="default-button bigger-button" onclick="importData()">Import Data</button>
<button class="default-button bigger-button" onclick="next()">Next →</button>
</template>
<template id="two-button-template">
<button class="default-button bigger-button" onclick="back()">← Back</button>
<button class="default-button bigger-button" onclick="next()">Next →</button>
</template>
<template id="path-template">
<h1 class="title">Enter Hollow Knight: Silksong path</h1>
<p>Please verify the path to your game and edit it if the auto detection didn't work</p>
<div class="horizontal-div">
<label for="silksong-path-label">Enter Silksong path: </label>
<input type="text" class="input" id="silksong-path-input" name="silksong-path-input">
<button class="default-button" onclick="autoDetectGamePath()">Auto Detect</button>
</div>
</template>
<template id="nexus-template">
<h1 class="title">Enter your nexus API key</h1>
<p>Please enter your nexus API key. To get your API key go to <a href="https://www.nexusmods.com/settings/api-keys" class="link" id="external-link">https://www.nexusmods.com/settings/api-keys</a> and click on new personnal API key</p>
<div class="horizontal-div">
<label for="nexus-api-label">Enter your nexus api: </label>
<input type="text" class="input" id="nexus-api-input" name="nexus-api-input">
<img class="nexus-check-image" id="nexus-check-image" src="assets/cross.svg">
<button class="default-button" onclick="verifyNexusAPI()">Verify</button>
</div>
</template>
<template id="style-template">
<h1 class="title">Chose the theme of the app</h1>
</template>
<template id="tutorial-template">
<h1 class="title">Tutorial to download mod from nexus</h1>
</template>
<script src="welcome.js"></script>
</body>
</html>

112
renderer/welcome.js Normal file
View File

@@ -0,0 +1,112 @@
let actualPage = 0
const pageDiv = document.getElementById("page");
const buttonDiv = document.getElementById("button-div");
const oneButtonTemplate = document.getElementById("one-button-template");
const twoButtonTemplate = document.getElementById("two-button-template");
const welcomeTemplate = document.getElementById("welcome-template");
const silksongPathTemplate = document.getElementById("path-template");
const nexusTemplate = document.getElementById("nexus-template");
const styleTemplate = document.getElementById("style-template");
const tutorialTemplate = document.getElementById("tutorial-template");
navigate()
async function navigate() {
pageDiv.replaceChildren()
switch (actualPage) {
case 0:
pageDiv.appendChild(welcomeTemplate.content.cloneNode(true))
buttonDiv.replaceChildren()
buttonDiv.appendChild(oneButtonTemplate.content.cloneNode(true))
break;
case 1:
pageDiv.appendChild(silksongPathTemplate.content.cloneNode(true))
buttonDiv.replaceChildren()
buttonDiv.appendChild(twoButtonTemplate.content.cloneNode(true))
const silksongPathInput = document.getElementById("silksong-path-input")
if (await files.loadSilksongPath() == "") {
autoDetectGamePath()
}
else {
document.getElementById("silksong-path-input").value = await files.loadSilksongPath()
}
silksongPathInput.addEventListener('input', async function(event) {
let silksongPath = silksongPathInput.value
await files.saveSilksongPath(silksongPath)
});
break;
case 2:
pageDiv.appendChild(nexusTemplate.content.cloneNode(true))
const nexusLink = document.getElementById("external-link")
nexusLink.addEventListener('click', function(event) {
event.preventDefault()
const url = nexusLink.href
electronAPI.openExternalLink(url)
})
const nexusAPIInput = document.getElementById("nexus-api-input")
nexusAPIInput.value = await files.loadNexusAPI()
nexusAPIInput.addEventListener('input', async function(event) {
let nexusAPI = nexusAPIInput.value
await files.saveNexusAPI(nexusAPI)
});
break;
case 3:
pageDiv.appendChild(styleTemplate.content.cloneNode(true))
break;
case 4:
pageDiv.appendChild(tutorialTemplate.content.cloneNode(true))
break;
case 5:
electronAPI.loadMainPage()
break;
}
}
function next() {
actualPage++
navigate()
}
function back() {
actualPage--
navigate()
}
async function autoDetectGamePath() {
await files.autoDetectGamePath()
document.getElementById("silksong-path-input").value = await files.loadSilksongPath()
}
async function verifyNexusAPI() {
response = await nexus.verifyAPI()
const nexusCheckImage = document.getElementById("nexus-check-image")
if (nexusCheckImage == undefined) {
return
}
if (response) {
nexusCheckImage.src = "assets/check.svg"
}
else {
nexusCheckImage.src = "assets/cross.svg"
}
}
async function importData() {
const res = await files.import()
if (res) {
electronAPI.loadMainPage()
}
}