Compare commits
No commits in common. "1.0.0" and "master" have entirely different histories.
25
.gitignore
vendored
25
.gitignore
vendored
|
@ -1,3 +1,24 @@
|
|||
node_modules/
|
||||
release/
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Lock files
|
||||
yarn.lock
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
|
||||
# Svelte Distribution
|
||||
public/build/
|
||||
|
||||
# Electron Distribution
|
||||
dist
|
||||
|
||||
# Project Build Automation Directory
|
||||
private
|
||||
|
||||
# Desktop Services Store on macOS
|
||||
.DS_Store
|
||||
|
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["svelte.svelte-vscode"]
|
||||
}
|
18
LICENSE
18
LICENSE
|
@ -44,7 +44,7 @@ To "convey" a work means any kind of propagation that enables other parties to m
|
|||
An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work.
|
||||
The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
|
||||
|
||||
|
@ -58,24 +58,24 @@ The Corresponding Source need not include anything that users can regenerate aut
|
|||
The Corresponding Source for a work in source code form is that same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
|
||||
All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
|
||||
No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
|
||||
You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
|
||||
You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
|
||||
|
||||
|
@ -88,7 +88,7 @@ You may convey a work based on the Program, or the modifications to produce it f
|
|||
A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
|
||||
You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
|
||||
|
||||
|
@ -113,7 +113,7 @@ The requirement to provide Installation Information does not include a requireme
|
|||
Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
|
||||
"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
|
||||
|
||||
|
@ -219,8 +219,8 @@ If you develop a new program, and you want it to be of the greatest possible use
|
|||
|
||||
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
EZPPLauncher
|
||||
Copyright (C) 2024 HorizonCode
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
|
|
18
README.md
18
README.md
|
@ -5,19 +5,23 @@ Welcome to the EZPPLauncher! A new way to connect to the EZPPFarm server.
|
|||
Just one click and you are ready to go!
|
||||
|
||||
## Installation
|
||||
|
||||
The Launcher is a "plug and play thing", download it, place it on the desktop and execute!
|
||||
|
||||
## Features
|
||||
|
||||
* Automatic osu! client updating before Launch
|
||||
* Account saving (soon)
|
||||
- Automatic osu! client updating before Launch
|
||||
- Custom osu! Logo in MainMenu
|
||||
- Relax misses and much more
|
||||
- Account saving
|
||||
|
||||
## Used Libraries
|
||||
## Build from source
|
||||
|
||||
* [Electron](https://www.npmjs.com/package/electron)
|
||||
* [custom-electron-titlebar](https://www.npmjs.com/package/custom-electron-titlebar)
|
||||
* [axios](https://www.npmjs.com/package/axios)
|
||||
* [jquery](https://www.npmjs.com/package/jquery)
|
||||
- clone repo
|
||||
- cd into the repo
|
||||
- use `npm i` to install all dependencies
|
||||
- use the buildscript `electron-pack` to build a executeable
|
||||
|
||||
## License
|
||||
|
||||
[AGPL-3.0](https://choosealicense.com/licenses/agpl-3.0/)
|
138
app.js
138
app.js
|
@ -1,138 +0,0 @@
|
|||
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
|
||||
const { setupTitlebar, attachTitlebarToWindow } = require('custom-electron-titlebar/main');
|
||||
const windowManager = require('./ui/windowManager');
|
||||
const osuUtil = require('./osuUtil');
|
||||
const config = require('./config');
|
||||
const fs = require('fs');
|
||||
|
||||
let tempOsuPath;
|
||||
|
||||
const run = () => {
|
||||
const gotTheLock = app.requestSingleInstanceLock()
|
||||
if (!gotTheLock) {
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
setupTitlebar();
|
||||
|
||||
let mainWindow;
|
||||
app.whenReady().then(() => {
|
||||
|
||||
mainWindow = createWindow();
|
||||
mainWindow.on('show', async () => {
|
||||
await doUpdateCheck(mainWindow);
|
||||
})
|
||||
app.on('activate', function () {
|
||||
if (BrowserWindow.getAllWindows().length === 0) mainWindow = createWindow();
|
||||
})
|
||||
app.on('window-all-closed', () => {
|
||||
app.quit()
|
||||
})
|
||||
ipcMain.handle('launch', async () => {
|
||||
const result = await osuUtil.startOsuWithDevServer(tempOsuPath, "ez-pp.farm", async () => {
|
||||
await doUpdateCheck(mainWindow);
|
||||
});
|
||||
return result;
|
||||
})
|
||||
ipcMain.on('do-update-check', async () => {
|
||||
await doUpdateCheck(mainWindow);
|
||||
})
|
||||
ipcMain.on('do-update', async () => {
|
||||
const osuPath = await config.get("osuPath", "");
|
||||
const isValid = await osuUtil.isValidOsuFolder(osuPath);
|
||||
if (osuPath.trim == "" || !isValid) {
|
||||
mainWindow.webContents.send('status_update', {
|
||||
type: "error",
|
||||
message: "Invalid osu! folder"
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (fs.existsSync(osuPath)) {
|
||||
tempOsuPath = osuPath;
|
||||
const osuConfig = await osuUtil.getLatestConfig(tempOsuPath);
|
||||
const lastVersion = await osuConfig.get("LastVersion");
|
||||
let releaseStream = "stable40";
|
||||
if (lastVersion.endsWith("cuttingedge"))
|
||||
releaseStream = "cuttingedge"
|
||||
else if (lastVersion.endsWith("beta"))
|
||||
releaseStream = "beta";
|
||||
|
||||
const releaseFiles = await osuUtil.getUpdateFiles(releaseStream);
|
||||
const filesToDownload = await osuUtil.filesThatNeedUpdate(tempOsuPath, releaseFiles);
|
||||
const downloadTask = await osuUtil.downloadUpdateFiles(osuPath, filesToDownload);
|
||||
downloadTask.on('completed', () => {
|
||||
mainWindow.webContents.send('status_update', {
|
||||
type: "update-complete"
|
||||
})
|
||||
});
|
||||
|
||||
} else
|
||||
mainWindow.webContents.send('status_update', {
|
||||
type: "error",
|
||||
message: "Invalid osu! folder"
|
||||
});
|
||||
})
|
||||
ipcMain.handle('set-osu-dir', async (event) => {
|
||||
const yes = await dialog.showOpenDialog({
|
||||
properties: ['openDirectory']
|
||||
})
|
||||
if (yes.filePaths.length <= 0)
|
||||
return undefined;
|
||||
const folderPath = yes.filePaths[0];
|
||||
const validOsuDir = await osuUtil.isValidOsuFolder(folderPath);
|
||||
|
||||
if (validOsuDir) await config.set("osuPath", folderPath);
|
||||
|
||||
return validOsuDir;
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function doUpdateCheck(window) {
|
||||
const osuPath = await config.get("osuPath", "");
|
||||
const isValid = await osuUtil.isValidOsuFolder(osuPath);
|
||||
if (osuPath.trim == "" || !isValid) {
|
||||
window.webContents.send('status_update', {
|
||||
type: "missing-folder"
|
||||
})
|
||||
return;
|
||||
}
|
||||
if (fs.existsSync(osuPath)) {
|
||||
tempOsuPath = osuPath;
|
||||
const osuConfig = await osuUtil.getLatestConfig(tempOsuPath);
|
||||
const lastVersion = await osuConfig.get("LastVersion");
|
||||
let releaseStream = "stable40";
|
||||
if (lastVersion.endsWith("cuttingedge"))
|
||||
releaseStream = "cuttingedge"
|
||||
else if (lastVersion.endsWith("beta"))
|
||||
releaseStream = "beta";
|
||||
|
||||
const releaseFiles = await osuUtil.getUpdateFiles(releaseStream);
|
||||
const filesToDownload = await osuUtil.filesThatNeedUpdate(tempOsuPath, releaseFiles);
|
||||
window.webContents.send('status_update', {
|
||||
type: filesToDownload.length > 0 ? "update-available" : "up-to-date"
|
||||
})
|
||||
} else
|
||||
window.webContents.send('status_update', {
|
||||
type: "missing-folder"
|
||||
})
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
// Create the browser window.
|
||||
const win = windowManager.createWindow(520, 350);
|
||||
|
||||
win.loadFile('./html/index.html');
|
||||
|
||||
attachTitlebarToWindow(win);
|
||||
win.webContents.setWindowOpenHandler(() => "deny");
|
||||
win.webContents.on('did-finish-load', function () {
|
||||
if (win.webContents.getZoomFactor() != 0.9)
|
||||
win.webContents.setZoomFactor(0.9)
|
||||
});
|
||||
|
||||
return win;
|
||||
}
|
||||
|
||||
run();
|
|
@ -1,4 +0,0 @@
|
|||
const appName = "EZPPLauncher"
|
||||
const appVersion = "1.0.0";
|
||||
|
||||
module.exports = { appName, appVersion };
|
23
assets/mdb.min.css
vendored
23
assets/mdb.min.css
vendored
File diff suppressed because one or more lines are too long
20
assets/mdb.min.js
vendored
20
assets/mdb.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
50
config.js
50
config.js
|
@ -1,50 +0,0 @@
|
|||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const configFolder = path.join(process.env['LOCALAPPDATA'], 'EZPPLauncher');
|
||||
if (!fs.existsSync(configFolder)) fs.mkdirSync(configFolder);
|
||||
|
||||
const configLocation = path.join(configFolder, `ezpplauncher.${path.basename(process.env['USERNAME'])}.cfg`);
|
||||
if (!fs.existsSync(configLocation)) fs.writeFileSync(configLocation, "");
|
||||
|
||||
|
||||
|
||||
async function get(key, defaultValue) {
|
||||
const fileStream = await fs.promises.readFile(configLocation, "utf-8");
|
||||
const lines = fileStream.split(/\r?\n/)
|
||||
for (const line of lines) {
|
||||
if (line.includes('=')) {
|
||||
const argsPair = line.split('=', 2);
|
||||
const keyname = argsPair[0]
|
||||
const value = argsPair[1];
|
||||
if (keyname == key) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
async function set(key, value) {
|
||||
const configValues = new Map();
|
||||
const fileStream = await fs.promises.readFile(configLocation, "utf-8");
|
||||
const lines = fileStream.split(/\r?\n/)
|
||||
for (const line of lines) {
|
||||
if (line.includes('=')) {
|
||||
const argsPair = line.split('=', 2);
|
||||
const keyname = argsPair[0]
|
||||
const value = argsPair[1];
|
||||
configValues.set(keyname, value);
|
||||
}
|
||||
}
|
||||
|
||||
configValues.set(key, value);
|
||||
|
||||
const arr = [];
|
||||
for (var [storkey, storvalue] of configValues.entries())
|
||||
arr.push(`${storkey}=${storvalue}`);
|
||||
|
||||
await fs.promises.writeFile(configLocation, arr.join('\n'));
|
||||
}
|
||||
|
||||
module.exports = { get, set }
|
4
electron/appInfo.js
Normal file
4
electron/appInfo.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
const appName = "EZPPLauncher";
|
||||
const appVersion = "2.1.7";
|
||||
|
||||
module.exports = { appName, appVersion };
|
53
electron/config.js
Normal file
53
electron/config.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
const sqlite = require("better-sqlite3");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
const configFolder = path.join(
|
||||
process.platform == "win32"
|
||||
? process.env["LOCALAPPDATA"]
|
||||
: process.env["HOME"],
|
||||
"EZPPLauncher",
|
||||
);
|
||||
if (!fs.existsSync(configFolder)) fs.mkdirSync(configFolder);
|
||||
|
||||
const dbFile = path.join(configFolder, "ezpplauncher.db");
|
||||
|
||||
const db = sqlite(dbFile);
|
||||
db.pragma("journal_mode = WAL");
|
||||
|
||||
db.exec(
|
||||
"CREATE TABLE IF NOT EXISTS config (configKey VARCHAR PRIMARY KEY, configValue VARCHAR);",
|
||||
);
|
||||
|
||||
const set = (key, value) => {
|
||||
db.prepare(
|
||||
`INSERT OR REPLACE INTO config (configKey, configValue) VALUES (?, ?)`,
|
||||
).run(key, value);
|
||||
};
|
||||
|
||||
const remove = (key) => {
|
||||
db.prepare(`DELETE FROM config WHERE configKey = ?`).run(key);
|
||||
};
|
||||
|
||||
const get = (
|
||||
key,
|
||||
) => {
|
||||
const result = db.prepare(
|
||||
"SELECT configKey key, configValue val FROM config WHERE key = ?",
|
||||
).get(key);
|
||||
return result ? result.val ?? undefined : undefined;
|
||||
};
|
||||
|
||||
const all = () => {
|
||||
const result = db.prepare(
|
||||
`SELECT configKey key, configValue val FROM config WHERE 1`,
|
||||
).all();
|
||||
return result ?? undefined;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
all,
|
||||
get,
|
||||
set,
|
||||
remove,
|
||||
};
|
11
electron/cryptoUtil.js
Normal file
11
electron/cryptoUtil.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
const cryptojs = require("crypto-js");
|
||||
|
||||
const encrypt = (string, salt) => {
|
||||
return cryptojs.AES.encrypt(string, salt).toString();
|
||||
};
|
||||
|
||||
const decrypt = (string, salt) => {
|
||||
return cryptojs.AES.decrypt(string, salt).toString(cryptojs.enc.Utf8);
|
||||
};
|
||||
|
||||
module.exports = { encrypt, decrypt };
|
20
electron/executeUtil.js
Normal file
20
electron/executeUtil.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
const childProcess = require("child_process");
|
||||
|
||||
const runFile = (folder, file, args, onExit) => {
|
||||
childProcess.execFile(file, args, {
|
||||
cwd: folder,
|
||||
}, (_err, _stdout, _stdin) => {
|
||||
if (onExit) onExit();
|
||||
});
|
||||
};
|
||||
|
||||
const runFileDetached = (folder, file, args) => {
|
||||
const subProcess = childProcess.spawn(file + (args ? " " + args : ""), {
|
||||
cwd: folder,
|
||||
detached: true,
|
||||
stdio: "ignore",
|
||||
});
|
||||
subProcess.unref();
|
||||
};
|
||||
|
||||
module.exports = { runFile, runFileDetached };
|
15
electron/fileUtil.js
Normal file
15
electron/fileUtil.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
const fs = require("fs");
|
||||
|
||||
function isWritable(filePath) {
|
||||
let fileAccess = false;
|
||||
try {
|
||||
fs.closeSync(fs.openSync(filePath, "r+"));
|
||||
fileAccess = true;
|
||||
} catch {
|
||||
}
|
||||
return fileAccess;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isWritable,
|
||||
};
|
13
electron/formattingUtil.js
Normal file
13
electron/formattingUtil.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
function formatBytes(bytes, decimals = 2) {
|
||||
if (!+bytes) return "0 B";
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))}${sizes[i]}`;
|
||||
}
|
||||
|
||||
module.exports = { formatBytes };
|
40
electron/hwidUtil.js
Normal file
40
electron/hwidUtil.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
const child_process = require("child_process");
|
||||
const options = { encoding: "ascii", windowsHide: true, timeout: 200 };
|
||||
const platforms = {
|
||||
win32: [
|
||||
"REG QUERY HKLM\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid",
|
||||
/MachineGuid\s+REG_SZ\s+(.*?)\s/,
|
||||
],
|
||||
darwin: [
|
||||
"ioreg -rd1 -c IOPlatformExpertDevice",
|
||||
/"IOPlatformUUID" = "(.*?)"/,
|
||||
],
|
||||
linux: [
|
||||
"cat /var/lib/dbus/machine-id /etc/machine-id 2> /dev/null || true",
|
||||
/^([\da-f]+)/,
|
||||
],
|
||||
};
|
||||
const crypto = require("crypto");
|
||||
|
||||
const defaultHWID = "recorderinthesandybridge";
|
||||
|
||||
/**
|
||||
* Returns machine hardware id.
|
||||
* Returns `undefined` if cannot determine.
|
||||
* @return {Promise<string>}
|
||||
*/
|
||||
function getHwId() {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const getter = platforms[process.platform];
|
||||
if (getter) {
|
||||
const result = getter[1].exec(child_process.execSync(getter[0], options));
|
||||
if (result) resolve(crypto.createHash("md5").update(result[1]).digest("hex"));
|
||||
}
|
||||
resolve(crypto.createHash("md5").update(defaultHWID).digest("hex"));
|
||||
} catch {
|
||||
resolve(crypto.createHash("md5").update(defaultHWID).digest("hex"));
|
||||
}
|
||||
})
|
||||
}
|
||||
exports.getHwId = getHwId;
|
21
electron/imageUtil.js
Normal file
21
electron/imageUtil.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
async function checkImageExists(url) {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "HEAD",
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0",
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
return false;
|
||||
}
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (!contentType) return false;
|
||||
return contentType.startsWith("image/");
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { checkImageExists };
|
44
electron/logging.js
Normal file
44
electron/logging.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
class Logger {
|
||||
constructor(directory) {
|
||||
this.directory = directory;
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
async init() {
|
||||
const filename = `${new Date().toISOString().replace(/:/g, "-")}.log`;
|
||||
this.logPath = path.join(this.directory, filename);
|
||||
}
|
||||
|
||||
async log(message) {
|
||||
if (this.logPath === undefined || this.enabled == false) {
|
||||
return;
|
||||
}
|
||||
if (!fs.existsSync(this.logPath)) {
|
||||
await fs.promises.mkdir(this.directory, { recursive: true });
|
||||
await fs.promises.writeFile(this.logPath, "");
|
||||
}
|
||||
const logMessage = `[${new Date().toISOString()}] LOG: ${message}`;
|
||||
await fs.promises.appendFile(this.logPath, `${logMessage}\n`);
|
||||
console.log(logMessage);
|
||||
}
|
||||
|
||||
async error(message, error) {
|
||||
if (this.logPath === undefined || this.enabled == false) {
|
||||
return;
|
||||
}
|
||||
if (!fs.existsSync(this.logPath)) {
|
||||
await fs.promises.mkdir(this.directory, { recursive: true });
|
||||
await fs.promises.writeFile(this.logPath, "");
|
||||
}
|
||||
const errorMessage = `[${
|
||||
new Date().toISOString()
|
||||
}] ERROR: ${message}\n${error.stack}`;
|
||||
await fs.promises.appendFile(this.logPath, `${errorMessage}\n`);
|
||||
console.error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Logger;
|
26
electron/netUtils.js
Normal file
26
electron/netUtils.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
const { exec } = require("child_process");
|
||||
|
||||
async function isNet8Installed() {
|
||||
return new Promise((resolve) => {
|
||||
exec("dotnet --list-runtimes", (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
if (stderr) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
const version = stdout.trim();
|
||||
for (const line of version.split('\n')) {
|
||||
if (line.startsWith("Microsoft.WindowsDesktop.App 8.")) {
|
||||
resolve(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
resolve(false);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { isNet8Installed };
|
478
electron/osuUtil.js
Normal file
478
electron/osuUtil.js
Normal file
|
@ -0,0 +1,478 @@
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
const EventEmitter = require("events");
|
||||
const { default: axios } = require("axios");
|
||||
const { runFile } = require("./executeUtil.js");
|
||||
|
||||
const checkUpdateURL =
|
||||
"https://osu.ppy.sh/web/check-updates.php?action=check&stream=";
|
||||
const ignoredOsuEntities = [
|
||||
"osu!auth.dll",
|
||||
];
|
||||
const gamemodes = {
|
||||
0: "osu!",
|
||||
1: "taiko",
|
||||
2: "catch",
|
||||
3: "mania",
|
||||
4: "osu!(rx)",
|
||||
5: "taiko(rx)",
|
||||
6: "catch(rx)",
|
||||
8: "osu!(ap)",
|
||||
};
|
||||
const osuEntities = [
|
||||
"avcodec-51.dll",
|
||||
"avformat-52.dll",
|
||||
"avutil-49.dll",
|
||||
"bass.dll",
|
||||
"bass_fx.dll",
|
||||
"collection.db",
|
||||
"d3dcompiler_47.dll",
|
||||
"libEGL.dll",
|
||||
"libGLESv2.dll",
|
||||
"Microsoft.Ink.dll",
|
||||
"OpenTK.dll",
|
||||
"osu!.cfg",
|
||||
"osu!.db",
|
||||
"osu!.exe",
|
||||
"osu!auth.dll",
|
||||
"osu!gameplay.dll",
|
||||
"osu!seasonal.dll",
|
||||
"osu!ui.dll",
|
||||
"presence.db",
|
||||
"pthreadGC2.dll",
|
||||
"scores.db",
|
||||
];
|
||||
|
||||
const ezppLauncherUpdateList = "https://ez-pp.farm/ezpplauncher";
|
||||
|
||||
async function isValidOsuFolder(path) {
|
||||
const allFiles = await fs.promises.readdir(path);
|
||||
let matches = 0;
|
||||
for (const file of allFiles) {
|
||||
if (osuEntities.includes(file)) matches = matches + 1;
|
||||
}
|
||||
return (Math.round((matches / osuEntities.length) * 100) >= 60);
|
||||
}
|
||||
|
||||
function getGlobalConfig(osuPath) {
|
||||
const configFileInfo = {
|
||||
name: "",
|
||||
path: "",
|
||||
get: async (key) => {
|
||||
if (!configFileInfo.path) {
|
||||
return "";
|
||||
}
|
||||
const fileStream = await fs.promises.readFile(
|
||||
configFileInfo.path,
|
||||
"utf-8",
|
||||
);
|
||||
const lines = fileStream.split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
if (line.includes(" = ")) {
|
||||
const argsPair = line.split(" = ", 2);
|
||||
const keyname = argsPair[0];
|
||||
const value = argsPair[1];
|
||||
if (keyname == key) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
const globalOsuConfig = path.join(osuPath, "osu!.cfg");
|
||||
if (fs.existsSync(globalOsuConfig)) {
|
||||
configFileInfo.name = "osu!.cfg";
|
||||
configFileInfo.path = globalOsuConfig;
|
||||
}
|
||||
return configFileInfo;
|
||||
}
|
||||
|
||||
function getUserConfig(osuPath) {
|
||||
const configFileInfo = {
|
||||
name: "",
|
||||
path: "",
|
||||
set: async (key, newValue) => {
|
||||
if (!configFileInfo.path) {
|
||||
return "";
|
||||
}
|
||||
const fileContents = [];
|
||||
const fileStream = await fs.promises.readFile(
|
||||
configFileInfo.path,
|
||||
"utf-8",
|
||||
);
|
||||
const lines = fileStream.split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
if (line.includes(" = ")) {
|
||||
const argsPair = line.split(" = ", 2);
|
||||
const keyname = argsPair[0];
|
||||
if (keyname == key) {
|
||||
fileContents.push(`${key} = ${newValue}`);
|
||||
} else {
|
||||
fileContents.push(line);
|
||||
}
|
||||
} else {
|
||||
fileContents.push(line);
|
||||
}
|
||||
}
|
||||
await fs.promises.writeFile(configFileInfo.path, fileContents.join("\n"));
|
||||
|
||||
return true;
|
||||
},
|
||||
get: async (key) => {
|
||||
if (!configFileInfo.path) {
|
||||
return "";
|
||||
}
|
||||
const fileStream = await fs.promises.readFile(
|
||||
configFileInfo.path,
|
||||
"utf-8",
|
||||
);
|
||||
const lines = fileStream.split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
if (line.includes(" = ")) {
|
||||
const argsPair = line.split(" = ", 2);
|
||||
const keyname = argsPair[0];
|
||||
const value = argsPair[1];
|
||||
if (keyname == key) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
const userOsuConfig = path.join(
|
||||
osuPath,
|
||||
`osu!.${process.env["USERNAME"]}.cfg`,
|
||||
);
|
||||
if (fs.existsSync(userOsuConfig)) {
|
||||
configFileInfo.name = `osu!.${process.env["USERNAME"]}.cfg`;
|
||||
configFileInfo.path = userOsuConfig;
|
||||
}
|
||||
return configFileInfo;
|
||||
}
|
||||
|
||||
async function getUpdateFiles(releaseStream) {
|
||||
const releaseData = await fetch(checkUpdateURL + releaseStream, {
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0",
|
||||
},
|
||||
});
|
||||
return releaseData.ok ? await releaseData.json() : undefined;
|
||||
}
|
||||
|
||||
async function getFilesThatNeedUpdate(osuPath, releaseStreamFiles) {
|
||||
const updateFiles = [];
|
||||
for (const updatePatch of releaseStreamFiles) {
|
||||
const fileName = updatePatch.filename;
|
||||
const fileHash = updatePatch.file_hash;
|
||||
|
||||
const fileOnDisk = path.join(osuPath, fileName);
|
||||
if (fs.existsSync(fileOnDisk)) {
|
||||
if (ignoredOsuEntities.includes(fileName)) continue;
|
||||
const fileHashOnDisk = crypto.createHash("md5").update(
|
||||
fs.readFileSync(fileOnDisk),
|
||||
).digest("hex");
|
||||
if (
|
||||
fileHashOnDisk.trim().toLowerCase() != fileHash.trim().toLowerCase()
|
||||
) {
|
||||
updateFiles.push(updatePatch);
|
||||
}
|
||||
} else updateFiles.push(updatePatch);
|
||||
}
|
||||
|
||||
return updateFiles;
|
||||
}
|
||||
|
||||
function downloadUpdateFiles(osuPath, updateFiles) {
|
||||
const eventEmitter = new EventEmitter();
|
||||
|
||||
const startDownload = async () => {
|
||||
for (const updatePatch of updateFiles) {
|
||||
try {
|
||||
const fileName = updatePatch.filename;
|
||||
const fileSize = updatePatch.filesize;
|
||||
const fileURL = updatePatch.url_full;
|
||||
|
||||
const axiosDownloadWithProgress = await axios.get(fileURL, {
|
||||
responseType: "stream",
|
||||
onDownloadProgress: (progressEvent) => {
|
||||
const { loaded, total } = progressEvent;
|
||||
eventEmitter.emit("data", {
|
||||
fileName,
|
||||
loaded,
|
||||
total,
|
||||
progress: Math.floor((loaded / total) * 100),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (fs.existsSync(path.join(osuPath, fileName))) {
|
||||
await fs.promises.rm(path.join(osuPath, fileName), {
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
await fs.promises.writeFile(
|
||||
path.join(osuPath, fileName),
|
||||
axiosDownloadWithProgress.data,
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
eventEmitter.emit("error", {
|
||||
fileName,
|
||||
error: err,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// wait until all files are downloaded
|
||||
return true;
|
||||
};
|
||||
|
||||
return {
|
||||
eventEmitter,
|
||||
startDownload,
|
||||
};
|
||||
}
|
||||
|
||||
function runOsuUpdater(osuPath, onExit) {
|
||||
const osuExecuteable = path.join(osuPath, "osu!.exe");
|
||||
runFile(osuPath, osuExecuteable, ["-repair"], onExit);
|
||||
}
|
||||
|
||||
function runOsuWithDevServer(osuPath, serverDomain, onExit) {
|
||||
const osuExecuteable = path.join(osuPath, "osu!.exe");
|
||||
runFile(osuPath, osuExecuteable, ["-devserver", serverDomain], onExit);
|
||||
}
|
||||
|
||||
async function getEZPPLauncherUpdateFiles(osuPath) {
|
||||
const filesToDownload = [];
|
||||
const updateFilesRequest = await fetch(ezppLauncherUpdateList, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0",
|
||||
},
|
||||
});
|
||||
const updateFiles = await updateFilesRequest.json();
|
||||
for (const updateFile of updateFiles) {
|
||||
const filePath = path.join(
|
||||
osuPath,
|
||||
...updateFile.folder.split("/"),
|
||||
updateFile.name,
|
||||
);
|
||||
if (fs.existsSync(filePath)) {
|
||||
const fileHash = updateFile.md5.toLowerCase();
|
||||
const localFileHash = crypto.createHash("md5").update(
|
||||
fs.readFileSync(filePath),
|
||||
).digest("hex").toLowerCase();
|
||||
if (fileHash !== localFileHash) {
|
||||
filesToDownload.push(updateFile);
|
||||
}
|
||||
} else {
|
||||
filesToDownload.push(updateFile);
|
||||
}
|
||||
}
|
||||
return [filesToDownload, updateFiles];
|
||||
}
|
||||
|
||||
async function downloadEZPPLauncherUpdateFiles(osuPath, updateFiles, allFiles) {
|
||||
const eventEmitter = new EventEmitter();
|
||||
|
||||
const startDownload = async () => {
|
||||
//NOTE: delete files that are not in the updateFiles array
|
||||
const foldersToPrune = allFiles.map((file) =>
|
||||
path.dirname(path.join(osuPath, ...file.folder.split("/"), file.name))
|
||||
).filter((folder, index, self) => self.indexOf(folder) === index);
|
||||
for (const pruneFolder of foldersToPrune) {
|
||||
//NOTE: check if the folder is not the osu root folder.
|
||||
if (path.basename(pruneFolder) == "osu!") {
|
||||
continue;
|
||||
}
|
||||
if (fs.existsSync(pruneFolder)) {
|
||||
for (const files of await fs.promises.readdir(pruneFolder)) {
|
||||
const filePath = path.join(pruneFolder, files);
|
||||
const validFolder = allFiles.find((file) =>
|
||||
path.dirname(filePath).endsWith(file.folder)
|
||||
);
|
||||
if (!validFolder) {
|
||||
if (
|
||||
allFiles.find((file) => file.name == path.basename(filePath)) ===
|
||||
undefined
|
||||
) {
|
||||
eventEmitter.emit("data", {
|
||||
fileName: path.basename(filePath),
|
||||
});
|
||||
try {
|
||||
await fs.promises.rm(filePath, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const updateFile of updateFiles) {
|
||||
try {
|
||||
const filePath = path.join(
|
||||
osuPath,
|
||||
...updateFile.folder.split("/"),
|
||||
updateFile.name,
|
||||
);
|
||||
const folder = path.dirname(filePath);
|
||||
if (!fs.existsSync(folder)) {
|
||||
await fs.promises.mkdir(folder, { recursive: true });
|
||||
}
|
||||
const axiosDownloadWithProgress = await axios.get(updateFile.url, {
|
||||
responseType: "stream",
|
||||
onDownloadProgress: (progressEvent) => {
|
||||
const fileSize = updateFile.size;
|
||||
const { loaded } = progressEvent;
|
||||
eventEmitter.emit("data", {
|
||||
fileName: path.basename(filePath),
|
||||
loaded,
|
||||
total: fileSize,
|
||||
progress: Math.floor((loaded / fileSize) * 100),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
await fs.promises.rm(filePath, {
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
await fs.promises.writeFile(
|
||||
filePath,
|
||||
axiosDownloadWithProgress.data,
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
eventEmitter.emit("error", {
|
||||
fileName: path.basename(filePath),
|
||||
error: err,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
eventEmitter,
|
||||
startDownload,
|
||||
};
|
||||
}
|
||||
|
||||
async function replaceUIFiles(osuPath, revert) {
|
||||
if (!revert) {
|
||||
const ezppUIFile = path.join(osuPath, "EZPPLauncher", "ezpp!ui.dll");
|
||||
const oldOsuUIFile = path.join(osuPath, "osu!ui.dll");
|
||||
const ezppGameplayFile = path.join(
|
||||
osuPath,
|
||||
"EZPPLauncher",
|
||||
"ezpp!gameplay.dll",
|
||||
);
|
||||
const oldOsuGameplayFile = path.join(osuPath, "osu!gameplay.dll");
|
||||
|
||||
await fs.promises.rename(
|
||||
oldOsuUIFile,
|
||||
path.join(osuPath, "osu!ui.dll.bak"),
|
||||
);
|
||||
await fs.promises.rename(ezppUIFile, oldOsuUIFile);
|
||||
|
||||
await fs.promises.rename(
|
||||
oldOsuGameplayFile,
|
||||
path.join(osuPath, "osu!gameplay.dll.bak"),
|
||||
);
|
||||
await fs.promises.rename(ezppGameplayFile, oldOsuGameplayFile);
|
||||
} else {
|
||||
const oldOsuUIFile = path.join(osuPath, "osu!ui.dll");
|
||||
const ezppUIFile = path.join(osuPath, "EZPPLauncher", "ezpp!ui.dll");
|
||||
const oldOsuGameplayFile = path.join(osuPath, "osu!gameplay.dll");
|
||||
const ezppGameplayFile = path.join(
|
||||
osuPath,
|
||||
"EZPPLauncher",
|
||||
"ezpp!gameplay.dll",
|
||||
);
|
||||
|
||||
await fs.promises.rename(oldOsuUIFile, ezppUIFile);
|
||||
await fs.promises.rename(
|
||||
path.join(osuPath, "osu!ui.dll.bak"),
|
||||
oldOsuUIFile,
|
||||
);
|
||||
|
||||
await fs.promises.rename(oldOsuGameplayFile, ezppGameplayFile);
|
||||
await fs.promises.rename(
|
||||
path.join(osuPath, "osu!gameplay.dll.bak"),
|
||||
oldOsuGameplayFile,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function findOsuInstallation() {
|
||||
const regedit = require("regedit-rs");
|
||||
|
||||
const osuLocationFromDefaultIcon =
|
||||
"HKLM\\SOFTWARE\\Classes\\osu\\DefaultIcon";
|
||||
const osuKey = regedit.listSync(osuLocationFromDefaultIcon);
|
||||
if (osuKey[osuLocationFromDefaultIcon].exists) {
|
||||
const key = osuKey[osuLocationFromDefaultIcon].values[""];
|
||||
let value = key.value;
|
||||
value = value.substring(1, value.length - 3);
|
||||
return path.dirname(value.trim());
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function updateOsuConfigHashes(osuPath) {
|
||||
const osuCfg = path.join(osuPath, "osu!.cfg");
|
||||
const fileStream = await fs.promises.readFile(osuCfg, "utf-8");
|
||||
const lines = fileStream.split(/\r?\n/);
|
||||
const newLines = [];
|
||||
for (const line of lines) {
|
||||
if (line.includes(" = ")) {
|
||||
const argsPair = line.split(" = ", 2);
|
||||
const key = argsPair[0];
|
||||
const value = argsPair[1];
|
||||
|
||||
if (key.startsWith("h_")) {
|
||||
const fileName = key.substring(2, key.length);
|
||||
const filePath = path.join(osuPath, fileName);
|
||||
if (!fs.existsSync(filePath)) continue;
|
||||
const binaryFileContents = await fs.promises.readFile(filePath);
|
||||
const existingFileMD5 = crypto.createHash("md5").update(
|
||||
binaryFileContents,
|
||||
).digest("hex");
|
||||
if (value == existingFileMD5) newLines.push(line);
|
||||
else newLines.push(`${key} = ${existingFileMD5}`);
|
||||
} else if (line.startsWith("u_UpdaterAutoStart")) {
|
||||
newLines.push(`${key} = 0`);
|
||||
} else {
|
||||
newLines.push(line);
|
||||
}
|
||||
} else {
|
||||
newLines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
await fs.promises.writeFile(osuCfg, newLines.join("\n"), "utf-8");
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isValidOsuFolder,
|
||||
getUserConfig,
|
||||
getGlobalConfig,
|
||||
getUpdateFiles,
|
||||
getFilesThatNeedUpdate,
|
||||
downloadUpdateFiles,
|
||||
runOsuWithDevServer,
|
||||
replaceUIFiles,
|
||||
findOsuInstallation,
|
||||
updateOsuConfigHashes,
|
||||
runOsuUpdater,
|
||||
getEZPPLauncherUpdateFiles,
|
||||
downloadEZPPLauncherUpdateFiles,
|
||||
gamemodes,
|
||||
};
|
71
electron/richPresence.js
Normal file
71
electron/richPresence.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
const DiscordRPC = require("discord-auto-rpc");
|
||||
const { appName, appVersion } = require("./appInfo.js");
|
||||
|
||||
const clientId = "1032772293220384808";
|
||||
|
||||
/** @type {DiscordRPC.AutoClient} */
|
||||
let richPresence;
|
||||
|
||||
let intervalId;
|
||||
|
||||
let currentStatus = {
|
||||
details: " ",
|
||||
state: "Idle in Launcher...",
|
||||
startTimestamp: new Date(),
|
||||
largeImageKey: "ezppfarm",
|
||||
largeImageText: `${appName} ${appVersion}`,
|
||||
smallImageKey: " ",
|
||||
smallImageText: " ",
|
||||
buttons: [
|
||||
{
|
||||
label: "Download the Launcher",
|
||||
url: "https://git.ez-pp.farm/EZPPFarm/EZPPLauncher/releases/latest",
|
||||
},
|
||||
{
|
||||
label: "Join EZPPFarm",
|
||||
url: "https://ez-pp.farm/discord",
|
||||
},
|
||||
],
|
||||
instance: false,
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
connect: () => {
|
||||
if (!richPresence) {
|
||||
richPresence = new DiscordRPC.AutoClient({ transport: "ipc" });
|
||||
richPresence.endlessLogin({ clientId });
|
||||
richPresence.once("ready", () => {
|
||||
console.log(
|
||||
"connected presence with user " + richPresence.user.username,
|
||||
);
|
||||
richPresence.setActivity(currentStatus);
|
||||
intervalId = setInterval(() => {
|
||||
richPresence.setActivity(currentStatus);
|
||||
}, 2500);
|
||||
});
|
||||
}
|
||||
},
|
||||
disconnect: async () => {
|
||||
if (richPresence) {
|
||||
clearInterval(intervalId);
|
||||
await richPresence.clearActivity();
|
||||
await richPresence.destroy();
|
||||
richPresence = null;
|
||||
}
|
||||
},
|
||||
updateStatus: ({ state, details, largeImageKey }) => {
|
||||
currentStatus.state = state ?? " ";
|
||||
currentStatus.details = details ?? " ";
|
||||
currentStatus.largeImageKey = largeImageKey ?? "ezppfarm";
|
||||
},
|
||||
updateUser: ({ username, id }) => {
|
||||
currentStatus.smallImageKey = id ? `https://a.ez-pp.farm/${id}` : " ";
|
||||
currentStatus.smallImageText = username ?? " ";
|
||||
},
|
||||
update: () => {
|
||||
if (richPresence && richPresence.user) {
|
||||
richPresence.setActivity(currentStatus);
|
||||
}
|
||||
},
|
||||
hasPresence: () => richPresence != undefined,
|
||||
};
|
30
electron/updateCheck.js
Normal file
30
electron/updateCheck.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
const semver = require("semver");
|
||||
const { appVersion } = require("./appInfo");
|
||||
|
||||
const repoApiUrl =
|
||||
"https://git.ez-pp.farm/api/v1/repos/EZPPFarm/EZPPLauncher/releases?limit=1";
|
||||
|
||||
const releasesUrl =
|
||||
"https://git.ez-pp.farm/EZPPFarm/EZPPLauncher/releases/latest";
|
||||
|
||||
module.exports = {
|
||||
updateAvailable: async () => {
|
||||
try {
|
||||
const latestRelease = await fetch(repoApiUrl, {
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0",
|
||||
},
|
||||
});
|
||||
const json = await latestRelease.json();
|
||||
if (json.length <= 0) return false;
|
||||
return {
|
||||
update: semver.lt(appVersion, json[0].tag_name),
|
||||
release: json[0],
|
||||
};
|
||||
} catch (err) {
|
||||
return { update: false };
|
||||
}
|
||||
},
|
||||
releasesUrl,
|
||||
};
|
|
@ -1,18 +0,0 @@
|
|||
const childProcess = require('child_process');
|
||||
module.exports = {
|
||||
runFile: (folder, file, args, onExit) => {
|
||||
childProcess.execFile(file, args, {
|
||||
cwd: folder
|
||||
}, (_err, _stdout, _stderr) => {
|
||||
onExit();
|
||||
});
|
||||
},
|
||||
runFileDetached: (folder, file, args) => {
|
||||
const subprocess = childProcess.spawn(file + " " + args, {
|
||||
cwd: folder,
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
})
|
||||
subprocess.unref()
|
||||
}
|
||||
}
|
15
fileUtil.js
15
fileUtil.js
|
@ -1,15 +0,0 @@
|
|||
const fs = require('fs');
|
||||
|
||||
async function existsAsync(filePath) {
|
||||
return new Promise(function (resolve, _reject) {
|
||||
fs.stat(filePath, function (err, _stat) {
|
||||
if (err == null) {
|
||||
resolve(true)
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { existsAsync };
|
|
@ -1,62 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>EZPPLauncher</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/png" href="../assets/logo.png" />
|
||||
<link href="../assets/mdb.min.css" rel="stylesheet" />
|
||||
<style>
|
||||
* {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="fixed-sn mdb-skin-custom" data-spy="scroll" data-target="#scrollspy" data-offset="15"
|
||||
oncontextmenu="return false;">
|
||||
<main>
|
||||
<div class="noselect">
|
||||
<div class="position-relative overflow-hidden p-3 p-md-5 m-md-3 text-center text-lg-end d-flex align-items-center justify-content-center"
|
||||
style="border-radius: 0.5em;">
|
||||
<div class="position-relative overflow-hidden p-3 p-md-5 m-md-3">
|
||||
<div class="container py-2 h-100">
|
||||
<div class="row d-flex justify-content-center align-items-center h-100">
|
||||
<div class="col col-xl-10">
|
||||
<div class="card" style="border-radius: 1rem;">
|
||||
<div class="row g-0">
|
||||
<div class="card-body p-4 p-lg-5 text-black">
|
||||
<div class="d-flex align-items-center mb-2 pb-1 text-white">
|
||||
<span class="h1 fw-bold mb-0">EZPPLauncher</span>
|
||||
</div>
|
||||
<h5 class="fw-lighter fs-5 mb-3 pb-3 text-white text-start"
|
||||
style="letter-spacing: 1px;">
|
||||
Launch osu! with connection to the EZPPFarm server
|
||||
</h5>
|
||||
<div class="pt-1 mb-4">
|
||||
<button id="launch-btn" class="btn btn-primary btn-lg btn-block"
|
||||
type="button" style="background-color:#d6016f" disabled>Looking for
|
||||
updates...</button>
|
||||
</div>
|
||||
<button class="btn btn-dark btn-sm float-start" id="account-btn" disabled>
|
||||
set account
|
||||
</button>
|
||||
<button class="btn btn-dark btn-sm float-end" id="folder-btn">
|
||||
set osu! directory
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
<script type="text/javascript" src="../assets/mdb.min.js"></script>
|
||||
|
||||
</html>
|
889
main.js
Normal file
889
main.js
Normal file
|
@ -0,0 +1,889 @@
|
|||
// Modules to control application life and create native browser window
|
||||
const { app, BrowserWindow, Menu, ipcMain, dialog, shell } = require(
|
||||
"electron",
|
||||
);
|
||||
|
||||
const path = require("path");
|
||||
const serve = require("electron-serve");
|
||||
const loadURL = serve({ directory: "public" });
|
||||
const config = require("./electron/config");
|
||||
const { setupTitlebar, attachTitlebarToWindow } = require(
|
||||
"custom-electron-titlebar/main",
|
||||
);
|
||||
const {
|
||||
isValidOsuFolder,
|
||||
getUpdateFiles,
|
||||
getGlobalConfig,
|
||||
getFilesThatNeedUpdate,
|
||||
downloadUpdateFiles,
|
||||
getUserConfig,
|
||||
runOsuWithDevServer,
|
||||
replaceUIFiles,
|
||||
findOsuInstallation,
|
||||
runOsuUpdater,
|
||||
gamemodes,
|
||||
getEZPPLauncherUpdateFiles,
|
||||
downloadEZPPLauncherUpdateFiles,
|
||||
} = require("./electron/osuUtil");
|
||||
const { formatBytes } = require("./electron/formattingUtil");
|
||||
const windowName = require("get-window-by-name");
|
||||
const fs = require("fs");
|
||||
const { runFileDetached } = require("./electron/executeUtil");
|
||||
const richPresence = require("./electron/richPresence");
|
||||
const cryptUtil = require("./electron/cryptoUtil");
|
||||
const { getHwId } = require("./electron/hwidUtil");
|
||||
const { appName, appVersion } = require("./electron/appInfo");
|
||||
const { updateAvailable, releasesUrl } = require("./electron/updateCheck");
|
||||
const fkill = require("fkill");
|
||||
const { checkImageExists } = require("./electron/imageUtil");
|
||||
const { isNet8Installed } = require("./electron/netUtils");
|
||||
const Logger = require("./electron/logging");
|
||||
const { isWritable } = require("./electron/fileUtil");
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
let mainWindow;
|
||||
let osuCheckInterval;
|
||||
let userOsuPath;
|
||||
let osuLoaded = false;
|
||||
let patch = false;
|
||||
let logger = new Logger(path.join(
|
||||
process.platform == "win32"
|
||||
? process.env["LOCALAPPDATA"]
|
||||
: process.env["HOME"],
|
||||
"EZPPLauncher",
|
||||
"logs",
|
||||
));
|
||||
|
||||
let currentUser = undefined;
|
||||
|
||||
function isDev() {
|
||||
return !app.isPackaged;
|
||||
}
|
||||
|
||||
function startOsuStatus() {
|
||||
osuCheckInterval = setInterval(async () => {
|
||||
const osuWindowTitle = windowName.getWindowText("osu!.exe");
|
||||
if (osuWindowTitle.length < 0) {
|
||||
return;
|
||||
}
|
||||
const firstInstance = osuWindowTitle[0];
|
||||
|
||||
if (firstInstance) {
|
||||
if (!osuLoaded) {
|
||||
osuLoaded = true;
|
||||
|
||||
try {
|
||||
const currentUserInfo = await fetch(
|
||||
`https://api.ez-pp.farm/get_player_info?name=${currentUser.username}&scope=info`,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0",
|
||||
},
|
||||
},
|
||||
);
|
||||
const currentUserInfoJson = await currentUserInfo.json();
|
||||
if (
|
||||
"player" in currentUserInfoJson &&
|
||||
currentUserInfoJson.player != null
|
||||
) {
|
||||
if (
|
||||
"info" in currentUserInfoJson.player &&
|
||||
currentUserInfoJson.player.info != null
|
||||
) {
|
||||
const id = currentUserInfoJson.player.info.id;
|
||||
const username = currentUserInfoJson.player.info.name;
|
||||
richPresence.updateUser({
|
||||
id,
|
||||
username,
|
||||
});
|
||||
richPresence.update();
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (patch) {
|
||||
const patcherExecuteable = path.join(
|
||||
userOsuPath,
|
||||
"EZPPLauncher",
|
||||
"patcher",
|
||||
"osu!.patcher.exe",
|
||||
);
|
||||
if (fs.existsSync(patcherExecuteable)) {
|
||||
runFileDetached(userOsuPath, patcherExecuteable);
|
||||
}
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
const windowTitle = firstInstance.processTitle;
|
||||
lastOsuStatus = windowTitle;
|
||||
const currentStatusRequest = await fetch(
|
||||
"https://api.ez-pp.farm/v1/get_player_status?name=" +
|
||||
currentUser.username,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0",
|
||||
},
|
||||
},
|
||||
);
|
||||
const currentStatus = await currentStatusRequest.json();
|
||||
|
||||
if (!("player_status" in currentStatus)) return;
|
||||
if (!("status" in currentStatus.player_status)) return;
|
||||
|
||||
const currentMode = currentStatus.player_status.status.mode;
|
||||
const currentModeString = gamemodes[currentMode];
|
||||
|
||||
const currentInfoRequest = await fetch(
|
||||
"https://api.ez-pp.farm/v1/get_player_info?name=" +
|
||||
currentUser.username + "&scope=all",
|
||||
{
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0",
|
||||
},
|
||||
},
|
||||
);
|
||||
const currentInfo = await currentInfoRequest.json();
|
||||
let currentUsername = currentInfo.player.info.name;
|
||||
const currentId = currentInfo.player.info.id;
|
||||
const currentStats = currentInfo.player.stats[currentMode];
|
||||
|
||||
currentUsername += ` (#${currentStats.rank})`;
|
||||
|
||||
let largeImageKey = "ezppfarm";
|
||||
let details = "Idle...";
|
||||
let infoText = currentStatus.player_status.status.info_text.length > 0
|
||||
? currentStatus.player_status.status.info_text
|
||||
: " ";
|
||||
if (
|
||||
"beatmap" in currentStatus.player_status.status &&
|
||||
currentStatus.player_status.status.beatmap !== null
|
||||
) {
|
||||
const setId = currentStatus.player_status.status.beatmap.set_id;
|
||||
if (setId) {
|
||||
const coverImage =
|
||||
`https://assets.ppy.sh/beatmaps/${setId}/covers/list@2x.jpg`;
|
||||
if (
|
||||
checkImageExists(coverImage)
|
||||
) {
|
||||
largeImageKey = coverImage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (currentStatus.player_status.status.action) {
|
||||
case 1:
|
||||
details = "AFK...";
|
||||
infoText = " ";
|
||||
largeImageKey = "ezppfarm";
|
||||
break;
|
||||
case 2:
|
||||
details = "Playing...";
|
||||
break;
|
||||
case 3:
|
||||
details = "Editing...";
|
||||
break;
|
||||
case 4:
|
||||
details = "Modding...";
|
||||
break;
|
||||
case 5:
|
||||
details = "Multiplayer: Selecting a Beatmap...";
|
||||
infoText = " ";
|
||||
largeImageKey = "ezppfarm";
|
||||
break;
|
||||
case 6:
|
||||
details = "Watching...";
|
||||
break;
|
||||
case 8:
|
||||
details = "Testing...";
|
||||
break;
|
||||
case 9:
|
||||
details = "Submitting...";
|
||||
largeImageKey = "ezppfarm";
|
||||
break;
|
||||
case 11:
|
||||
details = "Multiplayer: Idle...";
|
||||
infoText = " ";
|
||||
largeImageKey = "ezppfarm";
|
||||
break;
|
||||
case 12:
|
||||
details = "Multiplayer: Playing...";
|
||||
break;
|
||||
case 13:
|
||||
details = "Browsing osu!direct...";
|
||||
infoText = " ";
|
||||
largeImageKey = "ezppfarm";
|
||||
break;
|
||||
}
|
||||
|
||||
details = `[${currentModeString}] ${details}`;
|
||||
|
||||
richPresence.updateUser({
|
||||
username: currentUsername,
|
||||
id: currentId,
|
||||
});
|
||||
|
||||
richPresence.updateStatus({
|
||||
details,
|
||||
state: infoText,
|
||||
largeImageKey,
|
||||
});
|
||||
|
||||
richPresence.update();
|
||||
}
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
function stopOsuStatus() {
|
||||
clearInterval(osuCheckInterval);
|
||||
}
|
||||
|
||||
function registerIPCPipes() {
|
||||
ipcMain.handle("ezpplauncher:login", async (e, args) => {
|
||||
let hwid = "";
|
||||
try {
|
||||
hwid = await getHwId();
|
||||
} catch (err) {
|
||||
logger.error(`Failed to get HWID.`, err);
|
||||
return {
|
||||
code: 500,
|
||||
message: "Failed to get HWID.",
|
||||
};
|
||||
}
|
||||
const timeout = new AbortController();
|
||||
const timeoutId = setTimeout(() => timeout.abort(), 1000 * 10);
|
||||
logger.log(`Logging in with user ${args.username}...`);
|
||||
try {
|
||||
const fetchResult = await fetch("https://ez-pp.farm/login/check", {
|
||||
signal: timeout.signal,
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
username: args.username,
|
||||
password: args.password,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0",
|
||||
},
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (fetchResult.ok) {
|
||||
const result = await fetchResult.json();
|
||||
if ("user" in result) {
|
||||
if (args.saveCredentials) {
|
||||
config.set("username", args.username);
|
||||
config.set("password", cryptUtil.encrypt(args.password, hwid));
|
||||
}
|
||||
currentUser = args;
|
||||
config.remove("guest");
|
||||
logger.log(`Logged in as user ${args.username}!`);
|
||||
} else logger.log(`Login failed for user ${args.username}.`);
|
||||
return result;
|
||||
}
|
||||
logger.log(
|
||||
`Login failed for user ${args.username}.\nResponse:\n${await fetchResult
|
||||
.text()}`,
|
||||
);
|
||||
return {
|
||||
code: 500,
|
||||
message: "Something went wrong while logging you in.",
|
||||
};
|
||||
} catch (err) {
|
||||
logger.error("Error while logging in:", err);
|
||||
return {
|
||||
code: 500,
|
||||
message: "Something went wrong while logging you in.",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("ezpplauncher:autologin-active", async (e) => {
|
||||
const username = config.get("username");
|
||||
const password = config.get("password");
|
||||
const guest = config.get("guest");
|
||||
if (guest != undefined) return true;
|
||||
return username != undefined && password != undefined;
|
||||
});
|
||||
|
||||
ipcMain.handle("ezpplauncher:autologin", async (e) => {
|
||||
const hwid = await getHwId();
|
||||
const username = config.get("username");
|
||||
const guest = config.get("guest");
|
||||
if (guest) return { code: 200, message: "Login as guest", guest: true };
|
||||
if (username == undefined) {
|
||||
return { code: 200, message: "No autologin" };
|
||||
}
|
||||
const password = cryptUtil.decrypt(config.get("password"), hwid);
|
||||
if (username == undefined || password == undefined) {
|
||||
return { code: 200, message: "No autologin" };
|
||||
}
|
||||
const timeout = new AbortController();
|
||||
const timeoutId = setTimeout(() => timeout.abort(), 8000);
|
||||
logger.log(`Logging in with user ${username}...`);
|
||||
try {
|
||||
const fetchResult = await fetch("https://ez-pp.farm/login/check", {
|
||||
signal: timeout.signal,
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password: password,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0",
|
||||
},
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (fetchResult.ok) {
|
||||
const result = await fetchResult.json();
|
||||
if ("user" in result) {
|
||||
currentUser = {
|
||||
username: username,
|
||||
password: password,
|
||||
};
|
||||
logger.log(`Logged in as user ${username}!`);
|
||||
} else logger.log(`Login failed for user ${username}.`);
|
||||
|
||||
return result;
|
||||
} else {
|
||||
config.remove("password");
|
||||
}
|
||||
logger.log(
|
||||
`Login failed for user ${username}.\nResponse:\n${await fetchResult
|
||||
.text()}`,
|
||||
);
|
||||
return {
|
||||
code: 500,
|
||||
message: "Something went wrong while logging you in.",
|
||||
};
|
||||
} catch (err) {
|
||||
logger.error("Error while logging in:", err);
|
||||
return {
|
||||
code: 500,
|
||||
message: "Something went wrong while logging you in.",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("ezpplauncher:guestlogin", (e) => {
|
||||
config.remove("username");
|
||||
config.remove("password");
|
||||
config.set("guest", "1");
|
||||
currentUser = undefined;
|
||||
logger.log("Logged in as guest user.");
|
||||
});
|
||||
|
||||
ipcMain.handle("ezpplauncher:logout", (e) => {
|
||||
config.remove("username");
|
||||
config.remove("password");
|
||||
config.remove("guest");
|
||||
currentUser = undefined;
|
||||
logger.log("Loging out.");
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle("ezpplauncher:settings", async (e) => {
|
||||
return config.all();
|
||||
});
|
||||
|
||||
ipcMain.handle("ezpplauncher:setting-update", async (e, args) => {
|
||||
for (const key of Object.keys(args)) {
|
||||
const value = args[key];
|
||||
|
||||
if (key == "presence") {
|
||||
if (!value) richPresence.disconnect();
|
||||
else richPresence.connect();
|
||||
}
|
||||
|
||||
if (key == "logging") {
|
||||
logger.enabled = value;
|
||||
}
|
||||
|
||||
if (typeof value == "boolean") {
|
||||
config.set(key, value ? "true" : "false");
|
||||
} else {
|
||||
config.set(key, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("ezpplauncher:detect-folder", async (e) => {
|
||||
const detected = await findOsuInstallation();
|
||||
if (detected && await isValidOsuFolder(detected)) {
|
||||
mainWindow.webContents.send("ezpplauncher:alert", {
|
||||
type: "success",
|
||||
message: "osu! path successfully saved!",
|
||||
});
|
||||
config.set("osuPath", detected);
|
||||
}
|
||||
return config.all();
|
||||
});
|
||||
|
||||
ipcMain.handle("ezpplauncher:set-folder", async (e) => {
|
||||
const folderResult = await dialog.showOpenDialog({
|
||||
title: "Select osu! installation directory",
|
||||
properties: ["openDirectory"],
|
||||
});
|
||||
if (!folderResult.canceled) {
|
||||
const folder = folderResult.filePaths[0];
|
||||
if (await isValidOsuFolder(folder)) {
|
||||
config.set("osuPath", folder);
|
||||
mainWindow.webContents.send("ezpplauncher:alert", {
|
||||
type: "success",
|
||||
message: "osu! path successfully saved!",
|
||||
});
|
||||
} else {
|
||||
mainWindow.webContents.send("ezpplauncher:alert", {
|
||||
type: "error",
|
||||
message: "invalid osu! path!",
|
||||
});
|
||||
}
|
||||
}
|
||||
return config.all();
|
||||
});
|
||||
|
||||
ipcMain.handle("ezpplauncher:checkUpdate", async (e) => {
|
||||
const updateInfo = await updateAvailable();
|
||||
if (updateInfo.update) {
|
||||
mainWindow.webContents.send("ezpplauncher:update", updateInfo.release);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("ezpplauncher:exitAndUpdate", async (e) => {
|
||||
await shell.openExternal(releasesUrl);
|
||||
app.exit();
|
||||
});
|
||||
|
||||
ipcMain.handle("ezpplauncher:launch", async (e) => {
|
||||
try {
|
||||
const osuWindowTitle = windowName.getWindowText("osu!.exe");
|
||||
if (osuWindowTitle.length > 0) {
|
||||
mainWindow.webContents.send("ezpplauncher:alert", {
|
||||
type: "error",
|
||||
message: "osu! is running, please exit.",
|
||||
});
|
||||
mainWindow.webContents.send("ezpplauncher:launchabort");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log("Preparing launch...");
|
||||
const configPatch = config.get("patch");
|
||||
patch = configPatch != undefined ? configPatch == "true" : true;
|
||||
mainWindow.webContents.send("ezpplauncher:launchstatus", {
|
||||
status: "Checking osu! directory...",
|
||||
});
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
const osuPath = config.get("osuPath");
|
||||
userOsuPath = osuPath;
|
||||
if (osuPath == undefined) {
|
||||
mainWindow.webContents.send("ezpplauncher:launchabort");
|
||||
mainWindow.webContents.send("ezpplauncher:alert", {
|
||||
type: "error",
|
||||
message: "osu! path not set!",
|
||||
});
|
||||
mainWindow.webContents.send("ezpplauncher:open-settings");
|
||||
logger.log("osu! path is not set.");
|
||||
return;
|
||||
}
|
||||
if (!(await isValidOsuFolder(osuPath))) {
|
||||
mainWindow.webContents.send("ezpplauncher:launchabort");
|
||||
mainWindow.webContents.send("ezpplauncher:alert", {
|
||||
type: "error",
|
||||
message: "invalid osu! path!",
|
||||
});
|
||||
logger.log("osu! path is invalid.");
|
||||
return;
|
||||
}
|
||||
if (patch) {
|
||||
if (!(await isNet8Installed())) {
|
||||
mainWindow.webContents.send("ezpplauncher:launchabort");
|
||||
mainWindow.webContents.send("ezpplauncher:alert", {
|
||||
type: "error",
|
||||
message: ".NET 8 is not installed.",
|
||||
});
|
||||
//open .net 8 download in browser
|
||||
shell.openExternal(
|
||||
"https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-8.0.4-windows-x64-installer",
|
||||
);
|
||||
logger.log(".NET 8 is not installed.");
|
||||
}
|
||||
}
|
||||
mainWindow.webContents.send("ezpplauncher:launchstatus", {
|
||||
status: "Checking for osu! updates...",
|
||||
});
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
const releaseStream = await getGlobalConfig(osuPath).get(
|
||||
"_ReleaseStream",
|
||||
);
|
||||
const latestFiles = await getUpdateFiles(releaseStream);
|
||||
const updateFiles = await getFilesThatNeedUpdate(osuPath, latestFiles);
|
||||
if (updateFiles.length > 0) {
|
||||
logger.log("osu! updates found.");
|
||||
const updateDownloader = downloadUpdateFiles(osuPath, updateFiles);
|
||||
let errored = false;
|
||||
updateDownloader.eventEmitter.on("error", (data) => {
|
||||
const filename = data.fileName;
|
||||
logger.error(
|
||||
`Failed to download/replace ${filename}!`,
|
||||
data.error,
|
||||
);
|
||||
errored = true;
|
||||
mainWindow.webContents.send("ezpplauncher:alert", {
|
||||
type: "error",
|
||||
message:
|
||||
`Failed to download/replace ${filename}!\nMaybe try to restart EZPPLauncher.`,
|
||||
});
|
||||
});
|
||||
updateDownloader.eventEmitter.on("data", (data) => {
|
||||
if (data.progress >= 100) {
|
||||
logger.log(`Downloaded ${data.fileName} successfully.`);
|
||||
}
|
||||
mainWindow.webContents.send("ezpplauncher:launchprogress", {
|
||||
progress: Math.ceil(data.progress),
|
||||
});
|
||||
mainWindow.webContents.send("ezpplauncher:launchstatus", {
|
||||
status: `Downloading ${data.fileName}(${formatBytes(data.loaded)}/${
|
||||
formatBytes(data.total)
|
||||
})...`,
|
||||
});
|
||||
});
|
||||
await updateDownloader.startDownload();
|
||||
mainWindow.webContents.send("ezpplauncher:launchprogress", {
|
||||
progress: -1,
|
||||
});
|
||||
if (errored) {
|
||||
mainWindow.webContents.send("ezpplauncher:launchabort");
|
||||
return;
|
||||
}
|
||||
mainWindow.webContents.send("ezpplauncher:launchstatus", {
|
||||
status: "osu! is now up to date!",
|
||||
});
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
} else {
|
||||
mainWindow.webContents.send("ezpplauncher:launchstatus", {
|
||||
status: "osu! is up to date!",
|
||||
});
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
}
|
||||
|
||||
if (patch) {
|
||||
mainWindow.webContents.send("ezpplauncher:launchstatus", {
|
||||
status: "Looking for patcher updates...",
|
||||
});
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
const [patchFiles, allUpdateFiles] = await getEZPPLauncherUpdateFiles(
|
||||
osuPath,
|
||||
);
|
||||
if (patchFiles.length > 0) {
|
||||
logger.log("EZPPLauncher updates found.");
|
||||
const patcherDownloader = await downloadEZPPLauncherUpdateFiles(
|
||||
osuPath,
|
||||
patchFiles,
|
||||
allUpdateFiles,
|
||||
);
|
||||
let errored = false;
|
||||
patcherDownloader.eventEmitter.on("error", (data) => {
|
||||
const filename = data.fileName;
|
||||
logger.error(`Failed to download/replace ${filename}!`, data.error);
|
||||
errored = true;
|
||||
mainWindow.webContents.send("ezpplauncher:alert", {
|
||||
type: "error",
|
||||
message:
|
||||
`Failed to download/replace ${filename}!\nMaybe try to restart EZPPLauncher.`,
|
||||
});
|
||||
});
|
||||
patcherDownloader.eventEmitter.on("data", (data) => {
|
||||
if (data.progress >= 100) {
|
||||
logger.log(`Downloaded ${data.fileName} successfully.`);
|
||||
}
|
||||
mainWindow.webContents.send("ezpplauncher:launchprogress", {
|
||||
progress: Math.ceil(data.progress),
|
||||
});
|
||||
mainWindow.webContents.send("ezpplauncher:launchstatus", {
|
||||
status: `Downloading ${data.fileName}(${
|
||||
formatBytes(data.loaded)
|
||||
}/${formatBytes(data.total)})...`,
|
||||
});
|
||||
});
|
||||
patcherDownloader.eventEmitter.on("delete", (data) => {
|
||||
logger.log(`Deleting ${data.fileName}!`);
|
||||
mainWindow.webContents.send("ezpplauncher:launchprogress", {
|
||||
progress: -1,
|
||||
});
|
||||
mainWindow.webContents.send("ezpplauncher:launchstatus", {
|
||||
status: `Deleting ${data.fileName}...`,
|
||||
});
|
||||
});
|
||||
await patcherDownloader.startDownload();
|
||||
mainWindow.webContents.send("ezpplauncher:launchprogress", {
|
||||
progress: -1,
|
||||
});
|
||||
if (errored) {
|
||||
mainWindow.webContents.send("ezpplauncher:launchabort");
|
||||
return;
|
||||
}
|
||||
mainWindow.webContents.send("ezpplauncher:launchstatus", {
|
||||
status: "Patcher is now up to date!",
|
||||
});
|
||||
} else {
|
||||
mainWindow.webContents.send("ezpplauncher:launchstatus", {
|
||||
status: "Patcher is up to date!",
|
||||
});
|
||||
}
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
}
|
||||
if (updateFiles.length > 0) {
|
||||
mainWindow.webContents.send("ezpplauncher:launchstatus", {
|
||||
status: "Launching osu! updater to verify updates...",
|
||||
});
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
|
||||
await new Promise((res) => {
|
||||
runOsuUpdater(osuPath, async () => {
|
||||
await new Promise((res) => setTimeout(res, 500));
|
||||
const terminationThread = setInterval(async () => {
|
||||
const osuWindowTitle = windowName.getWindowText("osu!.exe");
|
||||
if (osuWindowTitle.length < 0) {
|
||||
return;
|
||||
}
|
||||
const firstInstance = osuWindowTitle[0];
|
||||
if (firstInstance) {
|
||||
const processId = firstInstance.processId;
|
||||
await fkill(processId, { force: true, silent: true });
|
||||
clearInterval(terminationThread);
|
||||
res();
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
|
||||
mainWindow.webContents.send("ezpplauncher:launchstatus", {
|
||||
status: "Preparing launch...",
|
||||
});
|
||||
|
||||
/* await updateOsuConfigHashes(osuPath); */
|
||||
logger.log("Replacing UI files...");
|
||||
try {
|
||||
await replaceUIFiles(osuPath, false);
|
||||
logger.log("UI files replaced successfully.");
|
||||
} catch (err) {
|
||||
logger.error("Failed to replace UI files:", err);
|
||||
mainWindow.webContents.send("ezpplauncher:alert", {
|
||||
type: "error",
|
||||
message: "Failed to replace UI files. try restarting EZPPLauncher.",
|
||||
});
|
||||
mainWindow.webContents.send("ezpplauncher:launchabort");
|
||||
return;
|
||||
}
|
||||
|
||||
const forceUpdateFiles = [
|
||||
".require_update",
|
||||
"help.txt",
|
||||
"_pending",
|
||||
];
|
||||
|
||||
try {
|
||||
for (const updateFileName of forceUpdateFiles) {
|
||||
const updateFile = path.join(osuPath, updateFileName);
|
||||
if (fs.existsSync(updateFile)) {
|
||||
await fs.promises.rm(updateFile, {
|
||||
force: true,
|
||||
recursive: (await fs.promises.lstat(updateFile)).isDirectory(),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Failed to remove force update files:", err);
|
||||
}
|
||||
|
||||
const userConfig = getUserConfig(osuPath);
|
||||
if (richPresence.hasPresence) {
|
||||
await userConfig.set("DiscordRichPresence", "0");
|
||||
}
|
||||
await userConfig.set("ShowInterfaceDuringRelax", "1");
|
||||
if (currentUser) {
|
||||
await userConfig.set("CredentialEndpoint", "ez-pp.farm");
|
||||
await userConfig.set("SavePassword", "1");
|
||||
await userConfig.set("SaveUsername", "1");
|
||||
await userConfig.set("Username", currentUser.username);
|
||||
await userConfig.set("Password", currentUser.password);
|
||||
}
|
||||
|
||||
mainWindow.webContents.send("ezpplauncher:launchstatus", {
|
||||
status: "Launching osu!...",
|
||||
});
|
||||
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
|
||||
logger.log("Launching osu!...");
|
||||
|
||||
const onExitHook = () => {
|
||||
logger.log("osu! has exited.");
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
stopOsuStatus();
|
||||
richPresence.updateUser({
|
||||
username: " ",
|
||||
id: undefined,
|
||||
});
|
||||
richPresence.updateStatus({
|
||||
state: "Idle in Launcher...",
|
||||
details: undefined,
|
||||
});
|
||||
richPresence.update();
|
||||
mainWindow.webContents.send("ezpplauncher:launchstatus", {
|
||||
status: "Waiting for cleanup...",
|
||||
});
|
||||
const timeStart = performance.now();
|
||||
logger.log("Waiting for cleanup...");
|
||||
|
||||
const cleanup = setInterval(async () => {
|
||||
const osuUIFile = path.join(osuPath, "osu!ui.dll");
|
||||
const osuGameplayFile = path.join(osuPath, "osu!gameplay.dll");
|
||||
if (isWritable(osuUIFile) && isWritable(osuGameplayFile)) {
|
||||
logger.log(
|
||||
`Cleanup complete, took ${
|
||||
((performance.now() - timeStart) / 1000).toFixed(3)
|
||||
} seconds.`,
|
||||
);
|
||||
clearInterval(cleanup);
|
||||
await replaceUIFiles(osuPath, true);
|
||||
mainWindow.webContents.send("ezpplauncher:launchabort");
|
||||
osuLoaded = false;
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
runOsuWithDevServer(osuPath, "ez-pp.farm", onExitHook);
|
||||
mainWindow.hide();
|
||||
startOsuStatus();
|
||||
return true;
|
||||
} catch (err) {
|
||||
logger.error("Failed to launch", err);
|
||||
mainWindow.webContents.send("ezpplauncher:alert", {
|
||||
type: "error",
|
||||
message: "Failed to launch osu!. Please try again.",
|
||||
});
|
||||
mainWindow.webContents.send("ezpplauncher:launchabort");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) {
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
setupTitlebar();
|
||||
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 550,
|
||||
height: 350,
|
||||
resizable: false,
|
||||
frame: false,
|
||||
titleBarStyle: "hidden",
|
||||
title: `${appName} ${appVersion}`,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
},
|
||||
icon: path.join(__dirname, "public/favicon.png"),
|
||||
show: false,
|
||||
});
|
||||
|
||||
const menu = Menu.buildFromTemplate([]);
|
||||
Menu.setApplicationMenu(menu);
|
||||
|
||||
// disable electron toolbar
|
||||
/* if (!isDev()) */
|
||||
mainWindow.setMenu(null);
|
||||
|
||||
attachTitlebarToWindow(mainWindow);
|
||||
|
||||
// This block of code is intended for development purpose only.
|
||||
// Delete this entire block of code when you are ready to package the application.
|
||||
if (isDev()) {
|
||||
mainWindow.loadURL("http://localhost:8080/");
|
||||
} else {
|
||||
loadURL(mainWindow);
|
||||
}
|
||||
|
||||
registerIPCPipes();
|
||||
|
||||
const presenceEnabled = config.get("presence");
|
||||
if (presenceEnabled == undefined) {
|
||||
richPresence.connect();
|
||||
} else {
|
||||
if (presenceEnabled == "true") {
|
||||
richPresence.connect();
|
||||
}
|
||||
}
|
||||
|
||||
logger.init();
|
||||
|
||||
const loggingEnabled = config.get("logging");
|
||||
if (loggingEnabled && loggingEnabled == "true") {
|
||||
logger.enabled = true;
|
||||
}
|
||||
// Uncomment the following line of code when app is ready to be packaged.
|
||||
// loadURL(mainWindow);
|
||||
|
||||
// Open the DevTools and also disable Electron Security Warning.
|
||||
if (isDev()) {
|
||||
process.env["ELECTRON_DISABLE_SECURITY_WARNINGS"] = true;
|
||||
mainWindow.webContents.openDevTools({ mode: "detach" });
|
||||
}
|
||||
|
||||
// Emitted when the window is closed.
|
||||
mainWindow.on("closed", function () {
|
||||
// Dereference the window object, usually you would store windows
|
||||
// in an array if your app supports multi windows, this is the time
|
||||
// when you should delete the corresponding element.
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
// Emitted when the window is ready to be shown
|
||||
// This helps in showing the window gracefully.
|
||||
mainWindow.once("ready-to-show", async () => {
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
});
|
||||
}
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.on("ready", createWindow);
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on("window-all-closed", async function () {
|
||||
// On macOS it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
await richPresence.disconnect();
|
||||
if (process.platform !== "darwin") app.quit();
|
||||
});
|
||||
|
||||
app.on("activate", function () {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (mainWindow === null) createWindow();
|
||||
});
|
||||
// In this file you can include the rest of your app's specific main process
|
||||
// code. You can also put them in separate files and require them here.
|
150
osuUtil.js
150
osuUtil.js
|
@ -1,150 +0,0 @@
|
|||
const fs = require('fs');
|
||||
const fu = require('./fileUtil');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const axios = require('axios').default;
|
||||
const executeUtil = require('./executeUtil');
|
||||
const { EventEmitter } = require('events');
|
||||
const { DownloaderHelper } = require('node-downloader-helper');
|
||||
|
||||
const checkUpdateURL = "https://osu.ppy.sh/web/check-updates.php?action=check&stream=";
|
||||
const osuEntities = [
|
||||
'avcodec-51.dll',
|
||||
'avformat-52.dll',
|
||||
'avutil-49.dll',
|
||||
'bass.dll',
|
||||
'bass_fx.dll',
|
||||
'collection.db',
|
||||
'd3dcompiler_47.dll',
|
||||
'Data',
|
||||
'Downloads',
|
||||
'libEGL.dll',
|
||||
'libGLESv2.dll',
|
||||
'Logs',
|
||||
'Microsoft.Ink.dll',
|
||||
'OpenTK.dll',
|
||||
'osu!.cfg',
|
||||
'osu!.db',
|
||||
'osu!.exe',
|
||||
'osu!auth.dll',
|
||||
'osu!gameplay.dll',
|
||||
'osu!seasonal.dll',
|
||||
'osu!ui.dll',
|
||||
'presence.db',
|
||||
'pthreadGC2.dll',
|
||||
'scores.db',
|
||||
'Skins',
|
||||
'Songs'
|
||||
]
|
||||
|
||||
async function isValidOsuFolder(path) {
|
||||
const allFiles = await fs.promises.readdir(path);
|
||||
let matches = 0;
|
||||
for (const file of allFiles) {
|
||||
if (osuEntities.includes(file)) matches = matches + 1;
|
||||
}
|
||||
return Math.round((matches / osuEntities.length) * 100) >= 60;
|
||||
}
|
||||
|
||||
async function getLatestConfig(osuPath) {
|
||||
const allFiles = await fs.promises.readdir(osuPath);
|
||||
const configFileInfo = {
|
||||
name: "",
|
||||
path: "",
|
||||
lastModified: 0,
|
||||
get: async (key) => {
|
||||
const fileStream = await fs.promises.readFile(configFileInfo.path, "utf-8");
|
||||
const lines = fileStream.split(/\r?\n/)
|
||||
for (const line of lines) {
|
||||
if (line.includes(' = ')) {
|
||||
const argsPair = line.split(' = ', 2);
|
||||
const keyname = argsPair[0]
|
||||
const value = argsPair[1];
|
||||
if (keyname == key) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const file of allFiles) {
|
||||
if (file.startsWith('osu!.') && file.endsWith('.cfg') && file !== "osu!.cfg") {
|
||||
const fullFilePath = path.join(osuPath, file);
|
||||
const fileStats = await fs.promises.stat(fullFilePath);
|
||||
const lastModified = fileStats.mtimeMs;
|
||||
if (lastModified > configFileInfo.lastModified) {
|
||||
configFileInfo.name = file;
|
||||
configFileInfo.path = fullFilePath;
|
||||
configFileInfo.lastModified = lastModified;
|
||||
}
|
||||
}
|
||||
}
|
||||
return configFileInfo;
|
||||
}
|
||||
|
||||
async function getUpdateFiles(releaseStream) {
|
||||
const releaseData = await axios.get(checkUpdateURL + releaseStream, {});
|
||||
return releaseData.data;
|
||||
}
|
||||
|
||||
async function filesThatNeedUpdate(osuPath, updateFiles) {
|
||||
const filesToDownload = [];
|
||||
for (const updatedFile of updateFiles) {
|
||||
const fileName = updatedFile.filename;
|
||||
const fileHash = updatedFile.file_hash;
|
||||
const fileURL = updatedFile.url_full;
|
||||
|
||||
const fileOnDisk = path.join(osuPath, fileName);
|
||||
if (await fu.existsAsync(fileOnDisk)) {
|
||||
const binaryFileContents = await fs.promises.readFile(fileOnDisk);
|
||||
const existingFileMD5 = crypto.createHash("md5").update(binaryFileContents).digest("hex");
|
||||
if (existingFileMD5.toLowerCase() != fileHash.toLowerCase()) {
|
||||
filesToDownload.push({
|
||||
fileName,
|
||||
fileURL
|
||||
})
|
||||
// console.log("hashes are not matching", `(${existingFileMD5} - ${fileHash})`);
|
||||
}
|
||||
} else {
|
||||
filesToDownload.push({
|
||||
fileName,
|
||||
fileURL
|
||||
});
|
||||
// console.log("new file " + fileName);
|
||||
}
|
||||
}
|
||||
return filesToDownload;
|
||||
}
|
||||
|
||||
async function downloadUpdateFiles(osuPath, filesToUpdate) {
|
||||
const eventEmitter = new EventEmitter();
|
||||
let completedIndex = 0;
|
||||
filesToUpdate.forEach(async (fileToUpdate) => {
|
||||
const filePath = path.join(osuPath, fileToUpdate.fileName);
|
||||
if (await fu.existsAsync(filePath))
|
||||
await fs.promises.rm(filePath);
|
||||
|
||||
const fileDownload = new DownloaderHelper(fileToUpdate.fileURL, osuPath, {
|
||||
fileName: fileToUpdate.fileName,
|
||||
override: true,
|
||||
});
|
||||
fileDownload.on('end', () => {
|
||||
completedIndex = completedIndex + 1;
|
||||
if (completedIndex >= filesToUpdate.length)
|
||||
eventEmitter.emit('completed');
|
||||
});
|
||||
|
||||
fileDownload.start().catch(err => console.error(err));
|
||||
});
|
||||
|
||||
return eventEmitter;
|
||||
}
|
||||
|
||||
async function startWithDevServer(osuPath, serverDomain, onExit) {
|
||||
const osuExe = path.join(osuPath, "osu!.exe");
|
||||
if (!await fu.existsAsync(osuExe)) return false;
|
||||
executeUtil.runFile(osuPath, osuExe, ["-devserver", serverDomain], onExit);
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = { isValidOsuFolder, getLatestConfig, getUpdateFiles, filesThatNeedUpdate, downloadUpdateFiles, startOsuWithDevServer: startWithDevServer }
|
9360
package-lock.json
generated
Normal file
9360
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
116
package.json
116
package.json
|
@ -1,54 +1,90 @@
|
|||
{
|
||||
"name": "ezpplauncher",
|
||||
"version": "1.0.0",
|
||||
"main": "app.js",
|
||||
"name": "ezpplauncher-next",
|
||||
"version": "2.1.7",
|
||||
"description": "EZPPLauncher rewritten with Svelte.",
|
||||
"private": false,
|
||||
"license": "MIT",
|
||||
"author": "HorizonCode",
|
||||
"main": "main.js",
|
||||
"author": "HorizonCode <horizoncode88@gmail.com>",
|
||||
"build": {
|
||||
"appId": "farm.ezpp.ezppfarm.launcher",
|
||||
"productName": "ezpplauncher",
|
||||
"directories": {
|
||||
"output": "release",
|
||||
"buildResources": "dist"
|
||||
},
|
||||
"asar": true,
|
||||
"icon": "public/favicon.png",
|
||||
"productName": "EZPPLauncher",
|
||||
"files": [
|
||||
"public/**/*",
|
||||
"main.js",
|
||||
"preload.js",
|
||||
"electron/*",
|
||||
"electron/**/*"
|
||||
],
|
||||
"win": {
|
||||
"icon": "./assets/logo.png",
|
||||
"target": [
|
||||
"portable"
|
||||
]
|
||||
},
|
||||
"nsis": {
|
||||
"runAfterFinish": true
|
||||
},
|
||||
"portable": {
|
||||
"artifactName": "EZPPLauncher.exe"
|
||||
}
|
||||
},
|
||||
"frontend": {
|
||||
"config": {
|
||||
"applicationName": "EZPPLauncher"
|
||||
}
|
||||
"linux": {},
|
||||
"mac": {}
|
||||
},
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"pack-win": "electron-builder --x64",
|
||||
"pack-win32": "electron-builder --ia32",
|
||||
"pack-winarm": "electron-builder --arm64",
|
||||
"pack-linux": "electron-builder --dir --linux --ia32 --arm64 --x64",
|
||||
"pack-mac": "electron-builder --dir --mac --ia32 --arm64 --x64",
|
||||
"dist": "electron-builder"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^17.4.3",
|
||||
"electron-builder": "^23.0.3",
|
||||
"electron-packager": "^15.5.1"
|
||||
"build": "rollup -c --bundleConfigAsCjs",
|
||||
"rebuild": "electron-rebuild",
|
||||
"dev": "rollup -c -w --bundleConfigAsCjs",
|
||||
"start": "sirv public --no-clear",
|
||||
"electron": "wait-on http://localhost:8080 && electron .",
|
||||
"electron-dev": "concurrently \"yarn run dev\" \"yarn run electron\"",
|
||||
"preelectron-pack": "electron-rebuild && yarn run build",
|
||||
"electron-pack": "electron-builder",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"custom-electron-titlebar": "^4.1.1",
|
||||
"jquery": "^3.6.0",
|
||||
"node-downloader-helper": "^2.1.4",
|
||||
"sweetalert2": "^11.5.2"
|
||||
"@types/better-sqlite3": "^7.6.8",
|
||||
"axios": "^1.6.5",
|
||||
"better-sqlite3": "^9.2.2",
|
||||
"crypto": "^1.0.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"custom-electron-titlebar": "^4.2.7",
|
||||
"discord-auto-rpc": "^1.0.17",
|
||||
"electron-serve": "^1.1.0",
|
||||
"electron-unhandled": "^4.0.1",
|
||||
"fkill": "^7.2.1",
|
||||
"get-window-by-name": "^2.0.0",
|
||||
"regedit-rs": "^1.0.2",
|
||||
"semver": "^7.5.4",
|
||||
"svelte-french-toast": "^1.2.0",
|
||||
"sweetalert2": "^11.10.8",
|
||||
"systeminformation": "^5.21.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron/rebuild": "^3.5.0",
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
"@rollup/plugin-image": "^3.0.3",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@rollup/plugin-typescript": "^11.1.5",
|
||||
"@tsconfig/svelte": "^5.0.2",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"concurrently": "^8.2.2",
|
||||
"electron": "^28.1.2",
|
||||
"electron-builder": "^24.9.1",
|
||||
"flowbite": "^2.2.1",
|
||||
"flowbite-svelte": "^0.44.21",
|
||||
"flowbite-svelte-icons": "^0.4.5",
|
||||
"postcss": "^8.4.32",
|
||||
"postcss-load-config": "^5.0.2",
|
||||
"rollup": "^4.9.2",
|
||||
"rollup-plugin-css-only": "^4.5.2",
|
||||
"rollup-plugin-livereload": "^2.0.5",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"rollup-plugin-progress": "^1.1.2",
|
||||
"rollup-plugin-svelte": "^7.1.6",
|
||||
"rollup-plugin-unused": "^0.1.1",
|
||||
"sirv-cli": "^2.0.2",
|
||||
"svelte": "^4.2.8",
|
||||
"svelte-check": "^3.6.2",
|
||||
"svelte-preprocess": "^5.1.3",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.3",
|
||||
"wait-on": "^7.2.0"
|
||||
}
|
||||
}
|
||||
|
|
13
postcss.config.cjs
Normal file
13
postcss.config.cjs
Normal file
|
@ -0,0 +1,13 @@
|
|||
const tailwindcss = require("tailwindcss");
|
||||
const autoprefixer = require("autoprefixer");
|
||||
|
||||
const config = {
|
||||
plugins: [
|
||||
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
|
||||
tailwindcss(),
|
||||
//But others, like autoprefixer, need to run after,
|
||||
autoprefixer,
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = config;
|
127
preload.js
Normal file
127
preload.js
Normal file
|
@ -0,0 +1,127 @@
|
|||
const { Titlebar, TitlebarColor } = require("custom-electron-titlebar");
|
||||
const { ipcRenderer } = require("electron");
|
||||
const { appName, appVersion } = require("./electron/appInfo");
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
const titlebar = new Titlebar({
|
||||
backgroundColor: TitlebarColor.fromHex("#202020"),
|
||||
itemBackgroundColor: TitlebarColor.fromHex("#202020"),
|
||||
menu: null,
|
||||
enableMnemonics: false,
|
||||
maximizable: false,
|
||||
});
|
||||
titlebar.updateTitle(`${appName} ${appVersion}`);
|
||||
});
|
||||
|
||||
window.addEventListener("login-attempt", async (e) => {
|
||||
const loginResult = await ipcRenderer.invoke("ezpplauncher:login", {
|
||||
username: e.detail.username,
|
||||
password: e.detail.password,
|
||||
saveCredentials: e.detail.saveCredentials,
|
||||
});
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("login-result", { detail: loginResult }),
|
||||
);
|
||||
});
|
||||
|
||||
window.addEventListener("autologin-active", async (e) => {
|
||||
const autologin = await ipcRenderer.invoke(
|
||||
"ezpplauncher:autologin-active",
|
||||
);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("autologin-result", { detail: autologin }),
|
||||
);
|
||||
});
|
||||
|
||||
window.addEventListener("autologin-attempt", async () => {
|
||||
const loginResult = await ipcRenderer.invoke("ezpplauncher:autologin");
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("login-result", { detail: loginResult }),
|
||||
);
|
||||
});
|
||||
|
||||
window.addEventListener("logout", async () => {
|
||||
await ipcRenderer.invoke("ezpplauncher:logout");
|
||||
});
|
||||
|
||||
window.addEventListener("guest-login", async () => {
|
||||
await ipcRenderer.invoke("ezpplauncher:guestlogin");
|
||||
});
|
||||
|
||||
window.addEventListener("launch", async (e) => {
|
||||
await ipcRenderer.invoke("ezpplauncher:launch", e.detail);
|
||||
});
|
||||
|
||||
window.addEventListener("settings-get", async () => {
|
||||
const settings = await ipcRenderer.invoke("ezpplauncher:settings");
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("settings-result", { detail: settings }),
|
||||
);
|
||||
});
|
||||
|
||||
window.addEventListener("setting-update", async (e) => {
|
||||
const detail = e.detail;
|
||||
await ipcRenderer.invoke("ezpplauncher:setting-update", detail);
|
||||
});
|
||||
|
||||
window.addEventListener("folder-auto", async (e) => {
|
||||
const result = await ipcRenderer.invoke("ezpplauncher:detect-folder");
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("settings-result", { detail: result }),
|
||||
);
|
||||
});
|
||||
|
||||
window.addEventListener("folder-set", async (e) => {
|
||||
const result = await ipcRenderer.invoke("ezpplauncher:set-folder");
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("settings-result", { detail: result }),
|
||||
);
|
||||
});
|
||||
|
||||
window.addEventListener("settings-set", async (e) => {
|
||||
await ipcRenderer.invoke("ezpplauncher:settings-set", e.detail);
|
||||
});
|
||||
|
||||
window.addEventListener("updateCheck", async () => {
|
||||
await ipcRenderer.invoke("ezpplauncher:checkUpdate");
|
||||
})
|
||||
|
||||
window.addEventListener("updateExit", async () => {
|
||||
await ipcRenderer.invoke("ezpplauncher:exitAndUpdate");
|
||||
});
|
||||
|
||||
ipcRenderer.addListener("ezpplauncher:launchabort", (e, args) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("launch-abort"),
|
||||
);
|
||||
});
|
||||
|
||||
ipcRenderer.addListener("ezpplauncher:alert", (e, args) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("alert", { detail: args }),
|
||||
);
|
||||
});
|
||||
|
||||
ipcRenderer.addListener("ezpplauncher:launchstatus", (e, args) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("launchStatusUpdate", { detail: args }),
|
||||
);
|
||||
});
|
||||
|
||||
ipcRenderer.addListener("ezpplauncher:launchprogress", (e, args) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("launchProgressUpdate", { detail: args }),
|
||||
);
|
||||
});
|
||||
|
||||
ipcRenderer.addListener("ezpplauncher:update", (e, args) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("update", { detail: args }),
|
||||
);
|
||||
});
|
||||
|
||||
ipcRenderer.addListener("ezpplauncher:open-settings", (e, args) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("open-settings"),
|
||||
);
|
||||
});
|
|
@ -1,108 +0,0 @@
|
|||
const { ipcRenderer } = require('electron');
|
||||
const { Titlebar, Color } = require('custom-electron-titlebar');
|
||||
const appInfo = require('../appInfo');
|
||||
let titlebar;
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
titlebar = new Titlebar({
|
||||
backgroundColor: Color.fromHex("#303030"),
|
||||
itemBackgroundColor: Color.fromHex("#121212"),
|
||||
menu: null,
|
||||
maximizable: false
|
||||
});
|
||||
|
||||
titlebar.updateTitle(`${appInfo.appName} ${appInfo.appVersion}`);
|
||||
|
||||
const $ = require('jquery');
|
||||
const Swal = require('sweetalert2');
|
||||
|
||||
let currentState;
|
||||
|
||||
$("#launch-btn").on('click', async () => {
|
||||
switch (currentState) {
|
||||
case "up-to-date":
|
||||
$("#launch-btn").attr('disabled', true);
|
||||
$('#launch-btn').html('Launching...');
|
||||
const result = await ipcRenderer.invoke("launch");
|
||||
if (!result) {
|
||||
Swal.fire({
|
||||
title: 'Uh oh!',
|
||||
text: "Something went wrong while launching!",
|
||||
icon: 'error',
|
||||
confirmButtonText: 'Okay'
|
||||
});
|
||||
$("#launch-btn").attr('disabled', false);
|
||||
$('#launch-btn').html('Launch');
|
||||
} else {
|
||||
$("#launch-btn").attr('disabled', true);
|
||||
$('#launch-btn').html('Running...');
|
||||
}
|
||||
break;
|
||||
case "update-available":
|
||||
$("#launch-btn").attr('disabled', true);
|
||||
$('#launch-btn').html('Updating...');
|
||||
ipcRenderer.send("do-update");
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
$("#folder-btn").on('click', async () => {
|
||||
const success = await ipcRenderer.invoke('set-osu-dir');
|
||||
if (success == undefined)
|
||||
return;
|
||||
if (success) {
|
||||
Swal.fire({
|
||||
title: 'Success!',
|
||||
text: 'osu! folder set.',
|
||||
icon: 'success',
|
||||
confirmButtonText: 'Cool'
|
||||
})
|
||||
ipcRenderer.send("do-update-check");
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: 'Uh oh!',
|
||||
text: 'The selected folder is not a osu! directory.',
|
||||
icon: 'error',
|
||||
confirmButtonText: 'Oops.. my bad!'
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on('status_update', (event, status) => {
|
||||
currentState = status.type;
|
||||
switch (status.type) {
|
||||
case "up-to-date":
|
||||
$("#launch-btn").attr('disabled', false);
|
||||
$('#launch-btn').html('Launch');
|
||||
break;
|
||||
case "update-available":
|
||||
$("#launch-btn").attr('disabled', false);
|
||||
$('#launch-btn').html('Update');
|
||||
break;
|
||||
case "missing-folder":
|
||||
$('#launch-btn').html('Please set your osu! folder');
|
||||
break;
|
||||
case "error":
|
||||
Swal.fire({
|
||||
title: 'Uh oh!',
|
||||
text: status.message,
|
||||
icon: 'error',
|
||||
confirmButtonText: 'Okay'
|
||||
});
|
||||
ipcRenderer.send("do-update-check");
|
||||
break;
|
||||
case "update-complete":
|
||||
Swal.fire({
|
||||
title: 'Yaaay!',
|
||||
text: "Your osu! client has been successfully updated!",
|
||||
icon: 'success',
|
||||
confirmButtonText: 'Thanks :3'
|
||||
});
|
||||
ipcRenderer.send("do-update-check");
|
||||
break;
|
||||
}
|
||||
})
|
||||
|
||||
// workaround for the dark theme
|
||||
$('head').append($('<link href="../assets/sweetalert2.dark.css" rel="stylesheet" />'));
|
||||
})
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
15
public/global.css
Normal file
15
public/global.css
Normal file
|
@ -0,0 +1,15 @@
|
|||
html,
|
||||
body {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
}
|
22
public/index.html
Normal file
22
public/index.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
|
||||
<title>EZPPLauncher</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Exo+2:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Prompt:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="stylesheet" href="/global.css" />
|
||||
<link rel="stylesheet" href="/build/bundle.css" />
|
||||
|
||||
<script defer src="/build/bundle.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="select-none bg-gray-100 dark:bg-gray-900 overflow-hidden"></body>
|
||||
</html>
|
96
rollup.config.js
Normal file
96
rollup.config.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
import svelte from "rollup-plugin-svelte";
|
||||
import commonjs from "@rollup/plugin-commonjs";
|
||||
import resolve from "@rollup/plugin-node-resolve";
|
||||
import livereload from "rollup-plugin-livereload";
|
||||
import terser from "@rollup/plugin-terser";
|
||||
import css from "rollup-plugin-css-only";
|
||||
import postcss from "rollup-plugin-postcss";
|
||||
import image from "@rollup/plugin-image";
|
||||
import sveltePreprocess from "svelte-preprocess";
|
||||
import typescript from "@rollup/plugin-typescript";
|
||||
import progress from "rollup-plugin-progress";
|
||||
|
||||
const production = !process.env.ROLLUP_WATCH;
|
||||
|
||||
function serve() {
|
||||
let server;
|
||||
|
||||
function toExit() {
|
||||
if (server) server.kill(0);
|
||||
}
|
||||
|
||||
return {
|
||||
writeBundle() {
|
||||
if (server) return;
|
||||
server = require("child_process").spawn("npm", [
|
||||
"run",
|
||||
"start",
|
||||
"--",
|
||||
"--dev",
|
||||
], {
|
||||
stdio: ["ignore", "inherit", "inherit"],
|
||||
shell: true,
|
||||
});
|
||||
|
||||
process.on("SIGTERM", toExit);
|
||||
process.on("exit", toExit);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
input: "src/main.ts",
|
||||
output: {
|
||||
sourcemap: true,
|
||||
format: "iife",
|
||||
name: "app",
|
||||
file: "public/build/bundle.js",
|
||||
},
|
||||
plugins: [
|
||||
!production && progress({ clearLine: true }),
|
||||
svelte({
|
||||
preprocess: sveltePreprocess({ sourceMap: !production }),
|
||||
compilerOptions: {
|
||||
// enable run-time checks when not in production
|
||||
dev: !production,
|
||||
},
|
||||
}),
|
||||
|
||||
// we'll extract any component CSS out into
|
||||
// a separate file - better for performance
|
||||
css({ output: "bundle.css" }),
|
||||
postcss({ sourceMap: "inline" }),
|
||||
|
||||
// If you have external dependencies installed from
|
||||
// npm, you'll most likely need these plugins. In
|
||||
// some cases you'll need additional configuration -
|
||||
// consult the documentation for details:
|
||||
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
||||
resolve({
|
||||
browser: true,
|
||||
dedupe: ["svelte"],
|
||||
exportConditions: ["svelte"],
|
||||
}),
|
||||
typescript({
|
||||
sourceMap: !production,
|
||||
inlineSources: !production,
|
||||
}),
|
||||
commonjs(),
|
||||
image(),
|
||||
|
||||
// In dev mode, call `npm run start` once
|
||||
// the bundle has been generated
|
||||
!production && serve(),
|
||||
|
||||
// Watch the `public` directory and refresh the
|
||||
// browser on changes when not in production
|
||||
!production && livereload("public"),
|
||||
|
||||
// If we're building for production (npm run build
|
||||
// instead of npm run dev), minify
|
||||
production && terser(),
|
||||
],
|
||||
watch: {
|
||||
clearScreen: false,
|
||||
},
|
||||
};
|
223
src/App.svelte
Normal file
223
src/App.svelte
Normal file
|
@ -0,0 +1,223 @@
|
|||
<script lang="ts">
|
||||
import Avatar from "flowbite-svelte/Avatar.svelte";
|
||||
import Dropdown from "flowbite-svelte/Dropdown.svelte";
|
||||
import DropdownItem from "flowbite-svelte/DropdownItem.svelte";
|
||||
import DropdownHeader from "flowbite-svelte/DropdownHeader.svelte";
|
||||
import DropdownDivider from "flowbite-svelte/DropdownDivider.svelte";
|
||||
import Button from "flowbite-svelte/Button.svelte";
|
||||
import Indicator from "flowbite-svelte/Indicator.svelte";
|
||||
import ArrowLeftSolid from "flowbite-svelte-icons/ArrowLeftSolid.svelte";
|
||||
import ArrowRightFromBracketSolid from "flowbite-svelte-icons/ArrowRightFromBracketSolid.svelte";
|
||||
import ArrowRightToBracketSolid from "flowbite-svelte-icons/ArrowRightToBracketSolid.svelte";
|
||||
import HeartSolid from "flowbite-svelte-icons/HeartSolid.svelte";
|
||||
import UserSettingsSolid from "flowbite-svelte-icons/UserSettingsSolid.svelte";
|
||||
import ezppLogo from "../public/favicon.png";
|
||||
import {
|
||||
currentPage,
|
||||
currentUser,
|
||||
launching,
|
||||
launchPercentage,
|
||||
launchStatus,
|
||||
} from "./storage/localStore";
|
||||
import { Page } from "./consts/pages";
|
||||
import Login from "./pages/Login.svelte";
|
||||
import Launch from "./pages/Launch.svelte";
|
||||
import toast, { Toaster } from "svelte-french-toast";
|
||||
import type { User } from "./types/user";
|
||||
import Settings from "./pages/Settings.svelte";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
let user: User | undefined = undefined;
|
||||
let loggedIn = false;
|
||||
|
||||
let updateInfo: Record<string, unknown>;
|
||||
|
||||
currentUser.subscribe((newUser) => {
|
||||
loggedIn = newUser != undefined;
|
||||
user = newUser;
|
||||
});
|
||||
|
||||
const logout = () => {
|
||||
window.dispatchEvent(new CustomEvent("logout"));
|
||||
currentUser.set(undefined);
|
||||
currentPage.set(Page.Login);
|
||||
toast.success("Successfully logged out!", {
|
||||
position: "bottom-center",
|
||||
className:
|
||||
"dark:!bg-gray-800 border-1 dark:!border-gray-700 dark:!text-gray-100",
|
||||
duration: 2000,
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener("update", async (e) => {
|
||||
const update = (e as CustomEvent).detail;
|
||||
await Swal.fire({
|
||||
html: `EZPPLauncher ${update.tag_name} is now available!<br>Click the Button bellow to download the latest release!`,
|
||||
title: "It's your lucky day!",
|
||||
allowOutsideClick: false,
|
||||
allowEscapeKey: false,
|
||||
allowEnterKey: false,
|
||||
confirmButtonText: "Thanks!",
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent("updateExit"));
|
||||
});
|
||||
|
||||
window.dispatchEvent(new CustomEvent("updateCheck"));
|
||||
|
||||
window.addEventListener("open-settings", (e) => {
|
||||
currentPage.set(Page.Settings);
|
||||
});
|
||||
|
||||
window.addEventListener("launchStatusUpdate", (e) => {
|
||||
const status = (e as CustomEvent).detail.status;
|
||||
launchStatus.set(status);
|
||||
});
|
||||
|
||||
window.addEventListener("launchProgressUpdate", (e) => {
|
||||
const progress = (e as CustomEvent).detail.progress;
|
||||
launchPercentage.set(progress);
|
||||
});
|
||||
|
||||
window.addEventListener("launch-abort", () => {
|
||||
launchPercentage.set(-1);
|
||||
launchStatus.set("");
|
||||
launching.set(false);
|
||||
});
|
||||
|
||||
window.addEventListener("alert", (e) => {
|
||||
const toastMessage = (e as CustomEvent).detail;
|
||||
switch (toastMessage.type) {
|
||||
case "success": {
|
||||
toast.success(toastMessage.message, {
|
||||
position: "bottom-center",
|
||||
className:
|
||||
"dark:!bg-gray-800 border-1 dark:!border-gray-700 dark:!text-gray-100",
|
||||
duration: 2000,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "error": {
|
||||
toast.error(toastMessage.message, {
|
||||
position: "bottom-center",
|
||||
className:
|
||||
"dark:!bg-gray-800 border-1 dark:!border-gray-700 dark:!text-gray-100",
|
||||
duration: 4000,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
toast(toastMessage.message, {
|
||||
icon: "ℹ",
|
||||
position: "bottom-center",
|
||||
className:
|
||||
"dark:!bg-gray-800 border-1 dark:!border-gray-700 dark:!text-gray-100",
|
||||
duration: 1500,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Toaster></Toaster>
|
||||
|
||||
{#if !updateInfo}
|
||||
<div class="p-2 flex flex-row justify-between items-center">
|
||||
<div class="flex flex-row items-center animate-fadeIn opacity-0">
|
||||
{#if $currentPage == Page.Settings}
|
||||
<Button
|
||||
class="!ring-0 w-10 h-10 mr-1 rounded-lg animate-sideIn opacity-0 active:scale-95 transition-transform duration-75"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
currentPage.set(Page.Launch);
|
||||
}}
|
||||
>
|
||||
<ArrowLeftSolid class="outline-none border-none" size="sm" />
|
||||
</Button>
|
||||
{/if}
|
||||
<img src={ezppLogo} alt="EZPPFarm Logo" class="w-12 h-12 mr-2" />
|
||||
<span class="text-gray-700 dark:text-gray-100 text-xl font-extralight">
|
||||
EZPPLauncher
|
||||
</span>
|
||||
</div>
|
||||
{#if $currentPage == Page.Launch}
|
||||
<div
|
||||
class="flex flex-row gap-2 w-fill cursor-pointer md:order-2 animate-lsideIn opacity-0"
|
||||
>
|
||||
<Avatar
|
||||
class="rounded-lg border dark:border-gray-700 hover:ring-4 hover:ring-gray-200 dark:hover:ring-gray-800"
|
||||
src={loggedIn
|
||||
? "https://a.ez-pp.farm/" + user?.id
|
||||
: "https://a.ez-pp.farm/0"}
|
||||
id="avatar-menu"
|
||||
/>
|
||||
<!-- TODO: if user has donator, display heart indicator-->
|
||||
{#if $currentUser && $currentUser.donor}
|
||||
<Indicator
|
||||
class="pointer-events-none"
|
||||
color="red"
|
||||
border
|
||||
size="xl"
|
||||
placement="top-right"
|
||||
>
|
||||
<span class="text-red-300 text-xs font-bold">
|
||||
<HeartSolid class="select-none pointer-events-none" size="xs" />
|
||||
</span>
|
||||
</Indicator>
|
||||
{/if}
|
||||
</div>
|
||||
<Dropdown placement="bottom-start" triggeredBy="#avatar-menu">
|
||||
<DropdownHeader>
|
||||
<span class="block text-sm">{loggedIn ? user?.name : "Guest"}</span>
|
||||
<span
|
||||
class="block truncate text-sm font-medium text-gray-500 dark:text-gray-200"
|
||||
>
|
||||
{loggedIn ? user?.email : "Please log in!"}
|
||||
</span>
|
||||
</DropdownHeader>
|
||||
<DropdownItem
|
||||
class="flex flex-row gap-2 border-0 transition-colors"
|
||||
on:click={() => {
|
||||
if (!$launching) currentPage.set(Page.Settings);
|
||||
}}
|
||||
>
|
||||
<UserSettingsSolid class="select-none outline-none border-none" />
|
||||
Settings
|
||||
</DropdownItem>
|
||||
<DropdownDivider />
|
||||
{#if loggedIn}
|
||||
<DropdownItem
|
||||
class="flex flex-row gap-2 border-0 transition-colors"
|
||||
on:click={() => {
|
||||
if (!$launching) logout();
|
||||
}}
|
||||
>
|
||||
<ArrowRightFromBracketSolid
|
||||
class="select-none outline-none border-none"
|
||||
/>
|
||||
Sign out
|
||||
</DropdownItem>
|
||||
{:else}
|
||||
<DropdownItem
|
||||
class="flex flex-row gap-2 border-0 transition-colors"
|
||||
on:click={() => {
|
||||
if (!$launching) currentPage.set(Page.Login);
|
||||
}}
|
||||
>
|
||||
<ArrowRightToBracketSolid
|
||||
class="select-none outline-none border-none"
|
||||
/>
|
||||
Login
|
||||
</DropdownItem>
|
||||
{/if}
|
||||
</Dropdown>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if $currentPage == Page.Login}
|
||||
<Login />
|
||||
{:else if $currentPage == Page.Settings}
|
||||
<Settings />
|
||||
{:else}
|
||||
<Launch />
|
||||
{/if}
|
||||
{/if}
|
93
src/app.pcss
Normal file
93
src/app.pcss
Normal file
|
@ -0,0 +1,93 @@
|
|||
/* Write your global styles here, in PostCSS syntax */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
* {
|
||||
font-family: "Prompt";
|
||||
-webkit-user-select: none !important;
|
||||
-khtml-user-select: none !important;
|
||||
-moz-user-select: none !important;
|
||||
-o-user-select: none !important;
|
||||
user-select: none !important;
|
||||
-webkit-user-drag: none !important;
|
||||
-khtml-user-drag: none !important;
|
||||
-moz-user-drag: none !important;
|
||||
-o-user-drag: none !important;
|
||||
user-drag: none !important;
|
||||
}
|
||||
|
||||
html .cet-titlebar {
|
||||
background-color: #ececec !important;
|
||||
color: #202020 !important;
|
||||
}
|
||||
|
||||
html .cet-titlebar .cet-control-icon svg {
|
||||
fill: #000;
|
||||
}
|
||||
|
||||
.cet-titlebar .cet-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cet-container {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.indeterminate {
|
||||
background-image: repeating-linear-gradient(
|
||||
90deg,
|
||||
rgb(217 5 89) -1%,
|
||||
rgb(217 5 89) 10%,
|
||||
#d3d3d3 10%,
|
||||
#d3d3d3 90%
|
||||
);
|
||||
background-size: 200%;
|
||||
background-position-x: 15%;
|
||||
animation: progress-loading 5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.indeterminate {
|
||||
background-image: repeating-linear-gradient(
|
||||
90deg,
|
||||
rgb(217 5 89) -1%,
|
||||
rgb(217 5 89) 10%,
|
||||
#535353 10%,
|
||||
#535353 90%
|
||||
);
|
||||
}
|
||||
|
||||
html .cet-titlebar .cet-control-icon svg {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
html .cet-titlebar {
|
||||
background-color: #202020 !important;
|
||||
color: #ececec !important;
|
||||
}
|
||||
|
||||
.swal2-container {
|
||||
background: #202020 !important;
|
||||
}
|
||||
|
||||
.swal2-container .swal2-popup {
|
||||
background: #323232 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
|
||||
.animatedProgress div {
|
||||
transition: width 0.35s cubic-bezier(0.65, -0.02, 0.31, 1.01);
|
||||
}
|
||||
|
||||
.noselect {
|
||||
-webkit-user-select: none !important;
|
||||
user-select: none !important;
|
||||
}
|
||||
|
||||
@keyframes progress-loading {
|
||||
50% {
|
||||
background-position-x: -115%;
|
||||
}
|
||||
}
|
5
src/consts/pages.ts
Normal file
5
src/consts/pages.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export enum Page {
|
||||
Login = 0,
|
||||
Launch = 1,
|
||||
Settings = 2,
|
||||
}
|
1
src/global.d.ts
vendored
Normal file
1
src/global.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference types="svelte" />
|
101
src/lib/Progressbar.svelte
Normal file
101
src/lib/Progressbar.svelte
Normal file
|
@ -0,0 +1,101 @@
|
|||
<script lang="ts">
|
||||
import { cubicOut } from "svelte/easing";
|
||||
import { tweened } from "svelte/motion";
|
||||
import { twMerge, twJoin } from "tailwind-merge";
|
||||
import { clamp } from "../util/mathUtil";
|
||||
|
||||
export let progress: number = 45;
|
||||
export let precision = 2;
|
||||
export let tweenDuration = 400;
|
||||
export let animate = false;
|
||||
export let size = "h-2.5";
|
||||
export let labelInside = false;
|
||||
export let labelOutside = "";
|
||||
export let easing = cubicOut;
|
||||
export let color = "primary";
|
||||
export let indeterminate = false;
|
||||
export let labelInsideClass =
|
||||
"text-primary-100 text-xs font-medium text-center p-0.5 leading-none rounded-full";
|
||||
export let divClass = "w-full bg-gray-200 rounded-full dark:bg-gray-700";
|
||||
|
||||
const barColors: Record<string, string> = {
|
||||
primary: "bg-primary-600",
|
||||
blue: "bg-blue-600",
|
||||
gray: "bg-gray-600 dark:bg-gray-300",
|
||||
red: "bg-red-600 dark:bg-red-500",
|
||||
green: "bg-green-600 dark:bg-green-500",
|
||||
yellow: "bg-yellow-400",
|
||||
purple: "bg-purple-600 dark:bg-purple-500",
|
||||
indigo: "bg-indigo-600 dark:bg-indigo-500"
|
||||
};
|
||||
|
||||
let _progress = tweened(0, {
|
||||
duration: tweenDuration,
|
||||
easing
|
||||
});
|
||||
|
||||
$: {
|
||||
progress = clamp(Number(progress), 0, 100);
|
||||
_progress.set(progress);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if labelOutside}
|
||||
<div
|
||||
{...$$restProps}
|
||||
class={twMerge("flex justify-between mb-1", $$props.classLabelOutside)}
|
||||
>
|
||||
<span class="text-base font-medium text-blue-700 dark:text-white"
|
||||
>{labelOutside}</span
|
||||
>
|
||||
<span class="text-sm font-medium text-blue-700 dark:text-white"
|
||||
>{animate
|
||||
? isNaN($_progress)
|
||||
? parseInt("100").toFixed(precision)
|
||||
: $_progress.toFixed(precision)
|
||||
: isNaN(progress)
|
||||
? parseInt("100").toFixed(precision)
|
||||
: progress.toFixed(precision)}%</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class={twMerge(divClass, size, $$props.class)}>
|
||||
{#if labelInside}
|
||||
{#if !indeterminate}
|
||||
<div
|
||||
class={twJoin(labelInsideClass, barColors[color])}
|
||||
style="width: {animate ? $_progress : progress}%"
|
||||
>
|
||||
{animate
|
||||
? isNaN($_progress)
|
||||
? parseInt("100").toFixed(precision)
|
||||
: $_progress.toFixed(precision)
|
||||
: isNaN(progress)
|
||||
? parseInt("100").toFixed(precision)
|
||||
: progress.toFixed(precision)}%
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class={twJoin(
|
||||
barColors[color],
|
||||
size,
|
||||
"indeterminate rounded-full animate-pulse"
|
||||
)}
|
||||
/>
|
||||
{/if}
|
||||
{:else if !indeterminate}
|
||||
<div
|
||||
class={twJoin(barColors[color], size, "rounded-full")}
|
||||
style="width: {animate ? $_progress : progress}%"
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class={twJoin(
|
||||
barColors[color],
|
||||
size,
|
||||
"indeterminate rounded-full animate-pulse"
|
||||
)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
8
src/main.ts
Normal file
8
src/main.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import "./app.pcss";
|
||||
import App from "./App.svelte";
|
||||
|
||||
const app = new App({
|
||||
target: document.body,
|
||||
});
|
||||
|
||||
export default app;
|
57
src/pages/Launch.svelte
Normal file
57
src/pages/Launch.svelte
Normal file
|
@ -0,0 +1,57 @@
|
|||
<script lang="ts">
|
||||
import Button from "flowbite-svelte/Button.svelte";
|
||||
import Progressbar from "../lib/Progressbar.svelte";
|
||||
import {
|
||||
launching,
|
||||
patch,
|
||||
launchStatus,
|
||||
launchPercentage,
|
||||
} from "./../storage/localStore";
|
||||
let progressbarFix = true;
|
||||
|
||||
setTimeout(() => {
|
||||
progressbarFix = false;
|
||||
}, 1000);
|
||||
|
||||
const launch = () => {
|
||||
launching.set(true);
|
||||
const patching = $patch;
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("launch", { detail: { patch: patching } })
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<main
|
||||
class="h-[265px] my-auto flex flex-col justify-center items-center p-5 animate-fadeIn"
|
||||
>
|
||||
<div
|
||||
class="container flex flex-col items-center justify-center gap-3 rounded-lg p-3"
|
||||
>
|
||||
<Button
|
||||
color="light"
|
||||
size="xl"
|
||||
class="{$launching
|
||||
? ''
|
||||
: 'active:scale-95 '}transition-transform duration-75"
|
||||
disabled={$launching}
|
||||
on:click={launch}>Launch</Button
|
||||
>
|
||||
<div
|
||||
class="w-full flex flex-col justify-center items-center gap-2 mt-2 {$launching
|
||||
? 'animate-fadeIn '
|
||||
: 'animate-fadeOut '}{progressbarFix ? '!opacity-0' : 'opacity-0'}"
|
||||
>
|
||||
<Progressbar
|
||||
animate={true}
|
||||
progress={$launchPercentage}
|
||||
indeterminate={$launchPercentage == -1}
|
||||
labelInside={true}
|
||||
size="h-3"
|
||||
class=""
|
||||
labelInsideClass="bg-primary-600 drop-shadow-xl text-gray-100 text-base font-medium text-center p-1 leading-none rounded-full !text-[0.7rem] !leading-[0.45]"
|
||||
/>
|
||||
<p class="m-0 p-0 dark:text-gray-400 font-light">{$launchStatus}</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
233
src/pages/Login.svelte
Normal file
233
src/pages/Login.svelte
Normal file
|
@ -0,0 +1,233 @@
|
|||
<script lang="ts">
|
||||
import Input from "flowbite-svelte/Input.svelte";
|
||||
import Button from "flowbite-svelte/Button.svelte";
|
||||
import Spinner from "flowbite-svelte/Spinner.svelte";
|
||||
import Checkbox from "flowbite-svelte/Checkbox.svelte";
|
||||
import { type User } from "../types/user";
|
||||
import { currentPage, currentUser, startup } from "../storage/localStore";
|
||||
import toast from "svelte-french-toast";
|
||||
import { Page } from "../consts/pages";
|
||||
import EyeSolid from "flowbite-svelte-icons/EyeSolid.svelte";
|
||||
import EyeSlashSolid from "flowbite-svelte-icons/EyeSlashSolid.svelte";
|
||||
|
||||
let loading = false;
|
||||
let username = "";
|
||||
let password = "";
|
||||
let saveCredentials = false;
|
||||
let showPassword = false;
|
||||
|
||||
const processLogin = async () => {
|
||||
if (username.length <= 0 || password.length <= 0) {
|
||||
toast.error(`Please provice a valid Username and Password!`, {
|
||||
position: "bottom-center",
|
||||
className:
|
||||
"dark:!bg-gray-800 border-1 dark:!border-gray-700 dark:!text-gray-100",
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
loading = true;
|
||||
const loginPromise = new Promise<string>((res, rej) => {
|
||||
window.addEventListener(
|
||||
"login-result",
|
||||
async (e) => {
|
||||
const customEvent = e as CustomEvent;
|
||||
const resultData = customEvent.detail;
|
||||
const wasSuccessful = "user" in resultData;
|
||||
|
||||
if (!wasSuccessful) {
|
||||
/* const errorResult = resultData as Error;
|
||||
toast.error(errorResult.message, {
|
||||
position: "bottom-center",
|
||||
className:
|
||||
"dark:!bg-gray-800 border-1 dark:!border-gray-700 dark:!text-gray-100",
|
||||
duration: 1500,
|
||||
}); */
|
||||
rej(resultData.message);
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
const userResult = resultData.user as User;
|
||||
currentUser.set(userResult);
|
||||
currentPage.set(Page.Launch);
|
||||
res("");
|
||||
toast.success(`Welcome back, ${userResult.name}!`, {
|
||||
position: "bottom-center",
|
||||
className:
|
||||
"dark:!bg-gray-800 border-1 dark:!border-gray-700 dark:!text-gray-100",
|
||||
duration: 3000,
|
||||
});
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("login-attempt", {
|
||||
detail: { username, password, saveCredentials },
|
||||
})
|
||||
);
|
||||
});
|
||||
toast.promise(
|
||||
loginPromise,
|
||||
{
|
||||
loading: "Logging in...",
|
||||
success: "Successfully logged in!",
|
||||
error: (e) => e,
|
||||
},
|
||||
{
|
||||
position: "bottom-center",
|
||||
className:
|
||||
"dark:!bg-gray-800 border-1 dark:!border-gray-700 dark:!text-gray-100",
|
||||
duration: 0,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const tryAutoLogin = async () => {
|
||||
loading = true;
|
||||
const loginPromise = new Promise<string>((res, rej) => {
|
||||
window.addEventListener(
|
||||
"login-result",
|
||||
(e) => {
|
||||
const customEvent = e as CustomEvent;
|
||||
const resultData = customEvent.detail;
|
||||
const isGuest = "guest" in resultData;
|
||||
const wasSuccessful = "user" in resultData;
|
||||
if (isGuest) {
|
||||
currentPage.set(Page.Launch);
|
||||
res("");
|
||||
toast.success(`Logged in as Guest`, {
|
||||
position: "bottom-center",
|
||||
className:
|
||||
"dark:!bg-gray-800 border-1 dark:!border-gray-700 dark:!text-gray-100",
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!wasSuccessful) {
|
||||
loading = false;
|
||||
rej(resultData.message);
|
||||
return;
|
||||
}
|
||||
const userResult = resultData.user as User;
|
||||
currentUser.set(userResult);
|
||||
currentPage.set(Page.Launch);
|
||||
res("");
|
||||
toast.success(`Welcome back, ${userResult.name}!`, {
|
||||
position: "bottom-center",
|
||||
className:
|
||||
"dark:!bg-gray-800 border-1 dark:!border-gray-700 dark:!text-gray-100",
|
||||
duration: 3000,
|
||||
});
|
||||
loading = false;
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
window.dispatchEvent(new CustomEvent("autologin-attempt"));
|
||||
});
|
||||
toast.promise(
|
||||
loginPromise,
|
||||
{
|
||||
loading: "Logging in...",
|
||||
success: "Successfully logged in!",
|
||||
error: (e) => e,
|
||||
},
|
||||
{
|
||||
position: "bottom-center",
|
||||
className:
|
||||
"dark:!bg-gray-800 border-1 dark:!border-gray-700 dark:!text-gray-100",
|
||||
duration: 0,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const proceedAsGuest = () => {
|
||||
window.dispatchEvent(new CustomEvent("guest-login"));
|
||||
currentPage.set(Page.Launch);
|
||||
toast.success(`Logged in as Guest`, {
|
||||
position: "bottom-center",
|
||||
className:
|
||||
"dark:!bg-gray-800 border-1 dark:!border-gray-700 dark:!text-gray-100",
|
||||
duration: 3000,
|
||||
});
|
||||
};
|
||||
|
||||
const shouldAutologin = async () => {
|
||||
const shouldAutologin = await new Promise<boolean>((res) => {
|
||||
window.addEventListener("autologin-result", (e) => {
|
||||
const customEvent = e as CustomEvent;
|
||||
const resultData = customEvent.detail;
|
||||
res(resultData);
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent("autologin-active"));
|
||||
});
|
||||
return shouldAutologin;
|
||||
};
|
||||
|
||||
(async () => {
|
||||
if (!$startup) {
|
||||
startup.set(true);
|
||||
if (await shouldAutologin()) tryAutoLogin();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<main
|
||||
class="h-[265px] my-auto flex flex-col justify-center items-center p-5 animate-fadeIn opacity-0"
|
||||
>
|
||||
<div
|
||||
class="container flex flex-col items-center justify-center gap-3 rounded-lg p-3"
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
size="md"
|
||||
class="animate-sideIn"
|
||||
disabled={loading}
|
||||
bind:value={username}
|
||||
/>
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="Password"
|
||||
size="md"
|
||||
class="animate-lsideIn"
|
||||
disabled={loading}
|
||||
bind:value={password}
|
||||
>
|
||||
<Button
|
||||
slot="right"
|
||||
color="none"
|
||||
class="!outline-none !ring-0 !p-0 !m-0 !bg-transparent !border-none"
|
||||
on:click={() => (showPassword = !showPassword)}
|
||||
>
|
||||
{#if showPassword}
|
||||
<EyeSolid class="outline-none border-none" />
|
||||
{:else}
|
||||
<EyeSlashSolid class="outline-none border-none" />
|
||||
{/if}
|
||||
</Button>
|
||||
</Input>
|
||||
<Checkbox bind:checked={saveCredentials} disabled={loading}
|
||||
>Save credentials</Checkbox
|
||||
>
|
||||
<div class="flex flex-col justify-center items-center gap-2 mt-1">
|
||||
<Button
|
||||
class="active:scale-95 transition-transform duration-75"
|
||||
color="light"
|
||||
disabled={loading}
|
||||
on:click={processLogin}
|
||||
>
|
||||
{#if loading}
|
||||
<Spinner size={"5"} color="white"></Spinner>
|
||||
{:else}
|
||||
Login
|
||||
{/if}
|
||||
</Button>
|
||||
<Button
|
||||
class="!bg-transparent font-light border-none dark:text-gray-700 hover:!bg-gray-700/15 ring-primary active:ring-2 focus:ring-2 active:scale-95 transition-transform duration-75"
|
||||
color="none"
|
||||
disabled={loading}
|
||||
on:click={proceedAsGuest}>Continue without login</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
96
src/pages/Settings.svelte
Normal file
96
src/pages/Settings.svelte
Normal file
|
@ -0,0 +1,96 @@
|
|||
<script lang="ts">
|
||||
import Button from "flowbite-svelte/Button.svelte";
|
||||
import ButtonGroup from "flowbite-svelte/ButtonGroup.svelte";
|
||||
import Input from "flowbite-svelte/Input.svelte";
|
||||
import Toggle from "flowbite-svelte/Toggle.svelte";
|
||||
import FileSearchSolid from "flowbite-svelte-icons/FileSearchSolid.svelte";
|
||||
import FolderSolid from "flowbite-svelte-icons/FolderSolid.svelte";
|
||||
import { patch, presence, logging } from "./../storage/localStore";
|
||||
|
||||
let folderPath: string = "";
|
||||
|
||||
window.addEventListener("settings-result", (e) => {
|
||||
const settings: Record<string, string>[] = (e as CustomEvent).detail;
|
||||
const osuPath = settings.find((setting) => setting.key == "osuPath");
|
||||
const settingPatch = settings.find((setting) => setting.key == "patch");
|
||||
const settingPresence = settings.find(
|
||||
(setting) => setting.key == "presence"
|
||||
);
|
||||
const settingLogging = settings.find((setting) => setting.key == "logging");
|
||||
patch.set(settingPatch ? settingPatch.val == "true" : true);
|
||||
presence.set(settingPresence ? settingPresence.val == "true" : true);
|
||||
logging.set(settingLogging ? settingLogging.val == "true" : false);
|
||||
folderPath = osuPath ? osuPath.val : "";
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent("settings-get"));
|
||||
|
||||
const setFolderPath = () => {
|
||||
window.dispatchEvent(new CustomEvent("folder-set"));
|
||||
};
|
||||
|
||||
const detectFolderPath = () => {
|
||||
window.dispatchEvent(new CustomEvent("folder-auto"));
|
||||
};
|
||||
|
||||
const togglePatching = () => {
|
||||
patch.set(!$patch);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("setting-update", { detail: { patch: $patch } })
|
||||
);
|
||||
};
|
||||
|
||||
const togglePresence = () => {
|
||||
presence.set(!$presence);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("setting-update", { detail: { presence: $presence } })
|
||||
);
|
||||
};
|
||||
|
||||
const toggleLogging = () => {
|
||||
logging.set(!$logging);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("setting-update", { detail: { logging: $logging } })
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<main
|
||||
class="h-[265px] flex flex-col justify-start p-3 animate-fadeIn opacity-0"
|
||||
>
|
||||
<div
|
||||
class="container flex flex-col items-center justify-center gap-5 rounded-lg p-3"
|
||||
>
|
||||
<ButtonGroup class="w-full">
|
||||
<Input
|
||||
type="text"
|
||||
id="oip"
|
||||
placeholder="Path to your osu! installation"
|
||||
value={folderPath}
|
||||
readonly
|
||||
/>
|
||||
<Button color="light" on:click={detectFolderPath}>
|
||||
<FileSearchSolid
|
||||
size="sm"
|
||||
class="dark:text-gray-300 text-gray-500 outline-none border-none select-none pointer-events-none"
|
||||
/>
|
||||
</Button>
|
||||
<Button color="light" class="active:!rounded-lg" on:click={setFolderPath}>
|
||||
<FolderSolid
|
||||
size="sm"
|
||||
class="dark:text-gray-300 text-gray-500 outline-none border-none select-none pointer-events-none"
|
||||
/>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 p-3">
|
||||
<Toggle class="w-fit" bind:checked={$presence} on:click={togglePresence}
|
||||
>Discord Presence</Toggle
|
||||
>
|
||||
<Toggle class="w-fit" bind:checked={$patch} on:click={togglePatching}
|
||||
>Patching</Toggle
|
||||
>
|
||||
<Toggle class="w-fit" bind:checked={$logging} on:click={toggleLogging}
|
||||
>Debug Logging</Toggle
|
||||
>
|
||||
</div>
|
||||
</main>
|
17
src/storage/localStore.ts
Normal file
17
src/storage/localStore.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { type Writable, writable } from "svelte/store";
|
||||
import { Page } from "../consts/pages";
|
||||
import type { User } from "../types/user";
|
||||
|
||||
export const startup = writable(false);
|
||||
export const updateAvailable = writable(false);
|
||||
export const launching = writable(false);
|
||||
export const launchStatus = writable("Waiting...");
|
||||
export const launchPercentage = writable(-1);
|
||||
|
||||
export const currentUser: Writable<undefined | User> = writable(undefined);
|
||||
export const currentPage = writable(Page.Login);
|
||||
|
||||
export const osuPath: Writable<undefined | string> = writable(undefined);
|
||||
export const patch = writable(true);
|
||||
export const presence = writable(true);
|
||||
export const logging = writable(false);
|
4
src/types/error.ts
Normal file
4
src/types/error.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export type Error = {
|
||||
code: number;
|
||||
message: string;
|
||||
};
|
5
src/types/images.d.ts
vendored
Normal file
5
src/types/images.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
declare module "*.jpg";
|
||||
declare module "*.jpeg";
|
||||
declare module "*.png";
|
||||
declare module "*.svg";
|
||||
declare module "*.svelte";
|
6
src/types/user.ts
Normal file
6
src/types/user.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export type User = {
|
||||
id: number;
|
||||
donor: boolean;
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
3
src/util/mathUtil.ts
Normal file
3
src/util/mathUtil.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export const clamp = (val: number, min: number, max: number) => {
|
||||
return Math.max(min, Math.min(val, max));
|
||||
};
|
7
svelte.config.js
Normal file
7
svelte.config.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
const preprocess = require("svelte-preprocess");
|
||||
|
||||
const config = {
|
||||
preprocess: [preprocess()],
|
||||
};
|
||||
|
||||
module.exports = config;
|
73
tailwind.config.cjs
Normal file
73
tailwind.config.cjs
Normal file
|
@ -0,0 +1,73 @@
|
|||
/** @type {import('tailwindcss').Config}*/
|
||||
const config = {
|
||||
content: [
|
||||
"./src/**/*.{html,js,svelte,ts}",
|
||||
"./node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}",
|
||||
],
|
||||
darkMode: "media",
|
||||
theme: {
|
||||
extend: {
|
||||
keyframes: {
|
||||
slideIn: {
|
||||
"0%": { opacity: "0", transform: "translateX(-5px)" },
|
||||
"100%": { opacity: "1" },
|
||||
},
|
||||
lslideIn: {
|
||||
"0%": { opacity: "0", transform: "translateX(5px)" },
|
||||
"100%": { opacity: "1" },
|
||||
},
|
||||
fadeIn: {
|
||||
"0%": { opacity: "0", transform: "translateY(5px)" },
|
||||
"100%": { opacity: "1" },
|
||||
},
|
||||
fadeOut: {
|
||||
"100%": { opacity: "0", transform: "translateY(5px)" },
|
||||
"0%": { opacity: "1" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
sideIn: "slideIn 1s ease forwards",
|
||||
lsideIn: "lslideIn 1s ease forwards",
|
||||
fadeIn: "fadeIn 1s ease forwards",
|
||||
fadeOut: "fadeOut 1s ease forwards",
|
||||
},
|
||||
transitionProperty: {
|
||||
"width": "width",
|
||||
},
|
||||
colors: {
|
||||
// flowbite-svelte
|
||||
primary: {
|
||||
DEFAULT: "#FA1C74",
|
||||
50: "#FED0E2",
|
||||
100: "#FEBCD6",
|
||||
200: "#FD94BD",
|
||||
300: "#FC6CA5",
|
||||
400: "#FB448C",
|
||||
500: "#FA1C74",
|
||||
600: "#D90559",
|
||||
700: "#A20442",
|
||||
800: "#6B022C",
|
||||
900: "#340115",
|
||||
950: "#19010A",
|
||||
},
|
||||
gray: {
|
||||
50: "#F9F9F9",
|
||||
100: "#ECECEC",
|
||||
200: "#D3D3D3",
|
||||
300: "#B9B9B9",
|
||||
400: "#A0A0A0",
|
||||
500: "#868686",
|
||||
600: "#6D6D6D",
|
||||
700: "#535353",
|
||||
800: "#393939",
|
||||
900: "#202020",
|
||||
950: "#1A1A1A",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
plugins: [require("flowbite/plugin")],
|
||||
};
|
||||
|
||||
module.exports = config;
|
23
tsconfig.json
Normal file
23
tsconfig.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"extends": "./node_modules/@tsconfig/svelte/tsconfig.json",
|
||||
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"electron/richPresence.js",
|
||||
"electron/config.js",
|
||||
"electron/cryptoUtil.js",
|
||||
"electron/executeUtil.js",
|
||||
"electron/formattingUtil.js",
|
||||
"electron/hwidUtil.js",
|
||||
"electron/osuUtil.js"
|
||||
],
|
||||
"exclude": ["node_modules/*", "__sapper__/*", "public/*"],
|
||||
"compilerOptions": {
|
||||
"typeRoots": [
|
||||
"node_modules/@types",
|
||||
"node_modules/@sveltejs",
|
||||
"node_modules/@sveltejs/types",
|
||||
"src/types"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
const path = require("path");
|
||||
const appInfo = require('../appInfo');
|
||||
const { BrowserWindow } = require('electron');
|
||||
const { attachTitlebarToWindow } = require('custom-electron-titlebar/main');
|
||||
|
||||
module.exports = {
|
||||
createWindow: function (windowWidth, windowHeight) {
|
||||
const window = new BrowserWindow({
|
||||
width: windowWidth,
|
||||
height: windowHeight,
|
||||
minHeight: windowHeight / 1.25,
|
||||
minWidth: windowWidth / 1.25,
|
||||
frame: false,
|
||||
titleBarStyle: 'hidden',
|
||||
backgroundColor: "#121212",
|
||||
resizable: false,
|
||||
maximizable: false,
|
||||
minimizable: true,
|
||||
alwaysOnTop: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true,
|
||||
show: false,
|
||||
zoomFactor: 0.9,
|
||||
preload: path.join(__dirname, '../preload/preload.js')
|
||||
},
|
||||
icon: './assets/logo.png'
|
||||
})
|
||||
window.hide();
|
||||
|
||||
window.webContents.once("did-finish-load", function (event, input) {
|
||||
window.show();
|
||||
});
|
||||
|
||||
window.webContents.setUserAgent(`${appInfo.appName} ${appInfo.appVersion}`);
|
||||
attachTitlebarToWindow(window);
|
||||
// window.webContents.openDevTools({
|
||||
// mode: "detach"
|
||||
// });
|
||||
|
||||
return window;
|
||||
},
|
||||
}
|
Loading…
Reference in New Issue
Block a user