Compare commits

..

100 Commits

Author SHA1 Message Date
HorizonCode ecf329dd69 Merge pull request 'add optional logging, cleanup fixes' (#15) from dev into master
Reviewed-on: #15
2024-04-15 11:12:10 +00:00
HorizonCode 4e79809c19 bump version 2024-04-15 13:11:52 +02:00
HorizonCode f06e63f7f8 remove unused import 2024-04-15 13:07:51 +02:00
HorizonCode 638f1e852e add optional logging, cleanup fixes 2024-04-15 13:07:40 +02:00
HorizonCode 8b30b7c1fa fix .net8 detection 2024-04-14 17:55:09 +02:00
HorizonCode 2bb4a86df3 Merge pull request 'dev' (#14) from dev into master
Reviewed-on: #14
2024-04-14 15:32:58 +00:00
HorizonCode f41ca92711 update readme 2024-04-14 17:32:11 +02:00
HorizonCode 65a3e86261 bump version 2024-04-14 17:31:37 +02:00
HorizonCode c74bc57453 remove test 2024-04-14 17:17:07 +02:00
HorizonCode 528af70446 add .net8 check when patch is enabled 2024-04-14 17:16:46 +02:00
HorizonCode 6f2764a047 patcher update, no more hardcoded urls, launch fixes 2024-04-14 16:36:32 +02:00
HorizonCode 1c4a40c495 add mode and global rank to rpc 2024-03-16 18:48:44 +01:00
HorizonCode 144d1bb86a Merge pull request 'bump version' (#13) from dev into master
Reviewed-on: #13
2024-03-11 18:36:17 +00:00
HorizonCode a9de377456 bump version 2024-03-11 19:35:54 +01:00
HorizonCode c3f0882951 Merge pull request 'add dynamic beatmap images to presence' (#12) from dev into master
Reviewed-on: #12
2024-03-11 17:55:54 +00:00
HorizonCode c17cbc48d8 fix user reset 2024-03-11 18:46:04 +01:00
HorizonCode 513692c2d5 reset user on exit 2024-03-11 18:37:33 +01:00
HorizonCode 90717ed960 display user as small image, fix dynamic beatmap images 2024-03-11 18:06:24 +01:00
HorizonCode 6bcce04b72 Merge branch 'dev' of https://git.ez-pp.farm/EZPPFarm/EZPPLauncher into dev 2024-03-11 17:34:38 +01:00
HorizonCode 8d2024aa0a check if rpc is connected 2024-03-11 17:34:36 +01:00
HorizonCode 22815e74b6 check if beatmap is not null 2024-03-11 15:46:07 +01:00
HorizonCode c4d9862860 assign the cover url to a const 2024-03-11 15:42:08 +01:00
HorizonCode d56d4875e0 add dynamic beatmap image to presence 2024-03-11 15:38:20 +01:00
HorizonCode 4c33323e9e Merge pull request 'oops' (#10) from dev into master
Reviewed-on: #10
2024-01-30 08:52:43 +00:00
HorizonCode da8e237679 oops 2024-01-30 09:51:23 +01:00
HorizonCode eb166c0165 Merge pull request 'bump patch version' (#9) from dev into master
Reviewed-on: #9
2024-01-29 07:58:55 +00:00
HorizonCode c4cd8fed12 bump patch version 2024-01-29 08:58:42 +01:00
HorizonCode 6bca0b32a9 Merge pull request 'disable unhandled error logging for now' (#8) from dev into master
Reviewed-on: #8
2024-01-29 07:57:48 +00:00
HorizonCode 72d466b1ec disable unhandled error logging for now 2024-01-29 08:57:28 +01:00
HorizonCode 401be3c977 Merge pull request 'remove console logging, add unhandled error dialog' (#7) from dev into master
Reviewed-on: #7
2024-01-25 13:20:12 +00:00
HorizonCode 0b679d320f remove console logging 2024-01-25 14:19:35 +01:00
HorizonCode 5b1407638a add dialog for unhandled errors 2024-01-25 14:10:18 +01:00
HorizonCode d2d2a618c5 Merge pull request 'bump version, disable image drag' (#6) from dev into master
Reviewed-on: #6
2024-01-25 11:05:15 +00:00
HorizonCode b85a3a8795 bump version, disable image drag 2024-01-25 12:04:43 +01:00
HorizonCode fc6ec3bd69 Merge pull request 'remove class overrides' (#5) from dev into master
Reviewed-on: #5
2024-01-25 09:23:35 +00:00
HorizonCode 4c26689f4e Merge branch 'master' into dev 2024-01-25 09:23:21 +00:00
HorizonCode 3d2ebdb640 remove class overrides 2024-01-25 10:23:03 +01:00
HorizonCode 9f3c4d4fa4 Merge pull request 'only run osu updater when launcher pulled updates' (#4) from dev into master
Reviewed-on: #4
2024-01-25 08:34:32 +00:00
HorizonCode 0567d57cb5 only run osu updater when launcher pulled updates 2024-01-25 09:34:15 +01:00
HorizonCode d907553f3e Merge pull request 'Update License' (#3) from dev into master
Reviewed-on: #3
2024-01-25 08:33:53 +00:00
HorizonCode 0e0676917a Merge branch 'dev' of https://git.ez-pp.farm/EZPPFarm/EZPPLauncher into dev 2024-01-25 09:33:13 +01:00
HorizonCode 51c4be0873 Update License 2024-01-25 09:33:05 +01:00
HorizonCode 8b4fb07f21 Merge pull request 'allow only a single instance, fix updating' (#2) from dev into master
Reviewed-on: #2
2024-01-25 08:28:58 +00:00
HorizonCode 3b658fd8de Merge branch 'master' into dev 2024-01-25 08:28:11 +00:00
HorizonCode 2329122ef0 fix updating task 2024-01-25 08:54:02 +01:00
HorizonCode 4348c36fa0 launch osu updater before launching to verify all files 2024-01-24 16:31:34 +01:00
HorizonCode 8c09719b43 remove force update files created by osu 2024-01-23 15:34:08 +01:00
HorizonCode 03b352bc15 allow only a single instance 2024-01-23 13:28:35 +01:00
HorizonCode 0f5eab873f Merge pull request 'disable osu integrated rpc when launcher has active rpc' (#1) from next into master
Reviewed-on: #1
2024-01-22 15:26:19 +00:00
HorizonCode 75d444b1d5 disable osu integrated rpc when launcher has active rpc 2024-01-22 16:25:29 +01:00
HorizonCode 779ff0c009 update readme 2024-01-22 13:03:30 +01:00
HorizonCode dc88e9f2cc remove console logs 2024-01-22 12:53:59 +01:00
HorizonCode bf8a458b5f finish update dialog 2024-01-22 12:37:29 +01:00
HorizonCode 3570582c6b remove requiring admin, this caused otd to not work 2024-01-22 12:06:22 +01:00
HorizonCode b9bd87c099 remove old files before replacing them 2024-01-22 12:06:04 +01:00
HorizonCode 705e47a1b8 this commit should fix all the previous issues, also peppy fixed osu!.exe patch update 2024-01-22 12:00:50 +01:00
HorizonCode 397c2255c1 reset loaded status when exited 2024-01-21 17:20:26 +01:00
HorizonCode 00d71704ab update fixes 2024-01-20 01:27:22 +01:00
HorizonCode f631c78805 remove sweetalert2 2024-01-19 16:25:27 +01:00
HorizonCode 8d61a8b2b7 preperations for update dialog 2024-01-19 16:24:39 +01:00
HorizonCode 61d5182854 add toggles for presence and patch 2024-01-19 15:04:58 +01:00
HorizonCode cef085c13d fix patching 2024-01-18 19:47:56 +01:00
HorizonCode a02737ef1b remove unused module 2024-01-18 19:47:39 +01:00
HorizonCode ae566f09a0 Merge branch 'next' of https://git.ez-pp.farm/EZPPFarm/EZPPLauncher into next 2024-01-18 18:14:57 +01:00
HorizonCode 01905d695a switch details and state 2024-01-18 18:12:55 +01:00
HorizonCode 94fa7e7bd8 fix autologin, add osu folder detection 2024-01-18 16:32:57 +01:00
HorizonCode d5b2b8012c fix autologin toasts 2024-01-18 14:10:47 +01:00
HorizonCode 05866946a3 do rebuild before packing 2024-01-18 12:52:06 +01:00
HorizonCode 0fc9cb828b re-add pre scripts for deploy 2024-01-18 12:47:52 +01:00
HorizonCode e119166c65 add animations to header 2024-01-18 12:47:14 +01:00
HorizonCode 6aa0a6c969 add modules to electron export, fix issues when downloading updates 2024-01-18 12:43:46 +01:00
HorizonCode 6369c0e8af added discord rpc 2024-01-14 00:21:33 +01:00
HorizonCode f11e84efd7 added launching, patching, and ui override 2024-01-13 23:41:40 +01:00
HorizonCode d9fec1193e file update check, also on launch 2024-01-12 16:10:19 +01:00
HorizonCode 2c6b51cbb2 settings now working 2024-01-12 14:19:00 +01:00
HorizonCode 756ae1be58 progress status and progress communication done 2024-01-12 12:09:28 +01:00
HorizonCode 4569d35a65 rework progressbar 2024-01-12 12:09:11 +01:00
HorizonCode 1adb1b7dec start launch function 2024-01-11 22:50:01 +01:00
HorizonCode ad2c91db4d disable futher actions when launching 2024-01-11 16:17:32 +01:00
HorizonCode 3588203527 change launch/patch to storage property 2024-01-11 16:15:56 +01:00
HorizonCode dfa933fb87 make input readonly 2024-01-11 16:09:38 +01:00
HorizonCode 2da2c9fa7e remove unused code and imports 2024-01-11 16:09:25 +01:00
HorizonCode 145f36f39b add Settings page 2024-01-11 15:56:02 +01:00
HorizonCode 67cf564b01 add password showing option 2024-01-11 15:55:56 +01:00
HorizonCode 4bd130aad9 add rollup plugin to detect unused imports 2024-01-11 15:55:35 +01:00
HorizonCode d0937f626d login is functional now 2024-01-11 12:59:52 +01:00
HorizonCode 05b9ddd5a1 add login mechanic 2024-01-11 01:00:43 +01:00
HorizonCode 6790fe5ef6 kms electron 2024-01-10 16:26:45 +01:00
HorizonCode 1f1dc65a97 make darkmode dependent on system color scheme 2024-01-10 14:50:58 +01:00
HorizonCode bbc3d7ed67 upgrade electron to latest stable release 2024-01-10 13:28:20 +01:00
HorizonCode 917edf0eb3 change icons 2024-01-10 13:28:00 +01:00
HorizonCode cfeda18b83 disable text selection 2024-01-10 13:03:43 +01:00
HorizonCode 0eb42c138b add custom progressbar, adjust icons 2024-01-10 13:02:43 +01:00
HorizonCode a4faa9dd1a add proper dark/light mode 2024-01-09 14:49:22 +01:00
HorizonCode 800a424b67 yes 2024-01-09 14:49:05 +01:00
HorizonCode 873aeaf921 update primary color 2024-01-09 14:48:58 +01:00
HorizonCode 310c206367 reformat, remove unused imports 2024-01-09 14:48:47 +01:00
HorizonCode 895b71ef16 adjust window size, enable dev tools in dev 2024-01-09 14:47:28 +01:00
HorizonCode e8f0027424 convert to ts pt.2 2024-01-09 13:48:27 +01:00
HorizonCode c6fa540575 revert to js 2024-01-09 13:10:37 +01:00
64 changed files with 10321 additions and 5421 deletions
-50
View File
@@ -1,50 +0,0 @@
name: Compile and Release
on:
release:
types: [released]
jobs:
build-linux:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run rebuild
- run: npm run pack-linux
- name: Add Artifact to Workflow
uses: actions/upload-artifact@v3
with:
name: artifact
path: |
release/*.AppImage
build-win:
runs-on: windows-latest
strategy:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run rebuild
- run: npm run pack-win
- name: Add Artifact to Workflow
uses: actions/upload-artifact@v3
with:
name: artifact
path: |
release/*.exe
+23 -3
View File
@@ -1,4 +1,24 @@
node_modules/
release/
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Lock files
yarn.lock
pnpm-lock.yaml
# Dependency directories
node_modules/
# Svelte Distribution
public/build/
# Electron Distribution
dist
# Project Build Automation Directory
private
# Desktop Services Store on macOS
.DS_Store
+3
View File
@@ -0,0 +1,3 @@
{
"recommendations": ["svelte.svelte-vscode"]
}
-17
View File
@@ -1,17 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
},
"args" : ["."],
"outputCapture": "std"
}
]
}
+9 -9
View File
@@ -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.
+9 -18
View File
@@ -1,36 +1,27 @@
# EZPPLauncher
Welcome to the EZPPLauncher! A new way to connect to the EZPPFarm server.
Just one click and you are ready to go!
## Preview Image
![preview](https://git.ez-pp.farm/EZPPFarm/EZPPLauncher/raw/branch/master/preview/EZPPLauncher.png)
## 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
## Used Libraries
* [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)
* [discord-auto-rpc](https://www.npmjs.com/package/discord-auto-rpc)
* [get-window-by-name](https://www.npmjs.com/package/get-window-by-name)
* [sweetalert2](https://www.npmjs.com/package/sweetalert2)
* [node-downloader-helper](https://www.npmjs.com/package/node-downloader-helper)
- Automatic osu! client updating before Launch
- Custom osu! Logo in MainMenu
- Relax misses and much more
- Account saving
## Build from source
- clone repo
- cd into the repo
- use `yarn install` to install all dependencies
- use the buildscript `pack-win` to build a executeable
- 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/)
-412
View File
@@ -1,412 +0,0 @@
const { app, BrowserWindow, ipcMain, dialog, Tray, Menu } = require('electron');
const { setupTitlebar } = require('custom-electron-titlebar/main');
const windowManager = require('./ui/windowManager');
const osuUtil = require('./osuUtil');
const ezppUtil = require('./ezppUtil');
const config = require('./config');
const fs = require('fs');
const path = require('path');
const rpc = require('./discordPresence');
const windowName = require('get-window-by-name');
const terminalUtil = require('./terminalUtil');
const osUtil = require('./osUtil');
const appInfo = require('./appInfo');
const { DownloaderHelper } = require('node-downloader-helper');
let tempOsuPath;
let osuWindowInfo;
let isIngame;
const platform = process.platform;
let linuxWMCtrlFound = false;
let osuLoaded = false;
const run = () => {
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
app.quit();
return;
}
setInterval(async () => {
if (platform == "win32") {
osuWindowInfo = windowName.getWindowText("osu!.exe");
const firstInstance = osuWindowInfo[0];
if (firstInstance) {
if (firstInstance.processTitle && firstInstance.processTitle.length > 0) {
if (!osuLoaded) {
osuLoaded = true;
//TODO: do patch
}
const windowTitle = firstInstance.processTitle;
let rpcOsuVersion = "";
let currentMap = undefined;
if (!windowTitle.includes("-")) {
rpcOsuVersion = windowTitle;
rpc.updateState("Idle...");
} else {
var string = windowTitle;
var components = string.split(' - ');
const splitArray = [components.shift(), components.join(' - ')];
rpcOsuVersion = splitArray[0];
currentMap = splitArray[1];
if (currentMap.endsWith(".osu")) {
rpc.updateState("Editing...");
currentMap = currentMap.substring(0, currentMap.length - 4);
} else rpc.updateState("Playing...");
}
rpc.updateStatus(currentMap, rpcOsuVersion);
} else {
if (osuLoaded) osuLoaded = false;
rpc.updateState("Idle in Launcher...");
rpc.updateStatus(undefined, undefined);
}
} else {
if (osuLoaded) osuLoaded = false;
rpc.updateState("Idle in Launcher...");
rpc.updateStatus(undefined, undefined);
}
} else if (platform == "linux") {
if (!linuxWMCtrlFound) {
if (isIngame) {
rpc.updateState("Playing...");
rpc.updateStatus("Clicking circles!", "runningunderwine");
} else {
rpc.updateState("Idle in Launcher...");
rpc.updateStatus(undefined, undefined);
}
return;
}
const processesOutput = await terminalUtil.execCommand(`wmctrl -l|awk '{$3=""; $2=""; $1=""; print $0}'`);
const allLines = processesOutput.split("\n");
const filteredProcesses = allLines.filter((line) => line.trim().startsWith("osu!"));
if (filteredProcesses.length > 0) {
const windowTitle = filteredProcesses[0].trim();
let rpcOsuVersion = "";
let currentMap = undefined;
if (!windowTitle.includes("-")) {
rpcOsuVersion = windowTitle;
rpc.updateState("Idle...");
} else {
var string = windowTitle;
var components = string.split(' - ');
const splitArray = [components.shift(), components.join(' - ')];
rpcOsuVersion = splitArray[0];
currentMap = splitArray[1];
if (currentMap.endsWith(".osu")) {
rpc.updateState("Editing/Modding...");
currentMap = currentMap.substring(0, currentMap.length - 4);
}
else rpc.updateState("Playing...");
}
rpc.updateStatus(currentMap, rpcOsuVersion);
} else {
rpc.updateState("Idle in Launcher...");
rpc.updateStatus(undefined, undefined);
}
} else {
if (isIngame) {
rpc.updateState("Playing...");
rpc.updateStatus("Clicking circles!", "runningunderwine");
} else {
rpc.updateState("Idle in Launcher...");
rpc.updateStatus(undefined, undefined);
}
}
}, 2000);
setupTitlebar();
rpc.connect();
let mainWindow;
let tray = null
app.whenReady().then(async () => {
const logoFile = path.join(config.configFolder, "logo.png");
if (!fs.existsSync(logoFile)) {
const logoDownload = new DownloaderHelper("https://ez-pp.farm/assets/img/icon.png", config.configFolder, {
fileName: "logo.png",
})
await logoDownload.start();
}
tray = new Tray(logoFile);
const trayMenuTemplate = [
{
label: `EZPPLauncher ${appInfo.appVersion}`,
enabled: false
},
{
label: "Show/Hide",
click: function () {
if (mainWindow.isVisible()) mainWindow.hide();
else mainWindow.show();
}
},
{
label: 'Exit',
click: function () {
if (isIngame) return;
app.exit(0);
}
}
]
let trayMenu = Menu.buildFromTemplate(trayMenuTemplate)
tray.setContextMenu(trayMenu)
mainWindow = createWindow();
mainWindow.on('close', (e) => {
if (isIngame) e.preventDefault();
});
mainWindow.once('show', async () => {
await updateConfigVars(mainWindow);
await tryLogin(mainWindow);
await doUpdateCheck(mainWindow);
if (platform === "linux") {
const linuxDistroInfo = await osUtil.getLinuxDistroInfo();
if (linuxDistroInfo?.id != "arch") {
if (linuxDistroInfo?.id_like != "arch") {
mainWindow.webContents.send('status_update', {
type: "info",
message: "We detected that you are running the Launcher under Linux. It's currently just compatible with Arch like distributions!"
});
}
}
try {
await terminalUtil.execCommand(`osu-stable -h`);
} catch (err) {
mainWindow.webContents.send('status_update', {
type: "package-issue",
message: "Seems like you dont have the osu AUR Package installed, please install it."
});
return;
}
/* mainWindow.webContents.send('status_update', {
type: "info",
message: "We detected that you are running the Launcher under Linux. It's currently just compatible with Arch and the osu AUR package!"
}); */
const terminalTest = await terminalUtil.execCommand(`wmctrl -l|awk '{$3=""; $2=""; $1=""; print $0}'`);
const isFailed = terminalTest.trim() == "";
if (isFailed) {
mainWindow.webContents.send('status_update', {
type: "info",
message: "Seems like you are missing the wmctrl package, please install it for the RPC to work!"
});
} else linuxWMCtrlFound = true;
} else {
const osuFolder = await config.get("osuPath");
if (!osuFolder || osuFolder == "") {
const foundFolder = await osuUtil.findOsuInstallation();
if (foundFolder && osuUtil.isValidOsuFolder(foundFolder)) {
mainWindow.webContents.send('alert_message', foundFolder);
}
}
}
});
ipcMain.on("alert_response", async (event, path) => {
await config.set("osuPath", path);
})
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) mainWindow = createWindow();
})
app.on('window-all-closed', () => {
app.quit()
})
ipcMain.handle('launch', async () => {
const osuFolder = await config.get("osuPath");
await osuUtil.updateOsuCfg(path.join(osuFolder, "osu!.cfg"));
const osuConfig = await osuUtil.getLatestConfig(tempOsuPath);
const username = await config.get('username');
const password = await config.get('password');
if (password && username) {
await osuUtil.setConfigValue(osuConfig.path, "SaveUsername", "1");
await osuUtil.setConfigValue(osuConfig.path, "SavePassword", "1");
await osuUtil.setConfigValue(osuConfig.path, "Username", username);
await osuUtil.setConfigValue(osuConfig.path, "Password", password);
await osuUtil.setConfigValue(osuConfig.path, "CredentialEndpoint", "ez-pp.farm");
} else {
await osuUtil.setConfigValue(osuConfig.path, "Username", "");
await osuUtil.setConfigValue(osuConfig.path, "Password", "");
}
await osuUtil.replaceUI(osuFolder, true);
rpc.updateState("Launching osu!...");
isIngame = true;
mainWindow.closable = false;
if (mainWindow.isVisible()) mainWindow.hide();
const result = await osuUtil.startOsuWithDevServer(tempOsuPath, "ez-pp.farm", async () => {
if (!mainWindow.isVisible()) mainWindow.show();
setTimeout(async () => {
isIngame = false;
await osuUtil.replaceUI(osuFolder, false);
await doUpdateCheck(mainWindow);
mainWindow.closable = true;
}, 2000);
});
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"
})
});
downloadTask.on('error', () => {
mainWindow.webContents.send('status_update', {
type: "error",
message: "An error occured while updating."
});
});
} 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, folderPath };
})
ipcMain.handle('perform-login', async (event, data) => {
const { username, password } = data;
const loginData = await ezppUtil.performLogin(username, password);
if (loginData && loginData.code === 200) {
await config.set("username", username);
await config.set("password", password);
}
return loginData;
})
ipcMain.on('perform-logout', async (event) => {
await config.remove("username");
await config.remove("password");
})
})
}
async function updateConfigVars(window) {
const osuPath = await config.get("osuPath", "");
window.webContents.send('config_update', {
osuPath: osuPath
})
}
async function tryLogin(window) {
const username = await config.get("username", "");
const password = await config.get("password", "");
if ((username && username.length > 0) && (password && password.length > 0)) {
const passwordPlain = password;
const loginResponse = await ezppUtil.performLogin(username, passwordPlain);
if (loginResponse && loginResponse.code === 200) {
window.webContents.send('account_update', {
type: "loggedin",
user: loginResponse.user
});
} else {
await config.remove("username");
await config.remove("password");
window.webContents.send('account_update', {
type: "login-failed",
message: loginResponse.message
})
}
} else {
window.webContents.send('account_update', {
type: "not-loggedin"
})
}
const checkUpdate = await appInfo.hasUpdate();
if (checkUpdate) {
window.webContents.send('launcher_update', checkUpdate);
}
}
async function doUpdateCheck(window) {
const osuPath = await config.get("osuPath");
if (!osuPath || osuPath.trim == "") {
window.webContents.send('status_update', {
type: "missing-folder"
})
return;
}
const isValid = await osuUtil.isValidOsuFolder(osuPath);
if (!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(700, 460);
win.loadFile('./html/index.html');
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();
-20
View File
@@ -1,20 +0,0 @@
const { default: axios } = require("axios");
const { compareVersions } = require("compare-versions");
const appName = "EZPPLauncher"
const appVersion = "1.1.5";
const hasUpdate = async () => {
const releaseInfo = await axios.get(`https://git.ez-pp.farm/api/v1/repos/EZPPFarm/${appName}/releases/latest`);
if (releaseInfo.status !== 200) return false;
const latestReleaseVersion = releaseInfo.data.tag_name;
const updateAvailable = compareVersions(latestReleaseVersion, appVersion);
if(updateAvailable > 0)
return {
version: latestReleaseVersion,
url: releaseInfo.data.html_url,
}
return undefined;
}
module.exports = { appName, appVersion, hasUpdate };
-85
View File
@@ -1,85 +0,0 @@
[type="checkbox"]:not(:checked),
[type="checkbox"]:checked {
position: absolute;
left: 0;
opacity: 0.01;
}
[type="checkbox"]:not(:checked)+label,
[type="checkbox"]:checked+label {
position: relative;
padding-left: 2.3em;
font-size: 1.05em;
line-height: 1.7;
cursor: pointer;
}
/* checkbox aspect */
[type="checkbox"]:not(:checked)+label:before,
[type="checkbox"]:checked+label:before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 1.4em;
height: 1.4em;
border: 1px solid #565656;
background: #4c4c4c;
border-radius: .2em;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, .1), 0 0 0 hsla(var(--main-accent), 93%, 48%, 20%);
-webkit-transition: all .275s;
transition: all .275s;
}
/* checked mark aspect */
[type="checkbox"]:not(:checked)+label:after,
[type="checkbox"]:checked+label:after {
content: '✕';
position: absolute;
top: .525em;
left: .18em;
font-size: 1.375em;
font-weight: bolder;
color: hsl(var(--main-accent), 93%, 48%);
line-height: 0;
-webkit-transition: all .2s;
transition: all .2s;
}
/* checked mark aspect changes */
[type="checkbox"]:not(:checked)+label:after {
opacity: 0;
-webkit-transform: scale(0) rotate(45deg);
transform: scale(0) rotate(45deg);
}
[type="checkbox"]:checked+label:after {
opacity: 1;
-webkit-transform: scale(1) rotate(0);
transform: scale(1) rotate(0);
translate: -1.5px -1px;
}
/* Disabled checkbox */
[type="checkbox"]:disabled:not(:checked)+label:before,
[type="checkbox"]:disabled:checked+label:before {
box-shadow: none;
border-color: #565656;
background-color: #4c4c4c;
cursor: not-allowed;
}
[type="checkbox"]:disabled:checked+label:after {
color: #777;
}
[type="checkbox"]:disabled+label {
color: #aaa;
cursor: not-allowed;
}
/* Accessibility */
[type="checkbox"]:checked:focus+label:before,
[type="checkbox"]:not(:checked):focus+label:before {
box-shadow: inset 0 1px 3px rgba(0, 0, 0, .1), 0 0 0 6px hsla(var(--main-accent), 93%, 48%, 20%);
}
-335
View File
@@ -1,335 +0,0 @@
@import url('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&display=swap');
:root {
--main-accent: 335deg;
--main-bg: 230deg;
}
* {
font-family: 'Exo 2', 'Roboto' !important;
}
body {
background-color: hsl(var(--main-bg), 24%, 19%);
}
p.text-white {
margin: .5rem 0;
color: white;
}
.quotetext {
color: white;
background-color: #727272;
border-radius: .35rem;
font-size: .95rem;
padding: .25rem .5rem;
}
.sections {
display: flex;
flex-flow: column;
}
.app-name {
font-size: 40px;
font-weight: 500;
text-shadow: 0 0 15px rgba(255, 255, 255, 0.37);
}
.launcher-window {
width: 100%;
}
.launch-button-section {
display: flex;
flex-direction: column;
}
.launch-button-section button {
margin-bottom: 10px;
}
.loading-section,
.login-section {
margin-top: 35px;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
}
.loading-section {
margin-top: 0;
}
.loading-section {}
.loading-indicator {}
.loading-indicator-text {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
}
.folder-section {
margin-top: 50px;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
color: #aaa;
font-weight: 500;
}
.folder-action {
font-style: italic;
cursor: pointer;
width: fit-content;
line-height: .9;
}
.launch-section {
width: 100%;
display: flex;
flex-flow: column;
align-items: center;
justify-content: center;
}
.btn.btn-launch {
margin-top: 10px;
width: 300px;
font-size: 23px;
text-transform: none;
}
.account-section {
display: flex;
align-items: center;
gap: 20px;
}
.server-logo img {
user-select: none;
pointer-events: none;
}
.user-info {
text-align: start;
display: flex;
flex-flow: column;
justify-content: center;
align-content: center;
overflow: hidden;
text-overflow: ellipsis;
}
.user-info .welcome-text {
font-size: 31px;
letter-spacing: .02rem;
height: 30px;
line-height: .8;
text-shadow: 0 0 10px rgba(0, 0, 0, 0.349);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-info .account-action {
font-size: 21px;
letter-spacing: .05rem;
font-weight: 500;
font-style: italic;
color: #aaa;
text-shadow: 0 0 10px rgba(0, 0, 0, 0.349);
cursor: pointer;
line-height: 1.2;
width: fit-content;
}
.user-image {
border: 5px solid white;
border-radius: 0.4rem;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.349);
background-image: url(https://a.ez-pp.farm/0);
width: 100px;
height: 100px;
background-size: cover;
background-position: center;
}
.form-outline .form-control:focus~.form-notch .form-notch-leading {
border-color: hsl(var(--main-accent), 93%, 48%);
box-shadow: -1px 0 0 0 hsl(var(--main-accent), 93%, 48%), 0 1px 0 0 hsl(var(--main-accent), 93%, 48%), 0 -1px 0 0 hsl(var(--main-accent), 93%, 48%);
}
.form-outline .form-control:focus~.form-notch .form-notch-middle {
border-color: hsl(var(--main-accent), 93%, 48%);
box-shadow: 0 1px 0 0 hsl(var(--main-accent), 93%, 48%);
border-top: 1px solid transparent;
}
.form-outline .form-control:focus~.form-notch .form-notch-trailing {
border-color: hsl(var(--main-accent), 93%, 48%);
box-shadow: 1px 0 0 0 hsl(var(--main-accent), 93%, 48%), 0 -1px 0 0 hsl(var(--main-accent), 93%, 48%), 0 1px 0 0 hsl(var(--main-accent), 93%, 48%);
}
.form-outline .form-control:focus~.form-label {
color: hsl(var(--main-accent), 93%, 48%);
}
.btn-accent,
.btn-accent:active {
background: hsl(var(--main-accent), 93%, 48%);
color: #fff;
}
.btn-accent:hover {
background: hsl(var(--main-accent), 93%, 42%);
color: #fff;
}
.btn-grey,
.btn-grey:active {
background: #727272;
color: #fff;
}
.btn-grey:hover {
background: #626262;
color: #fff;
}
.btn-grouped {
display: flex;
gap: 50px;
}
.clickable {
cursor: pointer;
}
.loader {
position: relative;
margin: 0px auto;
width: 200px;
height: 200px;
transform: scale(0.5);
}
.loader:before {
content: '';
display: block;
padding-top: 100%;
}
.circular-loader {
-webkit-animation: rotate 2s linear infinite;
animation: rotate 2s linear infinite;
height: 100%;
-webkit-transform-origin: center center;
-ms-transform-origin: center center;
transform-origin: center center;
width: 100%;
position: absolute;
top: 0;
left: 0;
margin: auto;
}
.loader-path {
stroke-dasharray: 150, 200;
stroke-dashoffset: -10;
-webkit-animation: dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite;
animation: dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite;
stroke-linecap: round;
}
@-webkit-keyframes rotate {
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes rotate {
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-webkit-keyframes dash {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -35;
}
100% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -124;
}
}
@keyframes dash {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -35;
}
100% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -124;
}
}
@-webkit-keyframes color {
0% {
stroke: #fff;
}
40% {
stroke: #fff;
}
66% {
stroke: #fff;
}
80%,
90% {
stroke: #fff;
}
}
@keyframes color {
0% {
stroke: #fff;
}
40% {
stroke: #fff;
}
66% {
stroke: #fff;
}
80%,
90% {
stroke: #fff;
}
}
-23
View File
File diff suppressed because one or more lines are too long
-20
View File
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
-71
View File
@@ -1,71 +0,0 @@
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 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'));
}
async function remove(key) {
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);
}
}
const arr = [];
for (var [storkey, storvalue] of configValues.entries()) {
if (storkey != key)
arr.push(`${storkey}=${storvalue}`);
}
await fs.promises.writeFile(configLocation, arr.join('\n'));
}
module.exports = { get, set, remove, configFolder }
-59
View File
@@ -1,59 +0,0 @@
const appInfo = require('./appInfo.js');
const DiscordAutoRPC = require("discord-auto-rpc");
const clientId = "1032772293220384808";
let client = undefined;
let lastState = "Idle in Launcher...";
let presenceEnabled = true;
let startDate = new Date();
const actionButtons = [
{
label: "Download the Launcher",
url: "https://git.ez-pp.farm/EZPPFarm/EZPPLauncher/releases/latest"
},
{
label: "Join EZPPFarm",
url: "https://ez-pp.farm/discord"
}
]
let lastActivity = {
details: " ",
state: lastState,
startTimestamp: startDate,
largeImageKey: "ezppfarm",
largeImageText: appInfo.appName + " " + appInfo.appVersion,
buttons: actionButtons,
instance: false,
};
module.exports = {
connect: () => {
if (client === undefined) {
client = new DiscordAutoRPC.AutoClient({ transport: "ipc" });
client.endlessLogin({ clientId: clientId });
client.once("ready", () => {
setInterval(() => {
if (lastActivity !== undefined)
lastActivity.state = lastState;
client.setActivity(presenceEnabled ? lastActivity : undefined);
}, 2500);
});
}
},
enablePresence: () => presenceEnabled = true,
disablePresence: () => presenceEnabled = false,
updateStartDate: () => startDate = new Date(),
updateState: (state) => lastState = state,
updateStatus: (details, osuVersion) => {
lastActivity = {
details: details ? details : " ",
state: lastState,
startTimestamp: startDate,
smallImageKey: osuVersion ? "osu" : " ",
smallImageText: osuVersion ? osuVersion : " ",
largeImageKey: "ezppfarm",
largeImageText: appInfo.appName + " " + appInfo.appVersion,
buttons: actionButtons,
instance: false,
}
}
}
+4
View File
@@ -0,0 +1,4 @@
const appName = "EZPPLauncher";
const appVersion = "2.1.5";
module.exports = { appName, appVersion };
+53
View 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
View 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
View 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
View 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
View 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 };
+32
View File
@@ -0,0 +1,32 @@
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");
/**
* Returns machine hardware id.
* Returns `undefined` if cannot determine.
* @return {string?}
*/
function getHwId() {
const getter = platforms[process.platform];
if (!getter) return;
const result = getter[1].exec(child_process.execSync(getter[0], options));
if (!result) return;
return crypto.createHash("md5").update(result[1]).digest("hex") ||
undefined;
}
exports.getHwId = getHwId;
+15
View File
@@ -0,0 +1,15 @@
async function checkImageExists(url) {
try {
const response = await fetch(url, { method: "HEAD" });
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
View 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
View 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 };
+434
View File
@@ -0,0 +1,434 @@
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);
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",
});
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;
}
async function downloadEZPPLauncherUpdateFiles(osuPath, updateFiles) {
const eventEmitter = new EventEmitter();
const startDownload = async () => {
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
View 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,
};
+25
View File
@@ -0,0 +1,25 @@
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);
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,
};
-18
View File
@@ -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()
}
}
-18
View File
@@ -1,18 +0,0 @@
const axios = require('axios').default;
const loginCheckEndpoint = 'https://ez-pp.farm/login/check';
let retries = 0;
const performLogin = async (username, password) => {
const result = await axios.post(loginCheckEndpoint, { username, password });
const code = result.data.code ?? 404;
if (code === 200 || code === 403) {
retries = 0;
return result.data;
} else {
if (retries++ >= 5) return { code: 403, message: "Login failed." }
return await performLogin(username, password);
}
}
module.exports = { performLogin };
-15
View File
@@ -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 };
-121
View File
@@ -1,121 +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" />
<link href="../assets/launcher.css" rel="stylesheet" />
<link href="../assets/checkbox.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-1 w-100 text-center text-lg-end d-flex align-items-center justify-content-center"
style="border-radius: 0.5em;">
<div class="launcher-window position-relative overflow-hidden">
<div class="container px-1 py-2 w-100 mw-100 h-100">
<div class="row d-flex justify-content-center align-items-center h-100">
<div id="loading-page" class="sections col col-xl-10">
<div class="launch-section flex-row">
<div class="server-logo">
<img src="../assets/logo.png" height="120">
</div>
<div class="app-name">EZPPLauncher</div>
</div>
<div class="loading-section">
<div class="loading-indicator">
<div class="loader">
<svg class="circular-loader" viewBox="25 25 50 50">
<circle class="loader-path" cx="50" cy="50" r="20" fill="none"
stroke="#fff" stroke-width="2" />
</svg>
</div>
</div>
<div class="loading-indicator-text">Loading... Please wait</div>
</div>
</div>
<div id="launch-page" class="sections col col-xl-10" style="display: none;">
<div class="account-section">
<div class="user-image">
</div>
<div class="user-info">
<div class="welcome-text" id="welcome-text">
Nice to see you!
</div>
<div class="account-action" id="account-action">
Click to login
</div>
</div>
</div>
<div class="launch-section">
<div class="server-logo">
<img src="../assets/logo.png" height="150">
</div>
<div class="launch-button-section">
<button class="btn btn-lg btn-launch btn-accent" id="launch-btn">Launch</button>
<div class="patch-checkbox" style="display: none;">
<input type="checkbox" id="enablePatching" disabled />
<label for="enablePatching" style="display: initial;">enable
Patching</label>
</div>
</div>
</div>
<div class="folder-section">
<div class="folder-location">
Current osu! directory: <span id="currentOsuPath"></span>
</div>
<div class="folder-action" id="change-folder-btn">
Not correct?
</div>
</div>
</div>
<div id="login-page" class="sections col col-xl-10" style="display: none;">
<div class="launch-section flex-row">
<div class="server-logo">
<img src="../assets/logo.png" height="120">
</div>
<div class="app-name">EZPPLauncher</div>
</div>
<div class="login-section">
<div class="form-outline mb-3 w-50">
<input type="text" id="login-username" class="form-control form-control-lg" />
<label class="form-label" for="login-username">Username</label>
</div>
<div class="form-outline mb-3 w-50">
<input type="password" id="login-password"
class="form-control form-control-lg" />
<label class="form-label" for="login-password">Password</label>
</div>
<div class="pt-1 mb-4">
<div class="btn-grouped">
<button id="action-cancel" class="btn btn-grey btn-lg"
type="button">Cancel</button>
<button id="action-login" class="btn btn-accent btn-lg"
type="button">Login</button>
</div>
</div>
<p class="text-muted clickable" id="register">Don't have an account?</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</body>
<script type="text/javascript" src="../assets/mdb.min.js"></script>
</html>
+851
View File
@@ -0,0 +1,851 @@
// 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`,
);
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/get_player_status?name=" + currentUser.username,
);
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/get_player_info?name=" + currentUser.username +
"&scope=all",
);
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 = 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(), 8000);
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",
},
});
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 ${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 = 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",
},
});
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 = logging;
}
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: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 = await getEZPPLauncherUpdateFiles(osuPath);
if (patchFiles.length > 0) {
logger.log("EZPPLauncher updates found.");
const patcherDownloader = await downloadEZPPLauncherUpdateFiles(
osuPath,
patchFiles,
);
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)})...`,
});
});
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 () => {
const updateInfo = await updateAvailable();
if (updateInfo.update) {
mainWindow.webContents.send("ezpplauncher:update", updateInfo.release);
}
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.
-17
View File
@@ -1,17 +0,0 @@
const fs = require("fs");
const getLinuxDistroInfo = async() => {
let os = await fs.promises.readFile('/etc/os-release', 'utf8')
let opj = {}
os?.split('\n')?.forEach((line, index) => {
let words = line?.split('=')
let key = words[0]?.toLowerCase()
if (key === '') return
let value = words[1]?.replace(/"/g, '')
opj[key] = value
})
return opj;
}
module.exports = { getLinuxDistroInfo };
-286
View File
@@ -1,286 +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 customUIDLLPath = "https://ez-pp.farm/assets/ezpp!ui.dll";
const customUIDLLHash = "https://ez-pp.farm/assets/ezpp!ui.md5";
const customUIDLLName = "ezpp!ui.dll";
const ignoredOsuEntities = [
'osu!auth.dll',
]
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',
]
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 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 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 axios.get(checkUpdateURL + releaseStream, {});
return releaseData.data;
}
async function getEZPPUIMD5() {
const releaseData = await axios.get(customUIDLLHash, {});
return releaseData.data;
}
async function filesThatNeedUpdate(osuPath, updateFiles) {
const filesToDownload = [];
for (const updatedFile of updateFiles) {
let fileName = updatedFile.filename;
let fileHash = updatedFile.file_hash;
let fileURL = updatedFile.url_full;
if ("url_patch" in updatedFile && updatedFile.url_patch != undefined) {
const stripped = updatedFile.url_patch.split("_");
fileHash = stripped[stripped.length - 1];
const lastIndex = fileURL.lastIndexOf("/");
const baseUrl = fileURL.slice(0, lastIndex);
fileURL = baseUrl + "/f_" + fileHash;
}
if (ignoredOsuEntities.includes(fileName)) continue;
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
});
}
} else {
filesToDownload.push({
fileName,
fileURL
});
}
}
const ezppUI = path.join(osuPath, "EZPPLauncher", customUIDLLName);
if (await fu.existsAsync(ezppUI)) {
const latestMd5Hash = await getEZPPUIMD5();
const binaryUIContents = await fs.promises.readFile(ezppUI);
const existingUIMD5 = crypto.createHash("md5").update(binaryUIContents).digest("hex");
if (existingUIMD5 != latestMd5Hash) {
filesToDownload.push({
folder: "EZPPLauncher",
fileName: "ezpp!ui.dll",
fileURL: customUIDLLPath
})
}
} else {
filesToDownload.push({
folder: "EZPPLauncher",
fileName: "ezpp!ui.dll",
fileURL: customUIDLLPath
})
}
return filesToDownload;
}
async function downloadUpdateFiles(osuPath, filesToUpdate) {
const eventEmitter = new EventEmitter();
let completedIndex = 0;
filesToUpdate.forEach(async (fileToUpdate) => {
if ("folder" in fileToUpdate) {
osuPath = path.join(osuPath, fileToUpdate.folder);
if (!(await fu.existsAsync(osuPath))) await fs.promises.mkdir(osuPath);
}
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.on('error', (err) => {
console.log(err);
eventEmitter.emit('error');
});
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;
switch (process.platform) {
case "linux":
executeUtil.runFile(osuPath, 'osu-stable', ["-devserver", serverDomain], onExit);
return true;
case "win32":
executeUtil.runFile(osuPath, osuExe, ["-devserver", serverDomain], onExit);
return true;
}
return false;
}
async function setConfigValue(configPath, key, value) {
const configLines = new Array();
const fileStream = await fs.promises.readFile(configPath, "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].trim();
if (key == keyname) {
configLines.push(`${keyname} = ${value}`);
} else {
configLines.push(line);
}
} else {
configLines.push(line);
}
}
await fs.promises.writeFile(configPath, configLines.join("\n"), 'utf-8');
}
async function updateOsuCfg(cfgPath) {
const osuFolder = path.dirname(cfgPath);
const fileStream = await fs.promises.readFile(cfgPath, "utf-8");
const lines = fileStream.split(/\r?\n/);
const newLines = [];
for (const line of lines) {
if (line.includes(' = ')) {
const argsPair = line.split(' = ', 2);
const keyname = argsPair[0]
const value = argsPair[1];
if (keyname.startsWith("h_")) {
const filename = keyname.substring(2, keyname.length);
const filepath = path.join(osuFolder, 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(`${keyname} = ${existingFileMD5}`);
} else {
newLines.push(line);
}
}
}
const ezppUI = path.join(osuFolder, customUIDLLName);
if (fs.existsSync(ezppUI)) {
const binaryFileContents = await fs.promises.readFile(ezppUI);
const existingFileMD5 = crypto.createHash("md5").update(binaryFileContents).digest("hex");
newLines.push(`h_${customUIDLLName} = ${existingFileMD5}`);
}
await fs.promises.writeFile(cfgPath, newLines.join("\n"), 'utf-8');
}
async function findOsuInstallation() {
const regedit = require('qiao-regedit');
const osuLocationFromDefaultIcon = "HKLM\\SOFTWARE\\Classes\\osu\\DefaultIcon";
const osuStruct = await regedit.listValuesSync(osuLocationFromDefaultIcon);
for (const line of osuStruct.split("\n")) {
if (line.includes("REG_SZ")) {
let value = (line.trim().split(" ")[2]);
value = value.substring(1, value.length - 3);
return path.dirname(value.trim());
}
}
return undefined;
}
async function replaceUI(folder, isStart) {
const ezppUIFile = path.join(folder, "EZPPLauncher", customUIDLLName);
const osuUIFile = path.join(folder, "osu!ui.dll");
const osuUIFileBackup = path.join(folder, "osu!ui.dll.bak");
if (isStart) {
if (fs.existsSync(osuUIFileBackup)) await fs.promises.unlink(osuUIFileBackup);
await fs.promises.rename(osuUIFile, osuUIFileBackup);
await new Promise((res) => setTimeout(res, 1000));
await fs.promises.copyFile(ezppUIFile, osuUIFile);
} else {
if (!fs.existsSync(osuUIFileBackup)) return;
await fs.promises.unlink(osuUIFile);
await new Promise((res) => setTimeout(res, 1000));
await fs.promises.rename(osuUIFileBackup, osuUIFile);
}
}
module.exports = {
isValidOsuFolder, getLatestConfig, getUpdateFiles, filesThatNeedUpdate,
downloadUpdateFiles, startOsuWithDevServer: startWithDevServer, setConfigValue,
findOsuInstallation, replaceUI, updateOsuCfg, getEZPPUIMD5
}
+7321 -1924
View File
File diff suppressed because it is too large Load Diff
+74 -43
View File
@@ -1,59 +1,90 @@
{
"name": "ezpplauncher",
"version": "1.1.5",
"main": "app.js",
"name": "ezpplauncher-next",
"version": "2.1.5",
"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-${version}.exe"
}
},
"frontend": {
"config": {
"applicationName": "EZPPLauncher"
}
"linux": {},
"mac": {}
},
"scripts": {
"start": "electron .",
"rebuild": "electron-rebuild -f -w get-window-by-name",
"pack-win": "electron-builder --win --x64",
"pack-linux": "electron-builder --linux --x64",
"pack-mac": "electron-builder --mac --x64",
"dist": "electron-builder"
},
"devDependencies": {
"electron": "^24.2.0",
"electron-builder": "^23.6.0",
"electron-packager": "^17.1.1",
"electron-rebuild": "^3.2.9"
"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",
"compare-versions": "^6.0.0-rc.1",
"custom-electron-titlebar": "^4.1.1",
"@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",
"discord-rpc": "^4.0.1",
"electron-serve": "^1.1.0",
"electron-unhandled": "^4.0.1",
"fkill": "^7.2.1",
"get-window-by-name": "^2.0.0",
"jquery": "^3.6.0",
"node-downloader-helper": "^2.1.4",
"qiao-regedit": "^0.1.5",
"sweetalert2": "^11.5.2"
"regedit-rs": "^1.0.2",
"semver": "^7.5.4",
"svelte-french-toast": "^1.2.0",
"sweetalert2": "^11.10.3",
"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
View 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;
+123
View File
@@ -0,0 +1,123 @@
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("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"),
);
});
-278
View File
@@ -1,278 +0,0 @@
const { ipcRenderer, shell } = require('electron');
const { Titlebar, TitlebarColor } = require('custom-electron-titlebar');
const appInfo = require('../appInfo');
let currentPage = "loading";
let loggedIn = false;
window.addEventListener('DOMContentLoaded', () => {
const titlebar = new Titlebar({
backgroundColor: TitlebarColor.fromHex("#24283B"),
itemBackgroundColor: TitlebarColor.fromHex("#121212"),
menu: null,
enableMnemonics: false,
maximizable: false,
});
titlebar.updateTitle(`${appInfo.appName} ${appInfo.appVersion}`);
const $ = require('jquery');
const Swal = require('sweetalert2');
$('#account-action').on('click', () => {
if (!loggedIn) {
changePage('login');
} else {
$('#welcome-text').text(`Nice to see you!`);
$('#account-action').text('Click to login');
$('.user-image').css('background-image', `url(https://a.ez-pp.farm/0)`)
loggedIn = false;
ipcRenderer.send("perform-logout");
Swal.fire({
title: 'See ya soon!',
text: "Successfully logged out!",
icon: 'success',
confirmButtonText: 'Okay'
});
}
});
$('#action-cancel').on('click', () => {
if (!loggedIn) {
changePage('launch');
}
});
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;
case "missing-folder":
const responseData = await ipcRenderer.invoke('set-osu-dir');
if (!responseData)
return;
if (responseData.validOsuDir) {
Swal.fire({
title: 'Success!',
text: 'osu! folder set.',
icon: 'success',
confirmButtonText: 'Cool'
})
$('#currentOsuPath').text(responseData.folderPath);
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!'
})
}
break;
}
});
$("#action-login").on('click', async () => {
const username = $('#login-username').val();
const password = $('#login-password').val();
$("#action-login").attr('disabled', true);
$("#action-cancel").attr('disabled', true);
const responseData = await ipcRenderer.invoke('perform-login', { username, password });
$("#action-login").attr('disabled', false);
$("#action-cancel").attr('disabled', false);
if (!responseData)
return;
if (responseData.code != 200) {
Swal.fire({
title: 'Uh oh!',
text: responseData.message,
icon: 'error',
confirmButtonText: 'Oops.. my bad!'
})
return;
}
$('#login-username').val("");
$('#login-password').val("");
$('#welcome-text').text(`Welcome back, ${responseData.user.name}!`);
$('#account-action').text('Not you?');
$('.user-image').css('background-image', `url(https://a.ez-pp.farm/${responseData.user.id})`);
loggedIn = true;
changePage('launch');
})
$("#change-folder-btn").on('click', async () => {
const responseData = await ipcRenderer.invoke('set-osu-dir');
if (!responseData)
return;
if (responseData.validOsuDir) {
Swal.fire({
title: 'Success!',
text: 'osu! folder set.',
icon: 'success',
confirmButtonText: 'Cool'
})
$('#currentOsuPath').text(responseData.folderPath);
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('config_update', (event, data) => {
if (data.osuPath) {
$('#currentOsuPath').text(data.osuPath);
}
})
ipcRenderer.on('launcher_update', async (event, data) => {
const res = await Swal.fire({
title: 'Update available!',
text: `Version ${data.version} has been released!`,
icon: 'info',
showCancelButton: true,
confirmButtonText: 'Download',
cancelButtonText: 'Remind me later',
});
if (res.isConfirmed) {
shell.openExternal(data.url);
}
})
ipcRenderer.on('account_update', (event, data) => {
switch (data.type) {
case "login-failed":
Swal.fire({
title: 'Uh oh!',
text: data.message,
icon: 'error',
confirmButtonText: 'Okay'
});
changePage("launch");
break;
case "not-loggedin":
changePage("launch");
break;
case "loggedin":
changePage("launch");
$('#welcome-text').text(`Welcome back, ${data.user.name}!`);
$('#account-action').text('Not you?');
$('.user-image').css('background-image', `url(https://a.ez-pp.farm/${data.user.id})`);
loggedIn = true;
break;
}
})
ipcRenderer.on('alert_message', async (event, path) => {
const res = await Swal.fire({
title: 'Hey!',
html: `<p class="text-white">Detected a osu! installation at</p><span class="quotetext">${path}</span><p class="text-white">Is this correct?</p>`,
icon: 'info',
showCancelButton: true,
confirmButtonText: 'Yes',
cancelButtonText: 'No',
});
if (res.isConfirmed) {
$('#currentOsuPath').text(path);
ipcRenderer.send('alert_response', path);
Swal.fire({
title: 'Success!',
text: 'osu! folder set.',
icon: 'success',
confirmButtonText: 'Cool'
})
ipcRenderer.send("do-update-check");
}
})
ipcRenderer.on('status_update', (event, status) => {
switch (status.type) {
case "up-to-date":
$("#launch-btn").attr('disabled', false);
$('#launch-btn').html('Launch');
currentState = status.type;
break;
case "update-available":
$("#launch-btn").attr('disabled', false);
$('#launch-btn').html('Update');
currentState = status.type;
break;
case "missing-folder":
$("#launch-btn").attr('disabled', false);
$('#launch-btn').html('set your osu! folder');
currentState = status.type;
break;
case "error":
Swal.fire({
title: 'Uh oh!',
text: status.message,
icon: 'error',
confirmButtonText: 'Okay'
});
ipcRenderer.send("do-update-check");
break;
case "info":
Swal.fire({
title: 'Info!',
text: status.message,
icon: 'info',
confirmButtonText: 'Okay'
});
break;
case "package-issue":
Swal.fire({
title: 'Uh oh!',
text: status.message,
icon: 'error',
confirmButtonText: 'Okay'
});
$("#launch-btn").attr('disabled', true);
$('#launch-btn').html('missing packages');
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;
}
})
function changePage(page) {
$(`#${currentPage}-page`).fadeOut(50, "swing", () => {
$(`#${page}-page`).fadeIn(350);
});
currentPage = page;
}
// workaround for the dark theme
$('head').append($('<link href="../assets/sweetalert2.dark.css" rel="stylesheet" />'));
})
Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

+15
View 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
View 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>
+95
View File
@@ -0,0 +1,95 @@
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"],
}),
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,
},
};
+225
View File
@@ -0,0 +1,225 @@
<script lang="ts">
import {
Avatar,
Dropdown,
DropdownItem,
DropdownHeader,
DropdownDivider,
Button,
Indicator,
} from "flowbite-svelte";
import {
ArrowLeftSolid,
ArrowRightFromBracketSolid,
ArrowRightToBracketSolid,
HeartSolid,
UserSettingsSolid,
} from "flowbite-svelte-icons";
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.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
View 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
View File
@@ -0,0 +1,5 @@
export enum Page {
Login = 0,
Launch = 1,
Settings = 2,
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="svelte" />
+101
View 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
View File
@@ -0,0 +1,8 @@
import "./app.pcss";
import App from "./App.svelte";
const app = new App({
target: document.body,
});
export default app;
+57
View File
@@ -0,0 +1,57 @@
<script lang="ts">
import { Button } from "flowbite-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>
+220
View File
@@ -0,0 +1,220 @@
<script lang="ts">
import { Input, Button, Spinner, Checkbox } from "flowbite-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 { EyeSlashSolid, EyeSolid } from "flowbite-svelte-icons";
let loading = false;
let username = "";
let password = "";
let saveCredentials = false;
let showPassword = false;
const processLogin = async () => {
loading = true;
const loginPromise = new Promise<void>((res, rej) => {
window.addEventListener(
"login-result",
(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();
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: "Invalid Username or Password!"
},
{
position: "bottom-center",
className:
"dark:!bg-gray-800 border-1 dark:!border-gray-700 dark:!text-gray-100",
duration: 3000
}
);
};
const tryAutoLogin = async () => {
loading = true;
const loginPromise = new Promise<void>((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();
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: "Failed to login."
},
{
position: "bottom-center",
className:
"dark:!bg-gray-800 border-1 dark:!border-gray-700 dark:!text-gray-100",
duration: 3000
}
);
};
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>
+92
View File
@@ -0,0 +1,92 @@
<script lang="ts">
import { Button, ButtonGroup, Input, Toggle } from "flowbite-svelte";
import { FileSearchSolid, FolderSolid } from "flowbite-svelte-icons";
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
View 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
View File
@@ -0,0 +1,4 @@
export type Error = {
code: number;
message: string;
};
+5
View File
@@ -0,0 +1,5 @@
declare module "*.jpg";
declare module "*.jpeg";
declare module "*.png";
declare module "*.svg";
declare module "*.svelte";
+6
View File
@@ -0,0 +1,6 @@
export type User = {
id: number;
donor: boolean;
name: string;
email: string;
};
+3
View File
@@ -0,0 +1,3 @@
export const clamp = (val: number, min: number, max: number) => {
return Math.max(min, Math.min(val, max));
};
+7
View File
@@ -0,0 +1,7 @@
const preprocess = require("svelte-preprocess");
const config = {
preprocess: [preprocess()],
};
module.exports = config;
+73
View 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;
-16
View File
@@ -1,16 +0,0 @@
const exec = require('child_process').exec;
const execPromise = function (cmd) {
return new Promise(function (resolve, reject) {
exec(cmd, function (err, stdout) {
if (err) return reject(err);
resolve(stdout);
});
});
}
const execCommand = async (command) => {
return await execPromise(command);
}
module.exports = { execCommand };
+23
View 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"
]
}
}
-45
View File
@@ -1,45 +0,0 @@
const path = require("path");
const appInfo = require('../appInfo');
const { BrowserWindow, Menu } = 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: "#24283B",
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'
})
const menu = Menu.buildFromTemplate([])
Menu.setApplicationMenu(menu);
window.hide();
window.webContents.once("did-finish-load", function (event, input) {
window.show();
});
window.webContents.setUserAgent(`${appInfo.appName} ${appInfo.appVersion}`);
attachTitlebarToWindow(window);
return window;
},
}