9 Commits

22 changed files with 934 additions and 232 deletions

106
README.MD
View File

@@ -1,13 +1,10 @@
<h1 align="center">Silk Fly Launcher</h1> <h1 align="center">Silk Fly Launcher</h1>
<h3 align="center">
— A Silksong mod manager —
</h3>
<p align="center"> <p align="center">
<img alt="Release" src="https://img.shields.io/github/v/release/Gabi-Zar/Silk-Fly-Launcher"> <img alt="Release" src="https://img.shields.io/github/v/release/Gabi-Zar/Silk-Fly-Launcher?include_prereleases&style=flat&color=green">
<img alt="Stars" src="https://img.shields.io/github/stars/Gabi-Zar/Silk-Fly-Launcher?color=magenta"> <img alt="Stars" src="https://img.shields.io/github/stars/Gabi-Zar/Silk-Fly-Launcher?style=flat&color=magenta">
<img alt="Forks" src="https://img.shields.io/github/forks/Gabi-Zar/Silk-Fly-Launcher?color=purple"> <img alt="Forks" src="https://img.shields.io/github/forks/Gabi-Zar/Silk-Fly-Launcher?style=flat&color=purple">
<img alt="License" src="https://img.shields.io/github/license/Gabi-Zar/Silk-Fly-Launcher?color=BB0000"> <img alt="License" src="https://img.shields.io/github/license/Gabi-Zar/Silk-Fly-Launcher?style=flat&color=BB0000">
<br> <br>
<a href="https://github.com/Gabi-Zar"><img title="Developer" src="https://img.shields.io/badge/developer-GabiZar-blue"></a> <a href="https://github.com/Gabi-Zar"><img title="Developer" src="https://img.shields.io/badge/developer-GabiZar-blue"></a>
<img alt="Maintained" src="https://img.shields.io/badge/Maintained-Yes-009900"> <img alt="Maintained" src="https://img.shields.io/badge/Maintained-Yes-009900">
@@ -16,95 +13,94 @@
--- ---
Silk Fly Launcher is an open-source mod manager for Hollow Knight: Silksong, designed to simplify installing and managing Nexus Mods. Silk Fly Launcher is an open-source mod manager for Hollow Knight: Silksong built with Electron, it download mods from Nexus Mods (Thunderstore comming in stable v1.0.0).
Built with Electron, it provides a clean UI, secure Nexus API integration, and seamless switching between vanilla and modded gameplay.
<p align="center"> <p align="center">
<img src="assets/github/about.png"> <img src="assets/github/about.png">
</p> </p>
<details> <details>
<summary>More screenshots (click me)</summary> <summary>More screenshots (click me)</summary>
<p align="center"> <p align="center">
<img src="assets/github/online-mods.png"> <img src="assets/github/online-mods.png">
<img src="assets/github/settings.png"> <img src="assets/github/settings.png">
<img src="assets/github/about-steel.png"> <img src="assets/github/about-steel.png">
<img src="assets/github/search-citadel.png"> <img src="assets/github/search-citadel.png">
<img src="assets/github/installed-mods-cradle.png"> <img src="assets/github/installed-mods-cradle.png">
<img src="assets/github/about-greyroot.png"> <img src="assets/github/about-greyroot.png">
<img src="assets/github/latest-mods-abyss.png"> <img src="assets/github/latest-mods-abyss.png">
</p> </p>
</details> </details>
## 🌟 Features ## Features
- Install / Uninstall / Backup BepInEx for Silksong. - Install / Uninstall / Backup BepInEx for Silksong.
- Automatically detect Silksong installation path for Steam on Windows - Automatically detect Silksong installation path for Steam on Windows
- Browse and download Nexus mods directly in the app - Browse and download Thunderstore and Nexus mods directly in the app
- Activate and deactivate mods
- Multiple themes inspired by Silksong - Multiple themes inspired by Silksong
- Launch Silksong in Vanilla or Modded without deleting the mods - Launch Silksong in Vanilla or Modded without deleting the mods
- Securely store your Nexus API key on your device using Electron's safeStorage, and then additionally encrypt it with an AES key (provided at build time) via Electron Store. - Securely store your Nexus API key on your device using Electron's safeStorage, and then encrypt it with an AES key (provided at build time) via Electron Store.
- Works on Windows (Linux coming soon) - Works on Windows (Linux coming soon)
## 💻 Compatibility ## Compatibility
- Windows x64 - [x] Windows x64
- 🛠 Linux (Comming in v1.0.0) - [ ] Linux (Comming in v1.0.0)
- macOS (need a tester) - [ ] macOS (need a tester)
## 🚀 Installation & Usage ## Installation & Usage
1. Download the app in your desired format from the [GitHub Releases](https://github.com/Gabi-Zar/Silk-Fly-Launcher/releases) page or build it from [source](#how-to-build) to have the latest features. 1. Download the app in your desired format from the [GitHub Releases](https://github.com/Gabi-Zar/Silk-Fly-Launcher/releases) page or build it from source to have the latest features.
2. Install it and follow the in-app guide 2. Install it and follow the in-app guide.
## ⚙️ How to build ## How to build
1. Install [Git](https://git-scm.com/) and [Node JS](https://nodejs.org) 1. Install [Git](https://git-scm.com/) and [Node JS](https://nodejs.org)
2. Clone the repo `git clone https://github.com/Gabi-Zar/Silk-Fly-Launcher ` 2. Clone the repo `git clone https://github.com/Gabi-Zar/Silk-Fly-Launcher `
3. Go into the repository `cd Silk-Fly-Launcher ` 3. Go into the repository `cd Silk-Fly-Launcher `
4. Install npm dependencies `npm install ` 4. Install npm dependencies `npm install `
5. Start the app with `npm run start ` 5. Start the app with `npm run start `
6. Build the app with `npm run make:options ` 6. Optionnal dependencies:
7. Available Build options are - [Wix toolset v3](https://github.com/wixtoolset/wix3/releases/tag/wix3141rtm) for msi windows build
7. Build the app with `npm run make:options `
8. Available Build options:
- none - build all
- zip - Make a zip for Windows x64
- msi - Make a msi build for Windows x64
``` ## Todo
none - build all
zip - Make a zip for Windows x64
msi - Make a msi build for Windows x64
```
## ✅ Todo #### For stable release 1.0.0
#### For release 1.0.0
- [x] BepInEx support - [x] BepInEx support
- [x] Nexus support - [x] Nexus support
- [ ] Disable / Enable Individual mods - [x] Disable / Enable Individual mods
- [ ] Support for offline mods - [x] Automatic update
- [ ] Auto Download Mods Dependencies - [x] Support for Thunderstore
- [ ] Linux support - [ ] Linux support
- [ ] Automatic update - [x] Support for offline mods
- [ ] Auto Download Mods Dependencies
#### For Later #### For later
- [ ] Automatically detect Silksong installation path on other platforms - [ ] Automatically detect Silksong installation path on other platforms
- [ ] Multiple mods profiles - [ ] Multiple mods profiles
- [ ] Support for MelonLoader - [ ] Support for MelonLoader
- [ ] Support for Thunderstore
- [ ] French translation - [ ] French translation
- [ ] macOS support (need a tester) - [ ] macOS support (need a tester)
## 🤝 Contributing ##### Todo list sorted by importance
Pull requests are welcome. ## Contributing
For major changes, please open an issue first to discuss what you would like to change.
## 📜 License and credit This is my first Electron project, I will love any feedback. Just open an issue or make a pull request.
This project is licensed under the [GPL-3.0 license](LICENSE). ## License and credit
This product uses third-party modules or assets under open source [third-party](THIRD-PARTY-LICENSES) licenses
This project is licensed under the [GPL-3.0 license](LICENSE).<br>
This product uses third-party modules or assets under open source [third-party](THIRD-PARTY-LICENSES) licenses.
> [!CAUTION] > [!CAUTION]
> Some assets came from Hollow Knight: Silksong. > Some assets came from Hollow Knight: Silksong.
@@ -113,4 +109,4 @@ This product uses third-party modules or assets under open source [third-party](
--- ---
<p align="center">If you like this app, consider giving it a on GitHub!</p> <p align="center">If you like this app, consider giving it a star on GitHub!</p>

View File

@@ -8,6 +8,152 @@ Hollow Knight: Silksong is property of Team Cherry.
This project is not affiliated with or endorsed by Team Cherry. This project is not affiliated with or endorsed by Team Cherry.
LibreICONS
MIT
MIT License
Copyright (c) 2018 Diemen Design
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.
Iconbuddy Simple Icons
CC0 1.0
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
7zip-bin 5.2.0 7zip-bin 5.2.0
MIT MIT
The MIT License (MIT) The MIT License (MIT)

View File

@@ -12,6 +12,7 @@ if (buildTarget == "msi" || buildTarget == "all") {
config: { config: {
icon: "./assets/icon.ico", icon: "./assets/icon.ico",
ui: { enabled: true, chooseDirectory: true }, ui: { enabled: true, chooseDirectory: true },
upgradeCode: "e3bb759f-c2b2-4545-bb7b-35bed5be4cd5",
}, },
}); });
} }
@@ -23,15 +24,6 @@ if (buildTarget == "zip" || buildTarget == "all") {
}); });
} }
async function fileExists(filePath) {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
module.exports = { module.exports = {
packagerConfig: { packagerConfig: {
asar: true, asar: true,

363
main.js
View File

@@ -11,6 +11,9 @@ import { path7za } from "7zip-bin";
import node7z from "node-7z"; 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 { 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);
@@ -36,9 +39,11 @@ let installedCachedModList;
let installedTotalModsCount; let installedTotalModsCount;
let onlineCachedModList; let onlineCachedModList;
let onlineTotalModsCount; let onlineTotalModsCount;
let thunderstoreCachedModList;
let thunderstoreTotalModsCount;
const bepinexFiles = [".doorstop_version", "changelog.txt", "doorstop_config.ini", "winhttp.dll"]; const bepinexFiles = [".doorstop_version", "changelog.txt", "doorstop_config.ini", "winhttp.dll"];
let bepinexVersion; let bepinexVersion = bepinexStore.get("bepinex-version");
let bepinexBackupVersion; let bepinexBackupVersion;
let mainWindow; let mainWindow;
@@ -79,6 +84,9 @@ async function createWindow() {
mainWindow.once("ready-to-show", () => { mainWindow.once("ready-to-show", () => {
mainWindow.show(); mainWindow.show();
if (!isDev) {
verifyUpdate();
}
}); });
} }
@@ -115,13 +123,60 @@ app.on("open-url", (event, url) => {
handleNxmUrl(url); handleNxmUrl(url);
}); });
async function verifyUpdate() {
const GITHUB_URL = "https://api.github.com/repos/Gabi-Zar/Silk-Fly-Launcher/releases";
const res = await fetch(GITHUB_URL, {
headers: {
"User-Agent": userAgent,
Accept: "application/vnd.github+json",
},
});
if (!res.ok) {
if (res.status == 403) {
mainWindow.webContents.send("showToast", "Github has blocked the application. Please try again later.", "error");
}
throw new Error(`GitHub API error: ${res.status}`);
}
const releases = await res.json();
const prerelease = releases.find((r) => r.prerelease);
const release = releases.find((r) => !r.prerelease && !r.draft);
let prereleaseVersion;
let releaseVersion;
let latestVersion;
if (prerelease) {
prereleaseVersion = prerelease.tag_name.replace(/^v/, "");
latestVersion = prereleaseVersion;
}
if (release) {
releaseVersion = release.tag_name.replace(/^v/, "");
latestVersion = releaseVersion;
}
if (prereleaseVersion && releaseVersion) {
latestVersion = semverGt(prereleaseVersion, releaseVersion) ? prereleaseVersion : releaseVersion;
}
if (latestVersion != VERSION) {
mainWindow.webContents.send(
"showBanner",
`Update v${latestVersion} is available on <a href="" class="link" onclick="electronAPI.openExternalLink('https://github.com/Gabi-Zar/Silk-Fly-Launcher/releases/tag/v${latestVersion}')">GitHub</a>! Your current version is ${VERSION}.`,
);
}
}
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
///////////////// SAVING AND LOADING ///////////////// ///////////////// SAVING AND LOADING /////////////////
ipcMain.handle("save-path", (event, path) => { ipcMain.handle("save-path", (event, path) => {
saveSilksongPath(path); saveSilksongPath(path);
}); });
function saveSilksongPath(path) { function saveSilksongPath(path) {
store.set("silksong-path", path); store.set("silksong-path", path);
checkInstalledMods();
} }
function loadSilksongPath() { function loadSilksongPath() {
@@ -201,13 +256,33 @@ 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) {
if (thunderstoreCachedModList) {
modInfo = thunderstoreCachedModList.find((mod) => mod.modId == modId);
}
}
if (!modInfo) {
modInfo = optionalModInfo;
}
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);
} }
@@ -310,9 +385,7 @@ async function installBepinex() {
saveBepinexVersion(release.tag_name); saveBepinexVersion(release.tag_name);
} }
if (await fileExists(modSavePath)) { checkInstalledMods();
await fs.cp(modSavePath, path.join(silksongPath, "BepInEx", "plugins"), { recursive: true });
}
} }
ipcMain.handle("install-bepinex", async () => { ipcMain.handle("install-bepinex", async () => {
@@ -401,7 +474,7 @@ ipcMain.handle("delete-bepinex-backup", async () => {
}); });
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
/////////////////////// NEXUS //////////////////////// //////////////// NEXUS / THUNDERSTORE ////////////////
async function createNexus(api) { async function createNexus(api) {
if (api == undefined) { if (api == undefined) {
@@ -414,9 +487,10 @@ async function createNexus(api) {
} catch (error) { } catch (error) {
if (error.mStatusCode == 401) { if (error.mStatusCode == 401) {
mainWindow.webContents.send("showToast", "Invalid Nexus API key", "error"); mainWindow.webContents.send("showToast", "Invalid Nexus API key", "error");
} } else if (error.code == "ENOTFOUND") {
if (error.code == "ENOTFOUND") {
mainWindow.webContents.send("showToast", "Unable to communicate with Nexus servers", "error"); mainWindow.webContents.send("showToast", "Unable to communicate with Nexus servers", "error");
} else {
mainWindow.webContents.send("showToast", "Unable to create Nexus API ", "error");
} }
nexus = undefined; nexus = undefined;
} }
@@ -440,12 +514,17 @@ ipcMain.handle("get-mods", async (event, type) => {
if (!installedCachedModList) { if (!installedCachedModList) {
await searchInstalledMods(""); await searchInstalledMods("");
} }
return { modsInfo: installedCachedModList, installedTotalCount: installedTotalModsCount }; return { installedModsInfo: installedCachedModList, installedTotalCount: installedTotalModsCount };
} else if (type == "mods-online") { } else if (type == "mods-online") {
if (!onlineCachedModList) { if (!onlineCachedModList) {
await searchNexusMods(""); await searchNexusMods("");
} }
return { mods: onlineCachedModList, onlineTotalCount: onlineTotalModsCount }; return { onlineModsInfo: onlineCachedModList, onlineTotalCount: onlineTotalModsCount };
} else if (type == "mods-thunderstore") {
if (!thunderstoreCachedModList) {
await searchThunderstoreMods("");
}
return { thunderstoreModsInfo: thunderstoreCachedModList, thunderstoreTotalCount: thunderstoreTotalModsCount };
} }
}); });
@@ -511,38 +590,6 @@ async function startDownload(modId, fileId, key, expires) {
installedCachedModList = undefined; installedCachedModList = undefined;
} }
async function checkInstalledMods() {
const bepinexFolderPath = path.join(loadSilksongPath(), "BepInEx");
for (const [key, modInfo] of Object.entries(installedModsStore.store)) {
modInfo.modId = String(modInfo.modId);
if (!(await fileExists(path.join(modSavePath, modInfo.modId)))) {
saveModInfo(key, true);
await fs.rm(path.join(bepinexFolderPath, "plugins", modInfo.modId), { recursive: true });
}
}
}
ipcMain.handle("uninstall-mod", async (event, modId) => {
modId = String(modId);
const BepinexPluginsPath = path.join(loadSilksongPath(), "BepInEx", "plugins");
const modPath = path.join(BepinexPluginsPath, modId);
if (await fileExists(path.join(modSavePath, modId))) {
await fs.rm(path.join(modSavePath, modId), { recursive: true });
}
if (await fileExists(modPath)) {
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, offset, count, sortFilter, sortOrder) => { ipcMain.handle("search-nexus-mods", async (event, keywords, offset, count, sortFilter, sortOrder) => {
await searchNexusMods(keywords, offset, count, sortFilter, sortOrder); await searchNexusMods(keywords, offset, count, sortFilter, sortOrder);
}); });
@@ -600,6 +647,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);
} }
@@ -608,6 +656,89 @@ async function searchNexusMods(keywords, offset = 0, count = 10, sortFilter = "d
onlineTotalModsCount = data.mods.totalCount; onlineTotalModsCount = data.mods.totalCount;
} }
ipcMain.handle("search-thunderstore-mods", async (event, keywords, offset, count, sortFilter, sortOrder) => {
await searchThunderstoreMods(keywords, offset, count, sortFilter, sortOrder);
});
async function searchThunderstoreMods(keywords, offset = 0, count = 10, sortFilter = "downloads", sortOrder = "DESC") {
const res = await fetch("https://thunderstore.io/c/hollow-knight-silksong/api/v1/package/", {
headers: {
"User-Agent": userAgent,
},
});
let modsInfo = await res.json();
const modsToRemove = [
"f21c391c-0bc5-431d-a233-95323b95e01b",
"58c9e43a-549d-4d49-a576-4eed36775a84",
"0fc63a3b-3c69-4be5-85f8-bb127eec81b3",
"d5419c5d-c22a-4a47-b73d-ba4101f28635",
"d5b65a03-1217-4496-8af6-8dda4d763676",
"42f76853-d2a4-4520-949b-13a02fdbbbcb",
"34eac80c-5497-470e-b98c-f53421b828c0",
];
let reMappedModsInfo = [];
for (let i = 0; i < modsInfo.length; i++) {
modsInfo[i].source = "thunderstore";
if (modsToRemove.includes(modsInfo[i].uuid4)) {
modsInfo.splice(i, 1);
i--;
continue;
}
reMappedModsInfo.push(reMapThunderstoreModsInfo(modsInfo[i]));
}
const result = sortAndFilterModsList(reMappedModsInfo, keywords, offset, count, sortFilter, sortOrder);
thunderstoreCachedModList = result.list;
thunderstoreTotalModsCount = result.totalCount;
}
function reMapThunderstoreModsInfo(modInfo) {
let totalDownloads = 0;
for (const version of modInfo.versions) {
totalDownloads += version.downloads;
}
return {
author: modInfo.owner,
endorsements: modInfo.rating_score,
modId: modInfo.uuid4,
name: modInfo.name,
pictureUrl: modInfo.versions[0].icon,
summary: modInfo.versions[0].description,
updatedAt: modInfo.date_updated,
createdAt: modInfo.date_created,
version: modInfo.versions[0].version_number,
downloads: totalDownloads,
fileSize: modInfo.versions[0].file_size,
source: modInfo.source,
};
}
ipcMain.handle("download-thunderstore-mods", async (event, url, modId) => {
const bepinexFolderPath = path.join(loadSilksongPath(), "BepInEx");
if (!(await fileExists(loadSilksongPath()))) {
mainWindow.webContents.send("showToast", "Path to the game invalid", "warning");
return;
}
if (!(await fileExists(modSavePath))) {
await fs.mkdir(modSavePath);
}
await downloadAndUnzip(url, path.join(modSavePath, modId));
if (await fileExists(bepinexFolderPath)) {
await fs.cp(path.join(modSavePath, modId), path.join(bepinexFolderPath, "plugins", modId), { recursive: true });
}
saveModInfo(modId);
mainWindow.webContents.send("showToast", "Mod downloaded successfully.");
installedCachedModList = undefined;
});
//////////////////////////////////////////////////////
//////////////////////// MODS ////////////////////////
ipcMain.handle("search-installed-mods", async (event, keywords, offset, count, sortFilter, sortOrder) => { ipcMain.handle("search-installed-mods", async (event, keywords, offset, count, sortFilter, sortOrder) => {
await searchInstalledMods(keywords, offset, count, sortFilter, sortOrder); await searchInstalledMods(keywords, offset, count, sortFilter, sortOrder);
}); });
@@ -618,23 +749,156 @@ async function searchInstalledMods(keywords, offset = 0, count = 10, sortFilter
modsInfo.push(modInfo); modsInfo.push(modInfo);
} }
const modsInfoFiltered = modsInfo.filter((mod) => mod.name.toLowerCase().includes(keywords.toLowerCase())); const result = sortAndFilterModsList(modsInfo, keywords, offset, count, sortFilter, sortOrder);
installedCachedModList = result.list;
installedTotalModsCount = result.totalCount;
}
async function checkInstalledMods() {
if (!loadSilksongPath()) {
return;
}
const bepinexPluginsPath = path.join(loadSilksongPath(), "BepInEx", "plugins");
for (const [key, modInfo] of Object.entries(installedModsStore.store)) {
modInfo.modId = String(modInfo.modId);
if (!(await fileExists(path.join(modSavePath, modInfo.modId)))) {
saveModInfo(key, true);
if (await fileExists(path.join(bepinexPluginsPath, modInfo.modId))) {
await fs.rm(path.join(bepinexPluginsPath, modInfo.modId), { recursive: true });
}
continue;
}
if (modInfo.activated) {
await fs.cp(path.join(modSavePath, modInfo.modId), path.join(bepinexPluginsPath, modInfo.modId), { recursive: true });
}
}
}
ipcMain.handle("uninstall-mod", async (event, modId) => {
modId = String(modId);
const BepinexPluginsPath = path.join(loadSilksongPath(), "BepInEx", "plugins");
const modPath = path.join(BepinexPluginsPath, modId);
if (await fileExists(path.join(modSavePath, modId))) {
await fs.rm(path.join(modSavePath, modId), { recursive: true });
}
if (await fileExists(modPath)) {
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("activate-mod", async (event, modId) => {
await activateMod(modId);
});
async function activateMod(modId) {
if (!loadSilksongPath()) {
return;
}
const BepinexPluginsPath = path.join(loadSilksongPath(), "BepInEx", "plugins");
if (!installedModsStore.get(`${modId}.activated`)) {
installedModsStore.set(`${modId}.activated`, true);
}
if (bepinexVersion) {
if (!(await fileExists(path.join(BepinexPluginsPath, String(modId))))) {
await fs.cp(path.join(modSavePath, String(modId)), path.join(BepinexPluginsPath, String(modId)), { recursive: true });
}
}
}
ipcMain.handle("deactivate-mod", async (event, modId) => {
const BepinexPluginsPath = path.join(loadSilksongPath(), "BepInEx", "plugins");
if (installedModsStore.get(`${modId}.activated`)) {
installedModsStore.set(`${modId}.activated`, false);
if (bepinexVersion) {
if (await fileExists(path.join(BepinexPluginsPath, String(modId)))) {
await fs.rm(path.join(BepinexPluginsPath, String(modId)), { recursive: true });
}
}
}
});
function sortAndFilterModsList(list, keywords, offset, count, sortFilter, sortOrder) {
const listFiltered = list.filter((element) => element.name.toLowerCase().includes(keywords.toLowerCase()));
const sortFactor = sortOrder == "ASC" ? 1 : -1; const sortFactor = sortOrder == "ASC" ? 1 : -1;
let modsInfoSorted; let listSorted;
if (sortFilter == "name" || sortFilter == "createdAt" || sortFilter == "updatedAt") { if (sortFilter == "name" || sortFilter == "createdAt" || sortFilter == "updatedAt") {
modsInfoSorted = modsInfoFiltered.sort((a, b) => sortFactor * a[sortFilter].localeCompare(b[sortFilter])); listSorted = listFiltered.sort((a, b) => sortFactor * a[sortFilter].localeCompare(b[sortFilter]));
} else if (sortFilter == "downloads" || sortFilter == "endorsements" || sortFilter == "size") { } else if (sortFilter == "downloads" || sortFilter == "endorsements" || sortFilter == "size") {
if (sortFilter == "size") { if (sortFilter == "size") {
sortFilter = "fileSize"; sortFilter = "fileSize";
} }
modsInfoSorted = modsInfoFiltered.sort((a, b) => sortFactor * (a[sortFilter] - b[sortFilter])); listSorted = listFiltered.sort((a, b) => sortFactor * (a[sortFilter] - b[sortFilter]));
} }
installedTotalModsCount = modsInfoSorted.length; return { list: listSorted.slice(offset, offset + count), totalCount: listSorted.length };
installedCachedModList = modsInfoSorted.slice(offset, offset + count);
} }
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 ////////////////////
@@ -682,6 +946,7 @@ ipcMain.handle("open-window", async (event, file) => {
ipcMain.handle("launch-game", async (event, mode) => { ipcMain.handle("launch-game", async (event, mode) => {
const silksongExecutablePath = path.join(loadSilksongPath(), "Hollow Knight Silksong.exe"); const silksongExecutablePath = path.join(loadSilksongPath(), "Hollow Knight Silksong.exe");
const bepinexFolderPath = path.join(loadSilksongPath(), "BepInEx");
if (!fileExists(silksongExecutablePath)) { if (!fileExists(silksongExecutablePath)) {
mainWindow.webContents.send("showToast", "Path to the game invalid", "warning"); mainWindow.webContents.send("showToast", "Path to the game invalid", "warning");
return; return;

7
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "silkflylauncher", "name": "silkflylauncher",
"version": "1.0.0", "version": "1.0.0-dev",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "silkflylauncher", "name": "silkflylauncher",
"version": "1.0.0", "version": "1.0.0-dev",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@nexusmods/nexus-api": "^1.1.5", "@nexusmods/nexus-api": "^1.1.5",
@@ -14,7 +14,8 @@
"electron-store": "^11.0.2", "electron-store": "^11.0.2",
"graphql": "^16.12.0", "graphql": "^16.12.0",
"graphql-request": "^7.4.0", "graphql-request": "^7.4.0",
"node-7z": "^3.0.0" "node-7z": "^3.0.0",
"semver": "^7.7.4"
}, },
"devDependencies": { "devDependencies": {
"@electron-forge/cli": "^7.11.1", "@electron-forge/cli": "^7.11.1",

View File

@@ -30,7 +30,8 @@
"electron-store": "^11.0.2", "electron-store": "^11.0.2",
"graphql": "^16.12.0", "graphql": "^16.12.0",
"graphql-request": "^7.4.0", "graphql-request": "^7.4.0",
"node-7z": "^3.0.0" "node-7z": "^3.0.0",
"semver": "^7.7.4"
}, },
"AES-key-nexus-api": "__AES_KEY__" "AES-key-nexus-api": "__AES_KEY__"
} }

View File

@@ -34,6 +34,11 @@ contextBridge.exposeInMainWorld("electronAPI", {
callback(message, type, duration); callback(message, type, duration);
}); });
}, },
onShowBanner: (callback) => {
ipcRenderer.on("showBanner", (event, message) => {
callback(message);
});
},
}); });
contextBridge.exposeInMainWorld("bepinex", { contextBridge.exposeInMainWorld("bepinex", {
@@ -45,9 +50,20 @@ contextBridge.exposeInMainWorld("bepinex", {
contextBridge.exposeInMainWorld("nexus", { contextBridge.exposeInMainWorld("nexus", {
verifyAPI: () => ipcRenderer.invoke("verify-nexus-api"), verifyAPI: () => ipcRenderer.invoke("verify-nexus-api"),
getMods: (type) => ipcRenderer.invoke("get-mods", type),
download: (link) => ipcRenderer.invoke("open-download", link), download: (link) => ipcRenderer.invoke("open-download", link),
uninstall: (modId) => ipcRenderer.invoke("uninstall-mod", modId),
search: (keywords, offset, count, sortFilter, sortOrder) => ipcRenderer.invoke("search-nexus-mods", keywords, offset, count, sortFilter, sortOrder), 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), });
contextBridge.exposeInMainWorld("mods", {
searchInstalled: (keywords, offset, count, sortFilter, sortOrder) => ipcRenderer.invoke("search-installed-mods", keywords, offset, count, sortFilter, sortOrder),
uninstall: (modId) => ipcRenderer.invoke("uninstall-mod", modId),
getMods: (type) => ipcRenderer.invoke("get-mods", type),
activateMods: (modId) => ipcRenderer.invoke("activate-mod", modId),
deactivateMods: (modId) => ipcRenderer.invoke("deactivate-mod", modId),
add: () => ipcRenderer.invoke("add-offline-mod"),
});
contextBridge.exposeInMainWorld("thunderstore", {
search: (keywords, offset, count, sortFilter, sortOrder) => ipcRenderer.invoke("search-thunderstore-mods", keywords, offset, count, sortFilter, sortOrder),
download: (url, modId) => ipcRenderer.invoke("download-thunderstore-mods", url, modId),
}); });

View File

@@ -1 +0,0 @@
<svg role="img" focusable="false" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14"><path d="m 1,8.3051015 c 0,-0.6641 0.2078922,-1.253128 0.6236766,-1.772859 0.4157844,-0.51973 0.9412897,-0.854668 1.5822907,-0.999037 0.1790183,-0.791146 0.5890279,-1.437922 1.2184793,-1.946102 0.6294514,-0.508181 1.3570741,-0.762272 2.1713184,-0.762272 0.79692,0 1.507219,0.248316 2.130895,0.739172 0.623677,0.490857 1.027912,1.126083 1.212705,1.899904 l 0.190568,0 c 0.51973,0 0.999037,0.127045 1.437921,0.375361 0.438883,0.248316 0.791145,0.594803 1.045236,1.033686 C 12.86718,7.3118385 13,7.7853705 13,8.3051015 c 0,0.508181 -0.12127,0.981713 -0.363811,1.414822 -0.242541,0.4331085 -0.577479,0.7795955 -0.999038,1.0394605 -0.421559,0.259866 -0.889317,0.398461 -1.391723,0.415785 l -6.4735319,0 C 2.9980751,11.14052 2.3455245,10.846006 1.8084697,10.291627 1.2714148,9.7430225 1,9.0789225 1,8.3051015 Z m 0.987488,0 c 0,0.502406 0.1732435,0.935515 0.5197305,1.305101 0.346487,0.369586 0.7680462,0.5659275 1.2646776,0.5948015 l 6.4619829,0 c 0.496631,-0.0231 0.91819,-0.2252155 1.264677,-0.5948015 0.352262,-0.369586 0.525506,-0.80847 0.525506,-1.305101 0,-0.508181 -0.190568,-0.94129 -0.565929,-1.310876 -0.375361,-0.369586 -0.820019,-0.554379 -1.33975,-0.554379 l -0.923965,0 c -0.06352,0 -0.09817,-0.03465 -0.09817,-0.103947 L 9.055828,6.0067375 C 8.992308,5.3830605 8.72089,4.8575555 8.247358,4.4359965 7.773826,4.0144375 7.225222,3.8007705 6.59577,3.8007705 c -0.6294512,0 -1.1838304,0.213667 -1.6458131,0.635226 -0.4677575,0.421559 -0.7333975,0.947064 -0.7911454,1.570741 l -0.046198,0.329162 c 0,0.0693 -0.040424,0.103947 -0.1154957,0.103947 l -0.3060635,0 c -0.4850818,0.05775 -0.8893167,0.26564 -1.2127045,0.617901 -0.3233879,0.352262 -0.4908566,0.768047 -0.4908566,1.247354 z"/></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1 +1 @@
<svg role="img" focusable="false" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14"><path d="m 13,5.384615 v 5.07693 q 0,0.66346 -0.47596,1.13942 -0.47596,0.47596 -1.13942,0.47596 H 2.61538 q -0.663457,0 -1.139418,-0.47596 Q 1,11.125005 1,10.461545 V 3.538465 Q 1,2.875005 1.475962,2.399045 1.951923,1.923075 2.61538,1.923075 h 2.3077 q 0.66346,0 1.13942,0.47597 0.47596,0.47596 0.47596,1.13942 v 0.23077 h 4.84616 q 0.66346,0 1.13942,0.47596 Q 13,4.721155 13,5.384615 z"/></svg> <svg role="img" focusable="false" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" fill="#ffffff"><path d="m 13,5.384615 v 5.07693 q 0,0.66346 -0.47596,1.13942 -0.47596,0.47596 -1.13942,0.47596 H 2.61538 q -0.663457,0 -1.139418,-0.47596 Q 1,11.125005 1,10.461545 V 3.538465 Q 1,2.875005 1.475962,2.399045 1.951923,1.923075 2.61538,1.923075 h 2.3077 q 0.66346,0 1.13942,0.47597 0.47596,0.47596 0.47596,1.13942 v 0.23077 h 4.84616 q 0.66346,0 1.13942,0.47596 Q 13,4.721155 13,5.384615 z"/></svg>

Before

Width:  |  Height:  |  Size: 503 B

After

Width:  |  Height:  |  Size: 518 B

View File

@@ -1 +1 @@
<svg class="social-github" role="img" focusable="false" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14"><path d="m 7,1.1480539 c -3.313515,0 -6,2.68652 -6,6 0,2.65087 1.719175,4.9003701 4.103505,5.6933601 C 5.403301,12.897114 5.5,12.711544 5.5,12.552834 l 0,-1.11716 C 3.831039,11.798964 3.483359,10.728164 3.483359,10.728164 3.210913,10.034324 2.817388,9.8497439 2.817388,9.8497439 c -0.544456,-0.37205 0.04101,-0.36474 0.04101,-0.36474 0.602563,0.0425 0.919922,0.6186401 0.919922,0.6186401 0.535136,0.91698 1.403301,0.65187 1.746088,0.49855 0.05371,-0.38773 0.208951,-0.6523401 0.380854,-0.8022501 -1.333048,-0.15137 -2.733437,-0.66603 -2.733437,-2.96534 0,-0.65528 0.234379,-1.19045 0.618204,-1.61036 -0.0625,-0.15137 -0.267612,-0.76171 0.05764,-1.5879 0,0 0.503913,-0.16113 1.650349,0.61523 0.478981,-0.13282 0.992214,-0.19969 1.501981,-0.20214 0.509767,0.002 1.023903,0.0693 1.502913,0.20214 1.145534,-0.77636 1.648427,-0.61523 1.648427,-0.61523 0.326709,0.82616 0.121107,1.4365 0.0591,1.5879 0.384786,0.41991 0.61768,0.95508 0.61768,1.61036 0,2.30467 -1.403797,2.81199 -2.739729,2.96044 0.214806,0.18606 0.411641,0.5517401 0.411641,1.1113201 0,0.80274 0,1.44923 0,1.64647 0,0.15967 0.09615,0.34669 0.400369,0.28812 C 11.283214,12.046534 13,9.7984539 13,7.1480839 c 0,-3.31348 -2.686544,-6.00003 -6,-6.00003 z"/></svg> <svg class="social-github" role="img" focusable="false" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" fill="#ffffff"><path d="m 7,1.1480539 c -3.313515,0 -6,2.68652 -6,6 0,2.65087 1.719175,4.9003701 4.103505,5.6933601 C 5.403301,12.897114 5.5,12.711544 5.5,12.552834 l 0,-1.11716 C 3.831039,11.798964 3.483359,10.728164 3.483359,10.728164 3.210913,10.034324 2.817388,9.8497439 2.817388,9.8497439 c -0.544456,-0.37205 0.04101,-0.36474 0.04101,-0.36474 0.602563,0.0425 0.919922,0.6186401 0.919922,0.6186401 0.535136,0.91698 1.403301,0.65187 1.746088,0.49855 0.05371,-0.38773 0.208951,-0.6523401 0.380854,-0.8022501 -1.333048,-0.15137 -2.733437,-0.66603 -2.733437,-2.96534 0,-0.65528 0.234379,-1.19045 0.618204,-1.61036 -0.0625,-0.15137 -0.267612,-0.76171 0.05764,-1.5879 0,0 0.503913,-0.16113 1.650349,0.61523 0.478981,-0.13282 0.992214,-0.19969 1.501981,-0.20214 0.509767,0.002 1.023903,0.0693 1.502913,0.20214 1.145534,-0.77636 1.648427,-0.61523 1.648427,-0.61523 0.326709,0.82616 0.121107,1.4365 0.0591,1.5879 0.384786,0.41991 0.61768,0.95508 0.61768,1.61036 0,2.30467 -1.403797,2.81199 -2.739729,2.96044 0.214806,0.18606 0.411641,0.5517401 0.411641,1.1113201 0,0.80274 0,1.44923 0,1.64647 0,0.15967 0.09615,0.34669 0.400369,0.28812 C 11.283214,12.046534 13,9.7984539 13,7.1480839 c 0,-3.31348 -2.686544,-6.00003 -6,-6.00003 z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="#ffffff"><path fill="#ffffff" d="M17.376 0c-.993 0-2.18.686-2.907 1.182c-1.676-.36-4.036-.545-6.787.635c-1.365-.513-2.425-.562-3.32-.488a2.16 2.16 0 0 0-1.27.429c-.33.22-2.788 2.69-3.069 4.652C-.15 7.508.68 8.932 1.218 9.718c-.44 1.76-.2 4.572.517 6.188c-.353 1.041-.713 2.089-.664 3.205c.01.584.061 1.188.398 1.684C1.72 21.19 4.528 24 6.545 24c.957 0 1.93-.428 3.07-1.24c2.16.383 4.402.348 6.448-.532c2.573 1.001 4.224.625 4.84.162c.587-.457 2.826-2.915 3.07-4.622c.1-.672-.023-1.638-1.226-3.397a11 11 0 0 0-.501-6.455c.396-1.069.673-2.188.59-3.337c-.015-.68-.221-1.167-.487-1.507c-.209-.335-2.415-2.39-4.028-2.91A3.1 3.1 0 0 0 17.376 0m-.03 2.082c.65.015 2.155 1.093 3.01 1.906l.355.34c-.959-.163-2.125.428-3.26 1.55a10.3 10.3 0 0 0-1.358 1.595c-.28.384-.517.768-.753 1.285l1.18.635l-3.895 1.477l-1.122-4.18l1.033.547c1.358-3.102 2.524-3.973 3.232-4.416h.015a5.1 5.1 0 0 1 1.49-.724zM12 3.065a9 9 0 0 1 2.22.279a8 8 0 0 0-.42.488a8.4 8.4 0 0 0-1.8-.196a8.34 8.34 0 0 0-5.897 2.432a8 8 0 0 1-.37-.433A8.9 8.9 0 0 1 12 3.065m-7.076.305c.71-.002 1.309.127 2.2.466a9.5 9.5 0 0 0-1.713 1.337c-.327-.542-.624-1.156-.488-1.803m-.606.042c-.162.96.428 2.126 1.55 3.264c.457.487 1.003.945 1.594 1.358c.383.281.767.517 1.283.754l.62-1.182l1.49 3.914l-4.176 1.122l.546-1.033c-3.099-1.36-3.969-2.526-4.412-3.235v-.015a5.1 5.1 0 0 1-.723-1.491l-.015-.074c.015-.65 1.092-2.156 1.904-3.013Zm16.035 1.483a1.3 1.3 0 0 1 .26.015l.14.023a5 5 0 0 1-.13 1.137v.015q-.152.574-.377 1.148a9.5 9.5 0 0 0-1.346-1.776c.547-.357 1.051-.546 1.453-.562M18.43 5.8a8.9 8.9 0 0 1 2.506 6.2a9 9 0 0 1-.27 2.183a8 8 0 0 0-.488-.425A8.4 8.4 0 0 0 20.364 12A8.33 8.33 0 0 0 18 6.173a8 8 0 0 1 .429-.373M3.315 9.905q.235.222.488.425A8.4 8.4 0 0 0 3.636 12c0 2.248.887 4.286 2.327 5.788a8 8 0 0 1-.426.376A8.9 8.9 0 0 1 3.065 12a9 9 0 0 1 .25-2.095m13.988 1.541l-.546 1.034c3.098 1.359 3.969 2.526 4.412 3.235v.014c.34.488.575.99.723 1.492l.014.074c-.014.65-1.092 2.156-1.903 3.013l-.34.354c.163-.96-.427-2.127-1.549-3.264a10.3 10.3 0 0 0-1.594-1.359a7 7 0 0 0-1.283-.753l-.605 1.152l-1.505-3.87zm-6.006 1.684l1.121 4.18l-1.033-.547c-1.357 3.102-2.523 3.973-3.231 4.416h-.015c-.487.34-.989.576-1.49.724l-.074.015c-.65-.015-2.154-1.093-3.01-1.906l-.354-.34c.959.163 2.124-.428 3.26-1.55c.488-.458.945-1.004 1.358-1.595c.28-.384.517-.768.753-1.285l-1.166-.635ZM3.72 16.663A9.5 9.5 0 0 0 5.086 18.5c-.697.47-1.33.665-1.777.59l-.138-.024c0-.367.038-.748.128-1.137v-.015c.11-.417.254-.835.42-1.252m14.131 1.314q.194.21.372.43A8.9 8.9 0 0 1 12 20.936a9 9 0 0 1-2.282-.296a8 8 0 0 0 .417-.487a8.34 8.34 0 0 0 7.716-2.175m.696.889c.43.666.607 1.267.534 1.698l-.023.138a5 5 0 0 1-1.136-.128h-.014a11 11 0 0 1-1.114-.366a9.5 9.5 0 0 0 1.753-1.342"/></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="244"
height="244"
viewBox="0 0 64.558333 64.558333"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1"
transform="translate(-1.5875,-1.5875005)">
<path
style="fill:none;stroke:#ffffff;stroke-width:6.35;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 62.970833,33.866667 H 4.7625"
id="path1" />
<path
style="fill:none;stroke:#ffffff;stroke-width:6.35;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 33.866666,62.970833 V 4.7625005"
id="path2" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 838 B

View File

@@ -3,7 +3,8 @@
focusable="false" focusable="false"
aria-hidden="true" aria-hidden="true"
viewBox="0 0 14 14" viewBox="0 0 14 14"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg"
fill="#ffffff">
<path <path
d="m 8.8752116,12.181305 v -0.845421 c 0.3824271,-0.154001 0.726847,-0.36571 1.0522956,-0.605995 l 0.7732288,0.43071 c 0.129524,0.07348 0.274423,0.111383 0.423312,0.111383 0.07294,0 0.14588,-0.0105 0.216596,-0.02797 0.216594,-0.05726 0.40349,-0.194791 0.514241,-0.381738 L 12.886967,9.137646 C 13.120376,8.7467822 12.980643,8.2446393 12.57519,8.0191521 L 11.798888,7.5845715 c 0.02336,-0.1937454 0.05915,-0.3841963 0.05915,-0.58406 0,-0.200282 -0.03578,-0.3919096 -0.05915,-0.5854981 L 12.57519,5.9817665 C 12.769673,5.8719503 12.910975,5.6936322 12.97129,5.4831529 13.02958,5.2718895 12.99661,5.0497752 12.88697,4.8614422 L 11.854885,3.1377619 C 11.699911,2.8751729 11.41463,2.7284127 11.122019,2.7284127 c -0.144899,0 -0.288488,0.034775 -0.421219,0.1098161 L 9.9275724,3.2698799 C 9.6021235,3.0301169 9.256657,2.8177291 8.8752766,2.6630465 V 1.8201103 C 8.8752766,1.3669134 8.4961856,1 8.0281931,1 H 5.9710286 C 5.5019897,1 5.1238146,1.3669134 5.1238146,1.8201103 V 2.6630465 C 4.7434808,2.8178336 4.3959207,3.0302213 4.0729577,3.2698799 L 3.2988145,2.8372867 C 3.1697472,2.7645994 3.0251103,2.7282557 2.8783147,2.7282557 c -0.073855,0 -0.1488238,0.010461 -0.2218277,0.027978 C 2.4398921,2.8119249 2.256332,2.9484095 2.1422449,3.1364026 L 1.1123216,4.8612069 c -0.23151155,0.3921709 -0.09407,0.8935296 0.3104686,1.1203242 l 0.7773489,0.433247 c -0.023163,0.193484 -0.059268,0.3851637 -0.059268,0.5855242 0,0.1997591 0.036177,0.39021 0.059268,0.583354 L 1.4227902,8.0174787 C 1.0182522,8.2431227 0.88081119,8.7454488 1.1123216,9.1373584 L 2.1421142,10.86198 c 0.1140877,0.186948 0.2977122,0.32453 0.5142421,0.381738 0.071957,0.0183 0.1458801,0.02797 0.218622,0.02797 0.149086,0 0.2937219,-0.03791 0.4237052,-0.111384 l 0.7722463,-0.430711 c 0.3249255,0.240286 0.6724201,0.452176 1.0528194,0.606022 v 0.84542 C 5.1237492,12.632693 5.5019241,13 5.9709632,13 h 2.0571645 c 0.4679925,1.83e-4 0.8470835,-0.367176 0.8470835,-0.818855 z M 4.663215,7.0004593 c 0,-1.2491741 1.0468009,-2.2607289 2.334205,-2.2607289 1.2928991,0 2.3395692,1.0114764 2.3395692,2.2607289 0,1.2470298 -1.0466701,2.2585586 -2.3395692,2.2585586 C 5.7100159,9.2589655 4.663215,8.2474891 4.663215,7.0004593 Z" /> d="m 8.8752116,12.181305 v -0.845421 c 0.3824271,-0.154001 0.726847,-0.36571 1.0522956,-0.605995 l 0.7732288,0.43071 c 0.129524,0.07348 0.274423,0.111383 0.423312,0.111383 0.07294,0 0.14588,-0.0105 0.216596,-0.02797 0.216594,-0.05726 0.40349,-0.194791 0.514241,-0.381738 L 12.886967,9.137646 C 13.120376,8.7467822 12.980643,8.2446393 12.57519,8.0191521 L 11.798888,7.5845715 c 0.02336,-0.1937454 0.05915,-0.3841963 0.05915,-0.58406 0,-0.200282 -0.03578,-0.3919096 -0.05915,-0.5854981 L 12.57519,5.9817665 C 12.769673,5.8719503 12.910975,5.6936322 12.97129,5.4831529 13.02958,5.2718895 12.99661,5.0497752 12.88697,4.8614422 L 11.854885,3.1377619 C 11.699911,2.8751729 11.41463,2.7284127 11.122019,2.7284127 c -0.144899,0 -0.288488,0.034775 -0.421219,0.1098161 L 9.9275724,3.2698799 C 9.6021235,3.0301169 9.256657,2.8177291 8.8752766,2.6630465 V 1.8201103 C 8.8752766,1.3669134 8.4961856,1 8.0281931,1 H 5.9710286 C 5.5019897,1 5.1238146,1.3669134 5.1238146,1.8201103 V 2.6630465 C 4.7434808,2.8178336 4.3959207,3.0302213 4.0729577,3.2698799 L 3.2988145,2.8372867 C 3.1697472,2.7645994 3.0251103,2.7282557 2.8783147,2.7282557 c -0.073855,0 -0.1488238,0.010461 -0.2218277,0.027978 C 2.4398921,2.8119249 2.256332,2.9484095 2.1422449,3.1364026 L 1.1123216,4.8612069 c -0.23151155,0.3921709 -0.09407,0.8935296 0.3104686,1.1203242 l 0.7773489,0.433247 c -0.023163,0.193484 -0.059268,0.3851637 -0.059268,0.5855242 0,0.1997591 0.036177,0.39021 0.059268,0.583354 L 1.4227902,8.0174787 C 1.0182522,8.2431227 0.88081119,8.7454488 1.1123216,9.1373584 L 2.1421142,10.86198 c 0.1140877,0.186948 0.2977122,0.32453 0.5142421,0.381738 0.071957,0.0183 0.1458801,0.02797 0.218622,0.02797 0.149086,0 0.2937219,-0.03791 0.4237052,-0.111384 l 0.7722463,-0.430711 c 0.3249255,0.240286 0.6724201,0.452176 1.0528194,0.606022 v 0.84542 C 5.1237492,12.632693 5.5019241,13 5.9709632,13 h 2.0571645 c 0.4679925,1.83e-4 0.8470835,-0.367176 0.8470835,-0.818855 z M 4.663215,7.0004593 c 0,-1.2491741 1.0468009,-2.2607289 2.334205,-2.2607289 1.2928991,0 2.3395692,1.0114764 2.3395692,2.2607289 0,1.2470298 -1.0466701,2.2585586 -2.3395692,2.2585586 C 5.7100159,9.2589655 4.663215,8.2474891 4.663215,7.0004593 Z" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -3,7 +3,8 @@
focusable="false" focusable="false"
aria-hidden="true" aria-hidden="true"
viewBox="0 0 14 14" viewBox="0 0 14 14"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg"
fill="#ffffff">
<path <path
d="M 5.75,4.5454543 C 5.75,4.6562497 5.71289,4.7521362 5.6386718,4.8331133 5.5644534,4.9140684 5.4765624,4.9545458 5.375,4.9545458 H 4.625 V 11.5 h -1.5 V 4.9545458 h -0.75 c -0.1015625,0 -0.1894532,-0.040477 -0.2636719,-0.1214325 C 2.0371098,4.7521364 2,4.6562497 2,4.5454543 2,4.434659 2.03711,4.3387842 2.1113281,4.2578297 l 1.5,-1.6363638 C 3.6855468,2.5404886 3.7734374,2.5 3.8749999,2.5 c 0.1015625,0 0.1894532,0.04049 0.2636719,0.1214659 l 1.5,1.6363638 C 5.7128911,4.3387838 5.75,4.434659 5.75,4.5454543 Z" /> d="M 5.75,4.5454543 C 5.75,4.6562497 5.71289,4.7521362 5.6386718,4.8331133 5.5644534,4.9140684 5.4765624,4.9545458 5.375,4.9545458 H 4.625 V 11.5 h -1.5 V 4.9545458 h -0.75 c -0.1015625,0 -0.1894532,-0.040477 -0.2636719,-0.1214325 C 2.0371098,4.7521364 2,4.6562497 2,4.5454543 2,4.434659 2.03711,4.3387842 2.1113281,4.2578297 l 1.5,-1.6363638 C 3.6855468,2.5404886 3.7734374,2.5 3.8749999,2.5 c 0.1015625,0 0.1894532,0.04049 0.2636719,0.1214659 l 1.5,1.6363638 C 5.7128911,4.3387838 5.75,4.434659 5.75,4.5454543 Z" />
<path <path

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -3,7 +3,8 @@
focusable="false" focusable="false"
aria-hidden="true" aria-hidden="true"
viewBox="0 0 14 14" viewBox="0 0 14 14"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg"
fill="#ffffff">
<path <path
d="M 6,3.7272726 C 6,3.8749998 5.9505205,4.0028484 5.8515624,4.1108179 5.7526046,4.2187579 5.6354166,4.2727279 5.5,4.2727279 H 4.4999999 V 13 H 2.5 V 4.2727279 h -1 c -0.1354167,0 -0.2526042,-0.053969 -0.3515625,-0.16191 C 1.0494797,4.0028487 1,3.8749998 1,3.7272726 1,3.5795455 1.0494795,3.4517124 1.1484375,3.3437731 l 2,-2.1818186 C 3.2473957,1.0539848 3.3645832,1 3.4999998,1 3.6354166,1 3.7526041,1.053986 3.8515624,1.1619545 l 2,2.1818186 C 5.9505213,3.4517119 6,3.5795455 6,3.7272726 Z" /> d="M 6,3.7272726 C 6,3.8749998 5.9505205,4.0028484 5.8515624,4.1108179 5.7526046,4.2187579 5.6354166,4.2727279 5.5,4.2727279 H 4.4999999 V 13 H 2.5 V 4.2727279 h -1 c -0.1354167,0 -0.2526042,-0.053969 -0.3515625,-0.16191 C 1.0494797,4.0028487 1,3.8749998 1,3.7272726 1,3.5795455 1.0494795,3.4517124 1.1484375,3.3437731 l 2,-2.1818186 C 3.2473957,1.0539848 3.3645832,1 3.4999998,1 3.6354166,1 3.7526041,1.053986 3.8515624,1.1619545 l 2,2.1818186 C 5.9505213,3.4517119 6,3.5795455 6,3.7272726 Z" />
<path <path

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -3,7 +3,8 @@
focusable="false" focusable="false"
aria-hidden="true" aria-hidden="true"
viewBox="0 0 14 14" viewBox="0 0 14 14"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg"
fill="#ffffff">
<path <path
d="m 10.781355,12.15887 v -0.352258 c 0.159345,-0.06416 0.302854,-0.152379 0.438458,-0.252498 l 0.32218,0.179463 c 0.05396,0.03062 0.114338,0.0464 0.176383,0.0464 0.03039,0 0.06079,-0.0044 0.09023,-0.01163 0.09026,-0.02386 0.168121,-0.08116 0.214269,-0.159057 l 0.430036,-0.718594 c 0.09725,-0.16286 0.03902,-0.372086 -0.129909,-0.466039 l -0.323454,-0.181075 c 0.0098,-0.08074 0.02464,-0.160081 0.02464,-0.243358 0,-0.08345 -0.01491,-0.1632952 -0.02464,-0.2439564 L 12.323,9.5757277 c 0.08104,-0.04575 0.139914,-0.1200624 0.165044,-0.2077546 0.02429,-0.08803 0.0105,-0.180574 -0.03513,-0.2590459 L 12.022891,8.3907278 c -0.06457,-0.1094124 -0.18344,-0.1705615 -0.305364,-0.1705615 -0.06038,0 -0.1202,0.01449 -0.175508,0.045756 L 11.21984,8.4457815 C 11.084235,8.3458802 10.940291,8.257386 10.781381,8.192935 V 7.8417121 C 10.781381,7.6528803 10.623426,7.5 10.428427,7.5 H 9.5712702 C 9.375837,7.5 9.2182616,7.6528803 9.2182616,7.8417121 V 8.192935 C 9.0597901,8.2574296 8.9149709,8.3459242 8.7804031,8.4457815 L 8.4578409,8.265535 C 8.4040669,8.235247 8.3438034,8.220105 8.2826318,8.220105 c -0.030776,0 -0.062013,0.00436 -0.092425,0.011662 -0.090237,0.023205 -0.166733,0.080074 -0.2142681,0.1584039 L 7.5468008,9.1088332 c -0.096463,0.1634049 -0.039188,0.3723033 0.1293616,0.466802 l 0.3238982,0.1805187 c -0.00962,0.080625 -0.024687,0.1604849 -0.024687,0.2439671 0,0.08324 0.015076,0.162588 0.024687,0.243065 l -0.3238977,0.180759 c -0.168558,0.09401 -0.2258256,0.30332 -0.1293616,0.466615 l 0.4290822,0.718592 c 0.047526,0.0779 0.1240501,0.13522 0.2142681,0.159057 0.029976,0.0076 0.060788,0.01162 0.091088,0.01162 0.062124,0 0.1223876,-0.0158 0.1765454,-0.04641 l 0.3217708,-0.179464 c 0.1353866,0.100125 0.280177,0.188406 0.4386774,0.25251 v 0.352258 c 0,0.18823 0.1575741,0.341274 0.3530085,0.341274 h 0.8571563 c 0.194997,7.6e-5 0.352954,-0.15299 0.352954,-0.341189 z m -1.75501,-2.158683 c 0,-0.520488 0.4361698,-0.9419688 0.9725912,-0.9419688 0.5387118,0 0.9748248,0.4214483 0.9748248,0.9419688 0,0.519595 -0.436113,0.941065 -0.9748248,0.941065 C 9.4625148,10.94123 9.026345,10.519782 9.026345,10.000187 Z" /> d="m 10.781355,12.15887 v -0.352258 c 0.159345,-0.06416 0.302854,-0.152379 0.438458,-0.252498 l 0.32218,0.179463 c 0.05396,0.03062 0.114338,0.0464 0.176383,0.0464 0.03039,0 0.06079,-0.0044 0.09023,-0.01163 0.09026,-0.02386 0.168121,-0.08116 0.214269,-0.159057 l 0.430036,-0.718594 c 0.09725,-0.16286 0.03902,-0.372086 -0.129909,-0.466039 l -0.323454,-0.181075 c 0.0098,-0.08074 0.02464,-0.160081 0.02464,-0.243358 0,-0.08345 -0.01491,-0.1632952 -0.02464,-0.2439564 L 12.323,9.5757277 c 0.08104,-0.04575 0.139914,-0.1200624 0.165044,-0.2077546 0.02429,-0.08803 0.0105,-0.180574 -0.03513,-0.2590459 L 12.022891,8.3907278 c -0.06457,-0.1094124 -0.18344,-0.1705615 -0.305364,-0.1705615 -0.06038,0 -0.1202,0.01449 -0.175508,0.045756 L 11.21984,8.4457815 C 11.084235,8.3458802 10.940291,8.257386 10.781381,8.192935 V 7.8417121 C 10.781381,7.6528803 10.623426,7.5 10.428427,7.5 H 9.5712702 C 9.375837,7.5 9.2182616,7.6528803 9.2182616,7.8417121 V 8.192935 C 9.0597901,8.2574296 8.9149709,8.3459242 8.7804031,8.4457815 L 8.4578409,8.265535 C 8.4040669,8.235247 8.3438034,8.220105 8.2826318,8.220105 c -0.030776,0 -0.062013,0.00436 -0.092425,0.011662 -0.090237,0.023205 -0.166733,0.080074 -0.2142681,0.1584039 L 7.5468008,9.1088332 c -0.096463,0.1634049 -0.039188,0.3723033 0.1293616,0.466802 l 0.3238982,0.1805187 c -0.00962,0.080625 -0.024687,0.1604849 -0.024687,0.2439671 0,0.08324 0.015076,0.162588 0.024687,0.243065 l -0.3238977,0.180759 c -0.168558,0.09401 -0.2258256,0.30332 -0.1293616,0.466615 l 0.4290822,0.718592 c 0.047526,0.0779 0.1240501,0.13522 0.2142681,0.159057 0.029976,0.0076 0.060788,0.01162 0.091088,0.01162 0.062124,0 0.1223876,-0.0158 0.1765454,-0.04641 l 0.3217708,-0.179464 c 0.1353866,0.100125 0.280177,0.188406 0.4386774,0.25251 v 0.352258 c 0,0.18823 0.1575741,0.341274 0.3530085,0.341274 h 0.8571563 c 0.194997,7.6e-5 0.352954,-0.15299 0.352954,-0.341189 z m -1.75501,-2.158683 c 0,-0.520488 0.4361698,-0.9419688 0.9725912,-0.9419688 0.5387118,0 0.9748248,0.4214483 0.9748248,0.9419688 0,0.519595 -0.436113,0.941065 -0.9748248,0.941065 C 9.4625148,10.94123 9.026345,10.519782 9.026345,10.000187 Z" />
<path <path

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -3,7 +3,8 @@
focusable="false" focusable="false"
aria-hidden="true" aria-hidden="true"
viewBox="0 0 14 14" viewBox="0 0 14 14"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg"
fill="#ffffff">
<path <path
d="m 2.1464844,1.0019531 c -0.1349019,-0.006832 -0.2388263,0.00771 -0.3085938,0.044922 -1.11627905,0.5953992 -1.11627905,11.3108509 0,11.9062499 C 2.9541697,13.548524 13,8.1907983 13,7 13,5.8836266 4.1700127,1.1044327 2.1464844,1.0019531 Z m 0.7617187,1.25 C 4.5101631,2.3330828 11.5,6.1162044 11.5,7 c 0,0.9427153 -7.9541697,5.184248 -8.8378906,4.712891 -0.883721,-0.471358 -0.883721,-8.954424 0,-9.4257816 0.055233,-0.02946 0.1392964,-0.040565 0.2460937,-0.035156 z" /> d="m 2.1464844,1.0019531 c -0.1349019,-0.006832 -0.2388263,0.00771 -0.3085938,0.044922 -1.11627905,0.5953992 -1.11627905,11.3108509 0,11.9062499 C 2.9541697,13.548524 13,8.1907983 13,7 13,5.8836266 4.1700127,1.1044327 2.1464844,1.0019531 Z m 0.7617187,1.25 C 4.5101631,2.3330828 11.5,6.1162044 11.5,7 c 0,0.9427153 -7.9541697,5.184248 -8.8378906,4.712891 -0.883721,-0.471358 -0.883721,-8.954424 0,-9.4257816 0.055233,-0.02946 0.1392964,-0.040565 0.2460937,-0.035156 z" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 616 B

After

Width:  |  Height:  |  Size: 634 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 24 24" fill="#ffffff"><path fill="#ffffff" d="m.322 13.174l4.706 8.192L7.2 16.855L4.824 12.72a1.416 1.416 0 0 1 0-1.444l2.965-5.16c.265-.46.718-.723 1.245-.723h1.595l-3.086 6.953h3.812L6.171 22.403L16.583 9.914h-3.201l2.184-4.52h6.052L24 1.25H7.175c-.86 0-1.598.428-2.028 1.174l-4.825 8.4a2.306 2.306 0 0 0 0 2.35m7.213 9.576h9.29a2.29 2.29 0 0 0 2.03-1.176l4.825-8.4a2.317 2.317 0 0 0 0-2.35l-1.93-3.36h-4.763l2.19 3.813c.262.46.262.987 0 1.444l-2.964 5.162a1.41 1.41 0 0 1-1.248.723h-2.154l-1.497-.017z"/></svg>

After

Width:  |  Height:  |  Size: 591 B

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Silk Fly Launcher</title> <title>Silk Fly Launcher</title>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; img-src 'self' https://*.nexusmods.com" /> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; img-src 'self' https://*.nexusmods.com https://*.thunderstore.io" />
<link rel="stylesheet" href="style.css" /> <link rel="stylesheet" href="style.css" />
</head> </head>
<body> <body>
@@ -17,43 +17,52 @@
<h5 class="logo-title">Silk Fly Launcher</h5> <h5 class="logo-title">Silk Fly Launcher</h5>
</div> </div>
<nav class="nav"> <div class="sidebar-content">
<div class="nav-section"> <nav class="nav">
<span class="nav-title">Execute Silksong</span> <div class="nav-section">
<button class="horizontal-div" onclick="launch('vanilla')"> <span class="nav-title">Execute Silksong</span>
<img src="assets/icons/start-vanilla.svg" class="icons invert-color" /> <button class="horizontal-div" onclick="launch('vanilla')">
Run Vanilla <img src="assets/icons/start-vanilla.svg" class="icons" />
</button> Run Vanilla
<button class="horizontal-div" onclick="launch('modded')"> </button>
<img src="assets/icons/start-modded.svg" class="icons invert-color" /> <button class="horizontal-div" onclick="launch('modded')">
Run Modded <img src="assets/icons/start-modded.svg" class="icons" />
</button> Run Modded
</div> </button>
</div>
<div class="nav-section"> <div class="nav-section">
<span class="nav-title">Mods</span> <span class="nav-title">Mods</span>
<button class="horizontal-div" onclick="navigate('mods-installed')"> <button class="horizontal-div" onclick="navigate('mods-installed')">
<img src="assets/icons/folder.svg" class="icons invert-color" /> <img src="assets/icons/folder.svg" class="icons" />
Installed Installed
</button> </button>
<button class="horizontal-div" onclick="navigate('mods-online')"> <button class="horizontal-div" onclick="navigate('mods-thunderstore')">
<img src="assets/icons/cloud.svg" class="icons invert-color" /> <img src="assets/icons/thunderstore.svg" class="icons" />
Online Thunderstore
</button> </button>
</div> <button class="horizontal-div" onclick="navigate('mods-online')">
<img src="assets/icons/nexus-mods.svg" class="icons" />
Nexus
</button>
</div>
</nav>
<div class="nav-section"> <nav class="nav">
<span class="nav-title">Settings</span>
<button class="horizontal-div" onclick="navigate('general-settings')"> <button class="horizontal-div" onclick="navigate('general-settings')">
<img src="assets/icons/settings.svg" class="icons invert-color" /> <img src="assets/icons/settings.svg" class="icons" />
General Settings
</button> </button>
</div> </nav>
</nav> </div>
</aside> </aside>
<!-- Main content --> <!-- Main content -->
<main class="content"> <main class="content">
<div class="banner-div" id="banner-div">
<p id="banner-text"></p>
<button class="default-button square-button" onclick="hideBanner()">X</button>
</div>
<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> <div class="toast-div" id="toast-div"></div>
@@ -72,12 +81,12 @@
</div> </div>
</div> </div>
<div class="horizontal-div"> <div class="horizontal-div">
<img onclick="electronAPI.openExternalLink('https://github.com/Gabi-Zar/Silk-Fly-Launcher')" src="assets/icons/github.svg" alt="Github logo" class="logo-img invert-color" /> <img onclick="electronAPI.openExternalLink('https://github.com/Gabi-Zar/Silk-Fly-Launcher')" src="assets/icons/github.svg" alt="Github logo" class="logo-img" />
</div> </div>
</div> </div>
<br /> <br />
<ul> <ul>
<li>Silk Fly Launcher is a launcher and mod manager for Silksong mods from Nexus, built with Electron.</li> <li>Silk Fly Launcher is a launcher and mod manager for Silksong mods from Nexus and Thunderstore, built with Electron.</li>
<li>This product is licensed under the <a href="" class="link" onclick="electronAPI.openWindow('LICENSE')">GNU General Public License Version 3</a>.</li> <li>This product is licensed under the <a href="" class="link" onclick="electronAPI.openWindow('LICENSE')">GNU General Public License Version 3</a>.</li>
<li>This product uses third-party modules or assets under <a href="" class="link" onclick="electronAPI.openWindow('THIRD-PARTY-LICENSES')">third-party licenses</a>.</li> <li>This product uses third-party modules or assets under <a href="" class="link" onclick="electronAPI.openWindow('THIRD-PARTY-LICENSES')">third-party licenses</a>.</li>
<li> <li>
@@ -107,7 +116,8 @@
<li id="size" onclick="changeSort('size')">by size</li> <li id="size" onclick="changeSort('size')">by size</li>
</div> </div>
</div> </div>
<button class="default-button square-button" onclick="inverseSort()"><img class="icons invert-color" 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">
@@ -122,7 +132,7 @@
</div> </div>
</template> </template>
<template id="online-mods-template"> <template id="nexus-mods-template">
<h2>List Of Nexus Mods</h2> <h2>List Of Nexus Mods</h2>
<div class="horizontal-div"> <div class="horizontal-div">
<form class="horizontal-div input-form" id="search-form"> <form class="horizontal-div input-form" id="search-form">
@@ -140,7 +150,40 @@
<li id="size" onclick="changeSort('size')">by size</li> <li id="size" onclick="changeSort('size')">by size</li>
</div> </div>
</div> </div>
<button class="default-button square-button" onclick="inverseSort()"><img class="icons invert-color" 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>
</div>
<div class="mods-container" id="mods-container"></div>
<div class="separated-div">
<div class="horizontal-div">
<button class="default-button" onclick="changeModsPage('min')">First page</button>
<button class="default-button" onclick="changeModsPage(-1)">Previous</button>
</div>
<div class="horizontal-div">
<button class="default-button" onclick="changeModsPage(1)">Next</button>
<button class="default-button" onclick="changeModsPage('max')">Last page</button>
</div>
</div>
</template>
<template id="thunderstore-mods-template">
<h2>List Of Thunderstore Mods</h2>
<div class="horizontal-div">
<form class="horizontal-div input-form" id="search-form">
<input class="input" id="search-input" type="text" placeholder="Search For Mods..." />
<button class="default-button" onclick="searchThunderstoreMods()">Search</button>
</form>
<div class="list-div">
<div class="default-button smaller-button" id="sort-button" onclick="toggleSortMenu()">Sort</div>
<div class="list-menu longer-button list-menu-inverted" id="sort-menu">
<li id="name" onclick="changeSort('name')">by name</li>
<li id="downloads" onclick="changeSort('downloads')">by downloads count</li>
<li id="endorsements" onclick="changeSort('endorsements')">by rating count</li>
<li id="createdAt" onclick="changeSort('createdAt')">by date of creation</li>
<li id="updatedAt" onclick="changeSort('updatedAt')">by date of updating</li>
<li id="size" onclick="changeSort('size')">by size</li>
</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>
</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">
@@ -161,15 +204,21 @@
<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>
<label class="checkbox-container">
<input type="checkbox" name="activated-mod" id="activated-mod" />
<span class="checkmark"></span>
</label>
</div> </div>
</div> </div>
@@ -221,7 +270,7 @@
<li id="Steel" onclick="changeTheme('Steel')">Steel</li> <li id="Steel" onclick="changeTheme('Steel')">Steel</li>
</div> </div>
</div> </div>
<label class="lace-pin-checkbox-container"> <label class="checkbox-container">
<input type="checkbox" name="lace-pin" id="lace-pin" /> <input type="checkbox" name="lace-pin" id="lace-pin" />
<span class="checkmark"></span> <span class="checkmark"></span>
Lace Pin Lace Pin

View File

@@ -4,7 +4,8 @@ const view = document.getElementById("view");
const HomeTemplate = document.getElementById("home-template"); const HomeTemplate = document.getElementById("home-template");
const installedModsTemplate = document.getElementById("installed-mods-template"); const installedModsTemplate = document.getElementById("installed-mods-template");
const onlineModsTemplate = document.getElementById("online-mods-template"); const nexusModsTemplate = document.getElementById("nexus-mods-template");
const thunderstoreModsTemplate = document.getElementById("thunderstore-mods-template");
const settingsTemplate = document.getElementById("settings-template"); const settingsTemplate = document.getElementById("settings-template");
const installedModTemplate = document.getElementById("installed-mod-template"); const installedModTemplate = document.getElementById("installed-mod-template");
const modTemplate = document.getElementById("mod-template"); const modTemplate = document.getElementById("mod-template");
@@ -14,18 +15,31 @@ let actualTheme = [];
let searchValueNexus = ""; let searchValueNexus = "";
let searchValueInstalled = ""; let searchValueInstalled = "";
let searchValueThunderstore = "";
let onlineSortFilter = "downloads"; let onlineSortFilter = "downloads";
let installedSortFilter = "name"; let installedSortFilter = "name";
let thunderstoreSortFilter = "downloads";
let onlineSortOrder = "DESC"; let onlineSortOrder = "DESC";
let installedSortOrder = "ASC"; let installedSortOrder = "ASC";
let thunderstoreSortOrder = "DESC";
let onlineOffset = 0; let onlineOffset = 0;
let installedOffset = 0; let installedOffset = 0;
let thunderstoreOffset = 0;
let lastOnlineOffset = 0; let lastOnlineOffset = 0;
let lastInstalledOffset = 0; let lastInstalledOffset = 0;
let lastThunderstoreOffset = 0;
let onlineModsCount = 10; let onlineModsCount = 10;
let installedModsCount = 10; let installedModsCount = 10;
let thunderstoreModsCount = 10;
let onlineModsTotalCount; let onlineModsTotalCount;
let installedModsTotalCount; let installedModsTotalCount;
let thunderstoreModsTotalCount;
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
///////////////// CONST FOR WELCOME ////////////////// ///////////////// CONST FOR WELCOME //////////////////
@@ -42,7 +56,6 @@ const welcomeTemplate = document.getElementById("welcome-template");
const silksongPathTemplate = document.getElementById("path-template"); const silksongPathTemplate = document.getElementById("path-template");
const nexusTemplate = document.getElementById("nexus-template"); const nexusTemplate = document.getElementById("nexus-template");
const styleTemplate = document.getElementById("style-template"); const styleTemplate = document.getElementById("style-template");
const tutorialTemplate = document.getElementById("tutorial-template");
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
////////////////////// STARTUP /////////////////////// ////////////////////// STARTUP ///////////////////////
@@ -98,14 +111,15 @@ async function navigate(page) {
toggleSelectedListButton("sort-menu", installedSortFilter); toggleSelectedListButton("sort-menu", installedSortFilter);
setSortOrderButton(); setSortOrderButton();
const { modsInfo, installedTotalCount } = await nexus.getMods(page); const { installedModsInfo, installedTotalCount } = await mods.getMods(page);
installedModsTotalCount = installedTotalCount; installedModsTotalCount = installedTotalCount;
if (modsInfo == []) { if (installedModsInfo == []) {
break; break;
} }
for (const modInfo of modsInfo) { for (const modInfo of installedModsInfo) {
const installedModTemplateCopy = installedModTemplate.content.cloneNode(true); const installedModTemplateCopy = installedModTemplate.content.cloneNode(true);
if (modInfo.name) { if (modInfo.name) {
const modTitleText = installedModTemplateCopy.getElementById("mod-title"); const modTitleText = installedModTemplateCopy.getElementById("mod-title");
modTitleText.innerText = modInfo.name; modTitleText.innerText = modInfo.name;
@@ -138,25 +152,58 @@ 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 modUrl = `https://www.nexusmods.com/hollowknightsilksong/mods/${modInfo.modId}`; const isActivatedCheckbox = installedModTemplateCopy.getElementById("activated-mod");
if (modInfo.activated) {
isActivatedCheckbox.checked = true;
}
const modLinkButton = installedModTemplateCopy.getElementById("external-link"); isActivatedCheckbox.addEventListener("change", async function () {
modLinkButton.href = modUrl; if (this.checked) {
modLinkButton.addEventListener("click", function (event) { mods.activateMods(modInfo.modId);
event.preventDefault(); searchInstalledMods();
const modLink = modLinkButton.href; } else {
electronAPI.openExternalLink(modLink); mods.deactivateMods(modInfo.modId);
searchInstalledMods();
}
}); });
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");
modLinkButton.href = modUrls[modInfo.source];
modLinkButton.addEventListener("click", function (event) {
event.preventDefault();
const modLink = modLinkButton.href;
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) {
event.preventDefault(); event.preventDefault();
await nexus.uninstall(modInfo.modId); await mods.uninstall(modInfo.modId);
navigate("refresh"); navigate("refresh");
}); });
@@ -167,8 +214,8 @@ async function navigate(page) {
break; break;
case "mods-online": case "mods-online":
title.innerText = "Online Mods"; title.innerText = "Nexus Mods";
const onlineModsTemplateCopy = onlineModsTemplate.content.cloneNode(true); const onlineModsTemplateCopy = nexusModsTemplate.content.cloneNode(true);
const ModsContainer = onlineModsTemplateCopy.getElementById("mods-container"); const ModsContainer = onlineModsTemplateCopy.getElementById("mods-container");
const searchFormNexus = onlineModsTemplateCopy.getElementById("search-form"); const searchFormNexus = onlineModsTemplateCopy.getElementById("search-form");
const searchInputNexus = onlineModsTemplateCopy.getElementById("search-input"); const searchInputNexus = onlineModsTemplateCopy.getElementById("search-input");
@@ -182,12 +229,12 @@ async function navigate(page) {
toggleSelectedListButton("sort-menu", onlineSortFilter); toggleSelectedListButton("sort-menu", onlineSortFilter);
setSortOrderButton(); setSortOrderButton();
const { mods, onlineTotalCount } = await nexus.getMods(page); const { onlineModsInfo, onlineTotalCount } = await mods.getMods(page);
onlineModsTotalCount = onlineTotalCount; onlineModsTotalCount = onlineTotalCount;
if (mods == undefined) { if (onlineModsInfo == undefined) {
break; break;
} }
for (const mod of mods) { for (const mod of onlineModsInfo) {
if (mod.name == undefined) { if (mod.name == undefined) {
continue; continue;
} }
@@ -250,6 +297,89 @@ async function navigate(page) {
} }
break; break;
case "mods-thunderstore":
title.innerText = "Thunderstore Mods";
const thunderstoreModsTemplateCopy = thunderstoreModsTemplate.content.cloneNode(true);
const thunderstoreModsContainer = thunderstoreModsTemplateCopy.getElementById("mods-container");
const searchFormThunderstore = thunderstoreModsTemplateCopy.getElementById("search-form");
const searchInputThunderstore = thunderstoreModsTemplateCopy.getElementById("search-input");
searchFormThunderstore.addEventListener("submit", async function (event) {
event.preventDefault();
});
searchInputThunderstore.value = searchValueThunderstore;
view.appendChild(thunderstoreModsTemplateCopy);
toggleSelectedListButton("sort-menu", thunderstoreSortFilter);
setSortOrderButton();
const { thunderstoreModsInfo, thunderstoreTotalCount } = await mods.getMods(page);
thunderstoreModsTotalCount = thunderstoreTotalCount;
if (thunderstoreModsInfo == undefined) {
break;
}
for (const mod of thunderstoreModsInfo) {
if (mod.name == undefined) {
continue;
}
const modTemplateCopy = modTemplate.content.cloneNode(true);
if (mod.name) {
const modTitleText = modTemplateCopy.getElementById("mod-title");
modTitleText.innerText = mod.name.replaceAll("_", " ");
}
if (mod.author) {
const modAuthorText = modTemplateCopy.getElementById("mod-author");
modAuthorText.innerText = `by ${mod.author}`;
}
if (mod.endorsements) {
const modEndorsementsNumber = modTemplateCopy.getElementById("mod-endorsements-number");
if (mod.endorsements > 1) {
modEndorsementsNumber.innerText = `${mod.endorsements} likes`;
} else {
modEndorsementsNumber.innerText = `${mod.endorsements} like`;
}
}
if (mod.downloads) {
const modDownloadsNumber = modTemplateCopy.getElementById("mod-downloads-number");
if (mod.downloads > 1) {
modDownloadsNumber.innerText = `${mod.downloads} downloads`;
} else {
modDownloadsNumber.innerText = `${mod.downloads} download`;
}
}
if (mod.summary) {
const modDescriptionText = modTemplateCopy.getElementById("mod-description");
modDescriptionText.innerText = mod.summary;
}
if (mod.pictureUrl) {
const modPicture = modTemplateCopy.getElementById("mod-icon");
modPicture.src = mod.pictureUrl;
}
if (mod.version && mod.updatedAt) {
const modVersionText = modTemplateCopy.getElementById("mod-version");
modVersionText.innerText = `V${mod.version} last updated on ${mod.updatedAt.slice(0, 10)}`;
}
const modUrl = `https://new.thunderstore.io/c/hollow-knight-silksong/p/${mod.author}/${mod.name}`;
const modLinkButton = modTemplateCopy.getElementById("external-link");
modLinkButton.href = modUrl;
modLinkButton.addEventListener("click", function (event) {
event.preventDefault();
const modLink = modLinkButton.href;
electronAPI.openExternalLink(modLink);
});
modDownloadButton = modTemplateCopy.getElementById("download-mod-button");
modDownloadButton.addEventListener("click", function (event) {
event.preventDefault();
const modDownloadLink = `https://thunderstore.io/package/download/${mod.author}/${mod.name}/${mod.version}`;
thunderstore.download(modDownloadLink, mod.modId);
});
thunderstoreModsContainer.appendChild(modTemplateCopy);
}
break;
case "general-settings": case "general-settings":
title.innerText = "Settings"; title.innerText = "Settings";
const settingsTemplateCopy = settingsTemplate.content.cloneNode(true); const settingsTemplateCopy = settingsTemplate.content.cloneNode(true);
@@ -330,6 +460,11 @@ async function welcomeNavigate() {
break; break;
case 2: case 2:
pageDiv.appendChild(styleTemplate.content.cloneNode(true));
toggleSelectedListButton("themes-menu", actualTheme[0]);
break;
case 3:
pageDiv.appendChild(nexusTemplate.content.cloneNode(true)); pageDiv.appendChild(nexusTemplate.content.cloneNode(true));
const nexusLink = document.getElementById("external-link"); const nexusLink = document.getElementById("external-link");
const nexusAPIForm = document.getElementById("nexus-api-form"); const nexusAPIForm = document.getElementById("nexus-api-form");
@@ -346,16 +481,7 @@ async function welcomeNavigate() {
setNexusAPI(); setNexusAPI();
break; break;
case 3:
pageDiv.appendChild(styleTemplate.content.cloneNode(true));
toggleSelectedListButton("themes-menu", actualTheme[0]);
break;
case 4: case 4:
pageDiv.appendChild(tutorialTemplate.content.cloneNode(true));
break;
case 5:
electronAPI.loadMainPage(); electronAPI.loadMainPage();
break; break;
} }
@@ -448,12 +574,12 @@ async function setBepinexVersion() {
async function searchInstalledMods() { async function searchInstalledMods() {
const searchInput = document.getElementById("search-input"); const searchInput = document.getElementById("search-input");
searchValueInstalled = searchInput.value; searchValueInstalled = searchInput.value;
await nexus.searchInstalled(searchValueInstalled, installedOffset, installedModsCount, installedSortFilter, installedSortOrder); await mods.searchInstalled(searchValueInstalled, installedOffset, installedModsCount, installedSortFilter, installedSortOrder);
await navigate("refresh"); await navigate("refresh");
} }
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
/////////////////////// NEXUS //////////////////////// //////////////// NEXUS / THUNDERSTORE ////////////////
async function verifyNexusAPI() { async function verifyNexusAPI() {
response = await nexus.verifyAPI(); response = await nexus.verifyAPI();
@@ -505,6 +631,20 @@ async function resetNexusAPI() {
} }
} }
async function searchThunderstoreMods() {
let searchInput = document.getElementById("search-input");
searchValueThunderstore = searchInput.value;
await thunderstore.search(searchValueThunderstore, thunderstoreOffset, thunderstoreModsCount, thunderstoreSortFilter, thunderstoreSortOrder);
await navigate("refresh");
searchInput = document.getElementById("search-input");
searchInput.value = searchValueThunderstore;
}
async function addOfflineMod() {
await mods.add();
searchInstalledMods();
}
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
//////////////// THEMES / SORT / LIST //////////////// //////////////// THEMES / SORT / LIST ////////////////
@@ -583,6 +723,8 @@ function setSortOrderButton() {
sortOrder = installedSortOrder; sortOrder = installedSortOrder;
} else if (oldPage == "mods-online") { } else if (oldPage == "mods-online") {
sortOrder = onlineSortOrder; sortOrder = onlineSortOrder;
} else if (oldPage == "mods-thunderstore") {
sortOrder = thunderstoreSortOrder;
} }
const sortOrderButton = document.getElementById("sort-order-image"); const sortOrderButton = document.getElementById("sort-order-image");
@@ -605,6 +747,9 @@ function changeSort(sortFilterParameter) {
} else if (oldPage == "mods-online") { } else if (oldPage == "mods-online") {
onlineSortFilter = sortFilterParameter; onlineSortFilter = sortFilterParameter;
searchNexusMods(); searchNexusMods();
} else if (oldPage == "mods-thunderstore") {
thunderstoreSortFilter = sortFilterParameter;
searchThunderstoreMods();
} }
} }
@@ -623,6 +768,13 @@ function inverseSort() {
onlineSortOrder = "ASC"; onlineSortOrder = "ASC";
} }
searchNexusMods(); searchNexusMods();
} else if (oldPage == "mods-thunderstore") {
if (thunderstoreSortOrder == "ASC") {
thunderstoreSortOrder = "DESC";
} else {
thunderstoreSortOrder = "ASC";
}
searchThunderstoreMods();
} }
} }
@@ -672,6 +824,23 @@ function showToast(message, type = "info", duration = 3000) {
electronAPI.onShowToast(showToast); electronAPI.onShowToast(showToast);
function showBanner(message) {
const bannerDiv = document.getElementById("banner-div");
const bannerText = document.getElementById("banner-text");
console.log(bannerDiv);
bannerText.innerHTML = message;
bannerDiv.classList.add("show");
}
electronAPI.onShowBanner(showBanner);
function hideBanner() {
const bannerDiv = document.getElementById("banner-div");
bannerDiv.classList.remove("show");
}
function changeModsPage(offsetChange) { function changeModsPage(offsetChange) {
if (oldPage == "mods-installed") { if (oldPage == "mods-installed") {
if (offsetChange == "min") { if (offsetChange == "min") {
@@ -701,6 +870,20 @@ function changeModsPage(offsetChange) {
lastOnlineOffset = onlineOffset; lastOnlineOffset = onlineOffset;
searchNexusMods(); searchNexusMods();
} }
} else if (oldPage == "mods-thunderstore") {
if (offsetChange == "min") {
thunderstoreOffset = 0;
} else if (offsetChange == "max") {
thunderstoreOffset = thunderstoreModsTotalCount;
} else {
thunderstoreOffset += thunderstoreModsCount * offsetChange;
thunderstoreOffset = clamp(thunderstoreOffset, 0, thunderstoreModsTotalCount);
}
thunderstoreOffset = Math.floor(thunderstoreOffset / 10) * 10;
if (lastThunderstoreOffset != thunderstoreOffset) {
lastThunderstoreOffset = thunderstoreOffset;
searchThunderstoreMods();
}
} }
} }

View File

@@ -49,6 +49,13 @@ body {
flex-direction: column; flex-direction: column;
} }
.sidebar-content {
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
}
.logo { .logo {
height: 70px; height: 70px;
display: flex; display: flex;
@@ -75,10 +82,6 @@ body {
height: 100px; height: 100px;
} }
.invert-color {
filter: invert(1);
}
.nav { .nav {
padding: 20px; padding: 20px;
} }
@@ -347,7 +350,7 @@ body {
background: var(--primary-color); background: var(--primary-color);
} }
.lace-pin-checkbox-container { .checkbox-container {
display: flex; display: flex;
align-items: center; align-items: center;
position: relative; position: relative;
@@ -356,13 +359,13 @@ body {
user-select: none; user-select: none;
} }
.lace-pin-checkbox-container input { .checkbox-container input {
opacity: 0; opacity: 0;
height: 0; height: 0;
width: 0; width: 0;
} }
.lace-pin-checkbox-container .checkmark { .checkbox-container .checkmark {
position: absolute; position: absolute;
left: 0; left: 0;
height: 30px; height: 30px;
@@ -373,27 +376,27 @@ body {
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.lace-pin-checkbox-container:hover .checkmark { .checkbox-container:hover .checkmark {
background: var(--darker-transparent-black); background: var(--darker-transparent-black);
border-color: var(--primary-color); border-color: var(--primary-color);
} }
.lace-pin-checkbox-container input:checked ~ .checkmark { .checkbox-container input:checked ~ .checkmark {
background: var(--primary-color); background: var(--primary-color);
border-color: var(--primary-color); border-color: var(--primary-color);
} }
.lace-pin-checkbox-container .checkmark:after { .checkbox-container .checkmark:after {
content: ""; content: "";
position: absolute; position: absolute;
display: none; display: none;
} }
.lace-pin-checkbox-container input:checked ~ .checkmark:after { .checkbox-container input:checked ~ .checkmark:after {
display: block; display: block;
} }
.lace-pin-checkbox-container .checkmark:after { .checkbox-container .checkmark:after {
left: 8px; left: 8px;
width: 10px; width: 10px;
height: 20px; height: 20px;
@@ -447,3 +450,19 @@ body {
.long-text { .long-text {
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.banner-div {
display: none;
align-items: center;
justify-content: space-between;
border: 1px solid var(--secondary-color);
background: var(--primary-color);
margin-bottom: 20px;
height: 64px;
border-radius: 12px;
padding: 10px;
}
.banner-div.show {
display: flex;
}

View File

@@ -22,7 +22,7 @@
<template id="welcome-template"> <template id="welcome-template">
<h1 class="title">Welcome to Silk Fly Launcher</h1> <h1 class="title">Welcome to Silk Fly Launcher</h1>
<div class="horizontal-div welcome-div"> <div class="horizontal-div welcome-div">
<p>Silk Fly Launcher is a launcher and mod manager for Silksong mods from Nexus, built with Electron.</p> <p>Silk Fly Launcher is a launcher and mod manager for Silksong mods from Nexus and Thunderstore, built with Electron.</p>
<p>Made with ♥ by <a href="" class="link" onclick="electronAPI.openExternalLink('https://github.com/Gabi-Zar')">GabiZar</a>.</p> <p>Made with ♥ by <a href="" class="link" onclick="electronAPI.openExternalLink('https://github.com/Gabi-Zar')">GabiZar</a>.</p>
</div> </div>
</template> </template>
@@ -47,22 +47,6 @@
</div> </div>
</template> </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">
<img class="nexus-check-image" id="nexus-check-image" src="assets/icons/cross.svg" />
<form class="horizontal-div input-form" id="nexus-api-form">
<input class="input" id="nexus-api-input" type="text" placeholder="Enter your nexus api" />
<button class="default-button" onclick="setNexusAPI()">Set</button>
</form>
<button class="default-button" onclick="resetNexusAPI()">Reset</button>
</div>
</template>
<template id="style-template"> <template id="style-template">
<h1 class="title">Chose the theme of the app</h1> <h1 class="title">Chose the theme of the app</h1>
<div class="horizontal-div welcome-div"> <div class="horizontal-div welcome-div">
@@ -82,9 +66,27 @@
</div> </div>
</template> </template>
<template id="tutorial-template"> <template id="nexus-template">
<h1 class="title">Tutorial to download mod from nexus</h1> <h1 class="title">Additional set up for nexus mods</h1>
<p>After clicking on "Download," a Nexus Mods window will pop up. Please log in, then click on "Download with Mod Manager."The Nexus window should close, and the mod will start downloading.</p> <h2>If you don't want to use nexus mods just click on next.</h2>
<br />
<br />
<p>
1. 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>
<br />
<div class="horizontal-div">
<img class="nexus-check-image" id="nexus-check-image" src="assets/icons/cross.svg" />
<form class="horizontal-div input-form" id="nexus-api-form">
<input class="input" id="nexus-api-input" type="text" placeholder="Enter your nexus api" />
<button class="default-button" onclick="setNexusAPI()">Set</button>
</form>
<button class="default-button" onclick="resetNexusAPI()">Reset</button>
</div>
<br />
<br />
<p>2. To download a mod from Nexus Mods you need to log in to your Nexus Mods account and then click on "Download with Mod Manager".</p>
</template> </template>
<script src="renderer.js"></script> <script src="renderer.js"></script>