Compare commits

..

17 Commits

Author SHA1 Message Date
HorizonCode 83a25c6911 Merge branch 'tauri' of https://git.ez-pp.farm/EZPPFarm/EZPPLauncher into tauri 2025-05-14 13:37:59 +02:00
HorizonCode c33a9b49e8 fix: bpm timing 2025-05-14 13:33:58 +02:00
HorizonCode 4e6e53f8f0 fix osu detection 2025-01-21 21:15:40 +01:00
HorizonCode f7d23174a5 fix menu 2025-01-04 23:20:42 +01:00
HorizonCode 80a4b9f1d1 add osu memory reader 2025-01-04 23:20:20 +01:00
HorizonCode d751ff4505 fix hover state when extended 2024-12-13 10:42:28 +01:00
HorizonCode 7ca2678a6f add proper menu 2024-12-13 10:39:17 +01:00
HorizonCode b7d50f8b9d fix permissions 2024-12-12 10:33:24 +01:00
HorizonCode 40bdd569e6 add nice animations 👀 2024-12-12 10:33:15 +01:00
HorizonCode 615a265c89 fix logo 2024-12-12 10:32:37 +01:00
HorizonCode e305e6eed9 reduce bg opacity 2024-12-12 10:32:29 +01:00
HorizonCode a271522069 add WIP design 2024-12-10 23:29:28 +01:00
HorizonCode 314fb4d19e spelling mistake, kms 2024-12-10 10:08:58 +01:00
HorizonCode 7b8ff2636c add shadcn-svelte, add custom titlebar 2024-12-10 10:08:23 +01:00
HorizonCode 99062d6bc8 add debug script 2024-12-09 23:20:59 +01:00
HorizonCode da2f4c5e06 add tauri-sqlite and guest module 2024-12-09 23:16:29 +01:00
HorizonCode ce8c259d1b initial commit 2024-12-09 22:53:43 +01:00
109 changed files with 9325 additions and 12314 deletions
+9 -23
View File
@@ -1,24 +1,10 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Lock files
yarn.lock
# Dependency directories
node_modules/
# Svelte Distribution
public/build/
# Electron Distribution
dist
# Project Build Automation Directory
private
# Desktop Services Store on macOS
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
+5 -1
View File
@@ -1,3 +1,7 @@
{
"recommendations": ["svelte.svelte-vscode"]
"recommendations": [
"svelte.svelte-vscode",
"tauri-apps.tauri-vscode",
"rust-lang.rust-analyzer"
]
}
+3
View File
@@ -0,0 +1,3 @@
{
"svelte.enable-ts-plugin": true
}
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Souleh Shaikh
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+2 -335
View File
@@ -1,336 +1,3 @@
<div align="center">
<img alt="Electron Svelte Crossover Banner" src="https://raw.githubusercontent.com/soulehshaikh99/assets/master/create-electron-framework-app/readme/svg/Electron_Svelte.svg" width="580" />
</div>
<br />
The boilerplate code to get started creating Cross-platform Desktop Apps with Electron and Svelte as front-end technology.
<br />
<br />
<div align="center">
# EZPPLauncher Tauri
[![forthebadge](http://forthebadge.com/images/badges/built-by-developers.svg)](http://forthebadge.com)&nbsp;&nbsp;&nbsp;&nbsp;[![forthebadge](http://forthebadge.com/images/badges/makes-people-smile.svg)](http://forthebadge.com)<br />
[![forthebadge](http://forthebadge.com/images/badges/uses-html.svg)](http://forthebadge.com)&nbsp;&nbsp;&nbsp;[![forthebadge](http://forthebadge.com/images/badges/uses-css.svg)](http://forthebadge.com)&nbsp;&nbsp;&nbsp;[![forthebadge](http://forthebadge.com/images/badges/uses-js.svg)](http://forthebadge.com)
[![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard)
<div>
<a href="https://gitHub.com/soulehshaikh99/create-svelte-electron-app/stargazers/"><img src="https://img.shields.io/github/stars/soulehshaikh99/create-svelte-electron-app.svg?style=social&amp;label=Star&amp;maxAge=2592000" alt="GitHub stars" style="max-width: 100%;"></a>&nbsp;&nbsp;&nbsp;<a href="https://gitHub.com/soulehshaikh99/create-svelte-electron-app/network/"><img src="https://img.shields.io/github/forks/soulehshaikh99/create-svelte-electron-app.svg?style=social&amp;label=Forks&amp;maxAge=2592000" alt="GitHub forks" style="max-width: 100%;"></a>&nbsp;&nbsp;&nbsp;<a href="https://gitHub.com/soulehshaikh99/create-svelte-electron-app/watchers/"><img src="https://img.shields.io/github/watchers/soulehshaikh99/create-svelte-electron-app.svg?style=social&amp;label=Watching&amp;maxAge=2592000" alt="GitHub watchers" style="max-width: 100%;"></a>
</div>
<br />
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/souleh99)
</div>
## ✒️ Overview
The aim of this project is to provide Web Developers using `svelte` the power to create cross-platform desktop apps using `electron`.
#### 🧐 What packages does the project use?
**`electron`** enables you to create desktop applications with pure JavaScript by providing a runtime with rich native (operating system) APIs. You could see it as a variant of the Node.js runtime that is focused on desktop applications instead of web servers.
**`electron-builder`** is used as a complete solution to package and build a ready for distribution (supports Numerous target formats) Electron app with "auto update" support out of the box.
**`electron-serve`** is used for Static file serving for Electron apps.
**`svelte`** is a radical new approach to building user interfaces. Whereas traditional frameworks like React and Vue do the bulk of their work in the browser, Svelte shifts that work into a compile step that happens when you build your app. Instead of using techniques like virtual DOM diffing, Svelte writes code that surgically updates the DOM when the state of your app changes.
**`concurrently`** is used to run multiple commands concurrently.
**`wait-on`** is used as it can wait for sockets, and http(s) resources to become available.
<br />
## 🚀 Getting Started
**Note:** If you wish to use npm over yarn then modify `package.json` by replacing `yarn` with `npm` in `electron-dev` and `preelectron-pack` scripts.
But I strongly recommend using <em>yarn</em> as it is a better choice when compared to <em>npm</em>.
### 🤓 Use this boilerplate
```bash
# Clone the Project
# Use degit scaffolding tool
$ npx degit soulehshaikh99/create-svelte-electron-app create-svelte-electron-app
# or GitHub CLI Users
$ gh repo clone https://github.com/soulehshaikh99/create-svelte-electron-app.git
# or Normal Git Users
$ git clone https://github.com/soulehshaikh99/create-svelte-electron-app.git
# Switch location to the cloned directory
$ cd create-svelte-electron-app
# Install dependencies
$ yarn # or npm install
# Run your app
$ yarn electron-dev # or npm run electron-dev
# Package Your App
$ yarn electron-pack # or npm run electron-pack
```
### 💫 Create this boilerplate from scratch (Manual Setup)
#### 1) Use degit scaffolding tool to get started with svelte template.
```bash
$ npx degit sveltejs/template create-svelte-electron-app
```
#### 2) Switch to project directory
```bash
$ cd create-svelte-electron-app
```
#### 3) Move all dependencies to devDependencies using IDE / Text Editor
```bash
# It should look something like this
"dependencies": {},
"devDependencies": {
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-node-resolve": "^11.0.0",
"rollup": "^2.3.4",
"rollup-plugin-css-only": "^3.1.0",
"rollup-plugin-livereload": "^2.0.0",
"rollup-plugin-svelte": "^7.0.0",
"rollup-plugin-terser": "^7.0.0",
"sirv-cli": "^2.0.0",
"svelte": "^3.0.0"
}
```
#### 4) Install & Update all Dependencies at the same time
```bash
$ yarn add --dev @rollup/plugin-commonjs@latest @rollup/plugin-node-resolve@latest rollup@latest rollup-plugin-css-only@latest rollup-plugin-livereload@latest rollup-plugin-svelte@latest rollup-plugin-terser@latest sirv-cli@latest svelte@latest
# npm i -D @rollup/plugin-commonjs@latest @rollup/plugin-node-resolve@latest rollup@latest rollup-plugin-css-only@latest rollup-plugin-livereload@latest rollup-plugin-svelte@latest rollup-plugin-terser@latest sirv-cli@latest svelte@latest
```
#### 5) Install Development Dependencies
```bash
$ yarn add --dev electron electron-builder wait-on concurrently
# npm i -D electron electron-builder wait-on concurrently
```
#### 6) Install Production Dependency
```bash
$ yarn add electron-serve # or npm i electron-serve
```
#### 7) Your dependencies should look something like this
```json
"dependencies": {
"electron-serve": "^1.1.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-node-resolve": "^15.0.1",
"concurrently": "^7.5.0",
"electron": "^21.2.0",
"electron-builder": "^23.6.0",
"rollup": "^3.2.3",
"rollup-plugin-css-only": "^4.2.0",
"rollup-plugin-livereload": "^2.0.5",
"rollup-plugin-svelte": "^7.1.0",
"rollup-plugin-terser": "^7.0.2",
"sirv-cli": "^2.0.2",
"svelte": "^3.52.0",
"wait-on": "^6.0.1"
}
```
#### 8) Download the app icon
[favicon.png](https://raw.githubusercontent.com/soulehshaikh99/assets/master/framework-icons/svelte/favicon.png) and place it in the public directory, overwriting the original (the original one is too small for packaging app, electron-builder will raise an error).
#### 9) Create main.js file (serves as entry point for Electron App's Main Process)
```bash
# Windows Users
$ fsutil file createnew main.js 0
# notepad main.js
# Linux and macOS Users
$ touch main.js
```
#### 10) Paste the below code in main.js file
```js
// Modules to control application life and create native browser window
const { app, BrowserWindow } = require('electron');
const path = require('path');
const serve = require('electron-serve');
const loadURL = serve({ directory: 'public' });
// 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;
function isDev() {
return !app.isPackaged;
}
function createWindow() {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
preload: path.join(__dirname, 'preload.js'),
// enableRemoteModule: true,
// contextIsolation: false
},
icon: path.join(__dirname, 'public/favicon.png'),
show: false
});
// 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);
}
// Uncomment the following line of code when app is ready to be packaged.
// loadURL(mainWindow);
// Open the DevTools and also disable Electron Security Warning.
// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = true;
// mainWindow.webContents.openDevTools();
// 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', () => {
mainWindow.show()
});
}
// 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', function () {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
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.
```
#### 11) Create preload.js file (optional, contains code that runs before your web page is loaded into the browser window)
```bash
# Windows Users
$ fsutil file createnew preload.js 0
# notepad preload.js
# Linux and macOS Users
$ touch preload.js
```
#### 12) Paste the below code in preload.js file
```js
console.log('Hello from preload.js file!');
```
#### 13) Update the script section of `package.json`
```bash
# Add this scripts
"electron": "wait-on http://localhost:8080 && electron .",
"electron-dev": "concurrently \"yarn run dev\" \"yarn run electron\"",
"preelectron-pack": "yarn run build",
"electron-pack": "electron-builder"
# Modify the build and dev scriptsW
"build": "rollup -c --bundleConfigAsCjs",
"dev": "rollup -c -w --bundleConfigAsCjs",
# You should end up with something similar
"scripts": {
"build": "rollup -c --bundleConfigAsCjs",
"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": "yarn run build",
"electron-pack": "electron-builder"
}
```
#### 14) Add the following configuration in `package.json`
**Note:** build configuration is used by electron-builder, modify it if you wish to add more packaging and native distribution options for different OS Platforms.
```bash
"name": "create-svelte-electron-app", # By default it is svelte-app
"main": "main.js", # Application Entry Point, please verify entry point is set to main.js
"build": {
"icon": "public/favicon.png",
"productName": "Svelte and Electron App",
"files": [
"public/**/*",
"main.js"
],
"win": {}, # Windows Specific Configuration
"linux": {}, # Linux Specific Configuration
"mac": {} # MacOs Specific Configuration
}
```
#### 15) Test drive your app
```bash
# Run your app
$ yarn electron-dev # or npm run electron-dev
# Package Your App
$ yarn electron-pack # or npm run electron-pack
```
### 💯 Result
<div align="center">
<img alt="Electron Svelte Window Screeenshot" src="https://raw.githubusercontent.com/soulehshaikh99/assets/master/create-electron-framework-app/readme/png/create-svelte-electron-app.png" />
</div>
<h3>😍 Made with ❤️ from Souleh</h3>
[![forthebadge](http://forthebadge.com/images/badges/built-with-love.svg)](http://forthebadge.com)
<br/>
<h3>💰 Support Me</h3>
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/souleh99)
<br/>
<h3>📋 License: </h3>
Licensed under the <a href="https://github.com/soulehshaikh99/create-svelte-electron-app/blob/master/LICENSE">MIT License</a>.
Trying to recode this shit with tauri, kms
BIN
View File
Binary file not shown.
+17
View File
@@ -0,0 +1,17 @@
{
"$schema": "https://next.shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": {
"config": "tailwind.config.ts",
"css": "src\\app.css",
"baseColor": "gray"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks"
},
"typescript": true,
"registry": "https://next.shadcn-svelte.com/registry"
}
-4
View File
@@ -1,4 +0,0 @@
const appName = "EZPPLauncher";
const appVersion = "2.0.0";
module.exports = { appName, appVersion };
-53
View File
@@ -1,53 +0,0 @@
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
@@ -1,11 +0,0 @@
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
@@ -1,20 +0,0 @@
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 };
-13
View File
@@ -1,13 +0,0 @@
function formatBytes(bytes, decimals = 2) {
if (!+bytes) return '0 Bytes'
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
@@ -1,32 +0,0 @@
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;
-478
View File
@@ -1,478 +0,0 @@
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 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 patcherFiles = [
{
name: "patcher.exe",
url_download: "https://ez-pp.farm/assets/patcher.exe",
url_hash: "https://ez-pp.farm/assets/patcher.md5",
},
{
name: "patch.hook.dll",
url_download: "https://ez-pp.farm/assets/patch.hook.dll",
url_hash: "https://ez-pp.farm/assets/patch.hook.md5",
},
];
const uiFiles = [
{
name: "ezpp!ui.dll",
url_download: "https://ez-pp.farm/assets/ezpp!ui.dll",
url_hash: "https://ez-pp.farm/assets/ezpp!ui.md5",
},
];
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) {
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),
});
},
});
axiosDownloadWithProgress.data.on("end", () => {
eventEmitter.emit("data", {
fileName,
loaded: fileSize,
total: fileSize,
progress: 100,
});
});
try {
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,
});
}
}
// wait until all files are downloaded
return true;
};
return {
eventEmitter,
startDownload,
};
}
function runOsuWithDevServer(osuPath, serverDomain, onExit) {
const osuExecuteable = path.join(osuPath, "osu!.exe");
runFile(osuPath, osuExecuteable, ["-devserver", serverDomain], onExit);
}
async function getPatcherUpdates(osuPath) {
const filesToDownload = [];
const patcherDir = path.join(osuPath, "EZPPLauncher");
if (!fs.existsSync(patcherDir)) fs.mkdirSync(patcherDir);
for (const patcherFile of patcherFiles) {
if (fs.existsSync(path.join(patcherDir, patcherFile.name))) {
const latestPatchFileHash = await (await fetch(patcherFile.url_hash))
.text();
const localPatchFileHash = crypto.createHash("md5").update(
fs.readFileSync(path.join(patcherDir, patcherFile.name)),
).digest("hex");
if (
latestPatchFileHash.trim().toLowerCase() !=
localPatchFileHash.trim().toLowerCase()
) filesToDownload.push(patcherFile);
} else filesToDownload.push(patcherFile);
}
return filesToDownload;
}
function downloadPatcherUpdates(osuPath, patcherUpdates) {
const eventEmitter = new EventEmitter();
const startDownload = async () => {
const patcherDir = path.join(osuPath, "EZPPLauncher");
if (!fs.existsSync(patcherDir)) fs.mkdirSync(patcherDir);
for (const patcherFile of patcherUpdates) {
const fileName = patcherFile.name;
const fileURL = patcherFile.url_download;
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),
});
},
});
try {
if (fs.existsSync(path.join(osuPath, "EZPPLauncher", fileName))) {
await fs.promises.rm(path.join(osuPath, "EZPPLauncher", fileName), {
force: true,
});
}
await fs.promises.writeFile(
path.join(osuPath, "EZPPLauncher", fileName),
axiosDownloadWithProgress.data,
);
} catch (err) {
console.log(err);
eventEmitter.emit("error", {
fileName,
});
}
}
};
return {
eventEmitter,
startDownload,
};
}
async function getUIFiles(osuPath) {
const filesToDownload = [];
const ezppLauncherDir = path.join(osuPath, "EZPPLauncher");
if (!fs.existsSync(ezppLauncherDir)) fs.mkdirSync(ezppLauncherDir);
for (const uiFile of uiFiles) {
if (fs.existsSync(path.join(ezppLauncherDir, uiFile.name))) {
const latestPatchFileHash = await (await fetch(uiFile.url_hash)).text();
const localPatchFileHash = crypto.createHash("md5").update(
fs.readFileSync(path.join(ezppLauncherDir, uiFile.name)),
).digest("hex");
if (
latestPatchFileHash.trim().toLowerCase() !=
localPatchFileHash.trim().toLowerCase()
) filesToDownload.push(uiFile);
} else filesToDownload.push(uiFile);
}
return filesToDownload;
}
function downloadUIFiles(osuPath, uiFiles) {
const eventEmitter = new EventEmitter();
const startDownload = async () => {
const ezpplauncherDir = path.join(osuPath, "EZPPLauncher");
if (!fs.existsSync(ezpplauncherDir)) fs.mkdirSync(ezpplauncherDir);
for (const uiFile of uiFiles) {
const fileName = uiFile.name;
const fileURL = uiFile.url_download;
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),
});
},
});
try {
if (fs.existsSync(path.join(osuPath, "EZPPLauncher", fileName))) {
await fs.promises.rm(path.join(osuPath, "EZPPLauncher", fileName), {
force: true,
});
}
await fs.promises.writeFile(
path.join(osuPath, "EZPPLauncher", fileName),
axiosDownloadWithProgress.data,
);
} catch (err) {
console.log(err);
eventEmitter.emit("error", {
fileName,
});
}
}
};
return {
eventEmitter,
startDownload,
};
}
async function replaceUIFile(osuPath, revert) {
if (!revert) {
const ezppUIFile = path.join(osuPath, "EZPPLauncher", "ezpp!ui.dll");
const oldOsuUIFile = path.join(osuPath, "osu!ui.dll");
await fs.promises.rename(
oldOsuUIFile,
path.join(osuPath, "osu!ui.dll.bak"),
);
await fs.promises.rename(ezppUIFile, oldOsuUIFile);
} else {
const oldOsuUIFile = path.join(osuPath, "osu!ui.dll");
const ezppUIFile = path.join(osuPath, "EZPPLauncher", "ezpp!ui.dll");
await fs.promises.rename(oldOsuUIFile, ezppUIFile);
await fs.promises.rename(
path.join(osuPath, "osu!ui.dll.bak"),
oldOsuUIFile,
);
}
}
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,
getPatcherUpdates,
downloadPatcherUpdates,
downloadUIFiles,
getUIFiles,
replaceUIFile,
findOsuInstallation,
updateOsuConfigHashes,
};
-63
View File
@@ -1,63 +0,0 @@
const DiscordRPC = require("discord-auto-rpc");
const { appName, appVersion } = require("./appInfo.js");
const clientId = "1032772293220384808";
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", () => {
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 }) => {
currentStatus.state = state ?? " ";
currentStatus.details = details ?? " ";
},
updateVersion: (osuVersion) => {
currentStatus.smallImageKey = osuVersion ? "osu" : " ";
currentStatus.smallImageText = osuVersion ? `osu! ${osuVersion}` : " ";
},
update: () => {
if (richPresence) {
richPresence.setActivity(currentStatus);
}
},
};
-25
View File
@@ -1,25 +0,0 @@
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,
};
-630
View File
@@ -1,630 +0,0 @@
// 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,
getPatcherUpdates,
downloadPatcherUpdates,
getUIFiles,
downloadUIFiles,
replaceUIFile,
findOsuInstallation,
updateOsuConfigHashes,
} = require("./electron/osuUtil");
const { formatBytes } = require("./electron/formattingUtil");
const windowName = require("get-window-by-name");
const { existsSync } = 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");
// 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 lastOsuStatus = "";
let lastStatusUpdate;
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;
setTimeout(() => {
if (patch) {
const patcherExecuteable = path.join(
userOsuPath,
"EZPPLauncher",
"patcher.exe",
);
if (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;
let details = "Idle...";
let infoText = currentStatus.player_status.status.info_text.length > 0
? currentStatus.player_status.status.info_text
: " ";
switch (currentStatus.player_status.status.action) {
case 1:
details = "AFK...";
infoText = " ";
break;
case 2:
details = "Playing...";
break;
case 3:
details = "Editing...";
break;
case 4:
details = "Modding...";
break;
case 5:
details = "Multiplayer: Selecting a Beatmap...";
infoText = " ";
break;
case 6:
details = "Watching...";
break;
case 8:
details = "Testing...";
break;
case 9:
details = "Submitting...";
break;
case 11:
details = "Multiplayer: Idle...";
infoText = " ";
break;
case 12:
details = "Multiplayer: Playing...";
break;
case 13:
details = "Browsing osu!direct...";
infoText = " ";
break;
}
richPresence.updateStatus({
details,
state: infoText,
});
richPresence.update();
}
}, 2500);
}
function stopOsuStatus() {
clearInterval(osuCheckInterval);
}
function registerIPCPipes() {
ipcMain.handle("ezpplauncher:login", async (e, args) => {
const hwid = getHwId();
const timeout = new AbortController();
const timeoutId = setTimeout(() => timeout.abort(), 8000);
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");
}
return result;
}
return {
code: 500,
message: "Something went wrong while logging you in.",
};
} catch (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);
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,
};
}
return result;
} else {
config.remove("password");
}
return {
code: 500,
message: "Something went wrong while logging you in.",
};
} catch (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;
});
ipcMain.handle("ezpplauncher:logout", (e) => {
config.remove("username");
config.remove("password");
config.remove("guest");
currentUser = undefined;
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 (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) => {
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!",
});
return;
}
if (!(await isValidOsuFolder(osuPath))) {
mainWindow.webContents.send("ezpplauncher:launchabort");
mainWindow.webContents.send("ezpplauncher:alert", {
type: "error",
message: "invalid osu! path!",
});
return;
}
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 uiFiles = await getUIFiles(osuPath);
const updateFiles = await getFilesThatNeedUpdate(osuPath, latestFiles);
if (uiFiles.length > 0) {
const uiDownloader = downloadUIFiles(osuPath, uiFiles);
let errored = false;
uiDownloader.eventEmitter.on("error", (data) => {
const filename = data.fileName;
errored = true;
mainWindow.webContents.send("ezpplauncher:alert", {
type: "error",
message:
`Failed to download/replace ${filename}!\nMaybe try to restart EZPPLauncher.`,
});
});
uiDownloader.eventEmitter.on("data", (data) => {
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 uiDownloader.startDownload();
mainWindow.webContents.send("ezpplauncher:launchprogress", {
progress: -1,
});
if (errored) {
mainWindow.webContents.send("ezpplauncher:launchabort");
return;
}
}
if (updateFiles.length > 0) {
const updateDownloader = downloadUpdateFiles(osuPath, updateFiles);
let errored = false;
updateDownloader.eventEmitter.on("error", (data) => {
const filename = data.fileName;
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) => {
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 getPatcherUpdates(osuPath);
if (patchFiles.length > 0) {
const patcherDownloader = downloadPatcherUpdates(osuPath, patchFiles);
let errored = false;
patcherDownloader.eventEmitter.on("error", (data) => {
const filename = data.fileName;
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) => {
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));
}
mainWindow.webContents.send("ezpplauncher:launchstatus", {
status: "Preparing launch...",
});
await updateOsuConfigHashes(osuPath);
await replaceUIFile(osuPath, false);
const userConfig = getUserConfig(osuPath);
richPresence.updateVersion(await userConfig.get("LastVersion"));
richPresence.update();
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!...",
});
const onExitHook = () => {
mainWindow.show();
mainWindow.focus();
stopOsuStatus();
richPresence.updateVersion();
richPresence.updateStatus({
state: "Idle in Launcher...",
details: undefined,
});
richPresence.update();
mainWindow.webContents.send("ezpplauncher:launchstatus", {
status: "Waiting for cleanup...",
});
setTimeout(async () => {
await replaceUIFile(osuPath, true);
mainWindow.webContents.send("ezpplauncher:launchabort");
osuLoaded = false;
}, 5000);
};
runOsuWithDevServer(osuPath, "ez-pp.farm", onExitHook);
mainWindow.hide();
startOsuStatus();
/* mainWindow.webContents.send("ezpplauncher:launchprogress", {
progress: 0,
});
mainWindow.webContents.send("ezpplauncher:launchprogress", {
progress: 100,
}); */
return true;
});
}
function createWindow() {
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();
}
}
// 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.
-9311
View File
File diff suppressed because it is too large Load Diff
+43 -82
View File
@@ -1,89 +1,50 @@
{
"name": "ezpplauncher-next",
"version": "2.0.0",
"description": "EZPPLauncher rewritten with Svelte.",
"private": false,
"license": "MIT",
"main": "main.js",
"author": "HorizonCode <horizoncode88@gmail.com>",
"build": {
"icon": "public/favicon.png",
"productName": "EZPPLauncher",
"files": [
"public/**/*",
"main.js",
"preload.js",
"electron/*",
"electron/**/*"
],
"win": {
"target": [
"portable"
]
},
"linux": {},
"mac": {}
},
"name": "ezpplauncher",
"version": "3.0.0",
"description": "",
"type": "module",
"scripts": {
"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"
"debug": "bun run tauri dev",
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"tauri": "tauri",
"update-logo": "bun run tauri icon ./static/logo.png"
},
"license": "MIT",
"dependencies": {
"@types/better-sqlite3": "^7.6.8",
"axios": "^1.6.5",
"better-sqlite3": "^9.2.2",
"crypto": "^1.0.1",
"crypto-js": "^4.2.0",
"custom-electron-titlebar": "^4.2.7",
"discord-auto-rpc": "^1.0.17",
"electron-serve": "^1.1.0",
"get-window-by-name": "^2.0.0",
"regedit-rs": "^1.0.2",
"semver": "^7.5.4",
"svelte-french-toast": "^1.2.0",
"sweetalert2": "^11.10.3",
"systeminformation": "^5.21.22"
"@elron/svelte-audio-store": "^1.0.0",
"@tailwindcss/typography": "^0.5.15",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-fs": "~2",
"@tauri-apps/plugin-shell": "^2",
"@tauri-apps/plugin-sql": "^2.2.0",
"ky": "^1.7.3",
"lucide-svelte": "^0.469.0",
"osu-classes": "^3.1.0",
"osu-parsers": "^4.1.7",
"radix-icons-svelte": "^1.2.1"
},
"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",
"@sveltejs/vite-plugin-svelte": "^3.0.1",
"@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"
"@sveltejs/adapter-static": "^3.0.5",
"@sveltejs/kit": "^2.7.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@tauri-apps/cli": "^2",
"autoprefixer": "^10.4.20",
"bits-ui": "^1.0.0-next.74",
"clsx": "^2.1.1",
"sass-embedded": "^1.82.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwind-merge": "^2.5.5",
"tailwind-variants": "^0.3.0",
"tailwindcss": "^3.4.9",
"tailwindcss-animate": "^1.0.7",
"tslib": "^2.8.0",
"typescript": "^5.5.0",
"vite": "^5.4.10"
}
}
}
-13
View File
@@ -1,13 +0,0 @@
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;
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};
-117
View File
@@ -1,117 +0,0 @@
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 }),
);
});
-63
View File
@@ -1,63 +0,0 @@
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;
}
a {
color: rgb(0,100,200);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a:visited {
color: rgb(0,80,160);
}
label {
display: block;
}
input, button, select, textarea {
font-family: inherit;
font-size: inherit;
-webkit-padding: 0.4em 0;
padding: 0.4em;
margin: 0 0 0.5em 0;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 2px;
}
input:disabled {
color: #ccc;
}
button {
color: #333;
background-color: #f4f4f4;
outline: none;
}
button:disabled {
color: #999;
}
button:not(:disabled):active {
background-color: #ddd;
}
button:focus {
border-color: #666;
}
-22
View File
@@ -1,22 +0,0 @@
<!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
@@ -1,95 +0,0 @@
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,
},
};
+7
View File
@@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas
+6273
View File
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
[package]
name = "ezpplauncher"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "ezpplauncher_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
rosu-mem = { git = "https://github.com/486c/rosu-mem.git", tag = "v1.0.0" }
rosu-pp = { git = "https://github.com/486c/rosu-pp.git", branch = "main", features = ["gradual"] }
tracy-client = { version = "0.16.4", default-features = false }
eyre = "0.6.12"
tauri = { version = "2", features = [] }
tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_repr = "0.1.17"
tauri-plugin-sql = "2.2.0"
tauri-plugin-dialog = "2"
tauri-plugin-fs = "2"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-single-instance = "2"
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
+19
View File
@@ -0,0 +1,19 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": [
"main"
],
"permissions": [
"core:default",
"shell:allow-open",
"shell:default",
"shell:allow-execute",
"dialog:default",
"core:window:allow-start-dragging",
"core:window:allow-minimize",
"core:window:allow-close",
"fs:default"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

+152
View File
@@ -0,0 +1,152 @@
mod reading_loop;
mod structs;
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
use crate::reading_loop::process_reading_loop;
use crate::structs::{State, StaticAddresses};
use eyre::Report;
use rosu_mem::{
error::ProcessError,
process::{Process, ProcessTraits},
};
use std::sync::{Arc, Mutex};
use std::{thread, time::Duration};
use structs::{InnerValues, OutputValues};
use tauri::Manager;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
println!("starting ezpplauncher!");
osu_memory_reading();
let mut builder = tauri::Builder::default().plugin(tauri_plugin_fs::init());
#[cfg(desktop)]
{
builder = builder.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
let app_window = app.get_webview_window("main").expect("no main window");
app_window
.set_always_on_top(true)
.expect("failed to set always on top");
app_window.set_focus().expect("failed to focus");
}));
}
builder
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_sql::Builder::default().build())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
fn osu_memory_reading() {
thread::spawn(|| {
let output_values = Arc::new(Mutex::new(OutputValues::default()));
let inner_values = InnerValues::default();
let mut state = State {
addresses: StaticAddresses::default(),
ivalues: inner_values,
values: output_values,
};
'init_loop: loop {
println!("Searching for osu! process");
let p = match Process::initialize("osu!.exe") {
Ok(p) => p,
Err(e) => {
println!("{:?}", Report::new(e));
thread::sleep(Duration::from_millis(5000));
continue 'init_loop;
}
};
let mut values = state.values.lock().unwrap();
println!("Using auto-detected osu! folder path");
if let Some(ref dir) = p.executable_dir {
values.osu_path.clone_from(dir);
} else {
println!(
"{:?}",
Report::msg(
"Can't auto-detect osu! folder path \
nor any was provided through command \
line argument",
)
);
continue 'init_loop;
}
if !values.osu_path.exists() {
println!(
"Provided osu path doesn't exists!\n Path: {}",
&values.osu_path.to_str().unwrap()
);
println!(
"{:?}",
Report::msg(
"Can't auto-detect osu! folder path \
nor any was provided through command \
line argument",
)
);
continue 'init_loop;
};
drop(values);
println!("Reading static signatures...");
match StaticAddresses::new(&p) {
Ok(v) => state.addresses = v,
Err(e) => match e.downcast_ref::<ProcessError>() {
Some(&ProcessError::ProcessNotFound) => {
thread::sleep(Duration::from_millis(2000));
continue 'init_loop;
}
#[cfg(target_os = "windows")]
Some(&ProcessError::OsError { .. }) => {
println!("{:?}", e);
thread::sleep(Duration::from_millis(2000));
continue 'init_loop;
}
Some(_) | None => {
println!("{:?}", e);
thread::sleep(Duration::from_millis(2000));
continue 'init_loop;
}
},
};
println!("osu! process found, Starting reading loop");
'main_loop: loop {
if let Err(e) = process_reading_loop(&p, &mut state) {
match e.downcast_ref::<ProcessError>() {
Some(&ProcessError::ProcessNotFound) => {
thread::sleep(Duration::from_millis(5000));
continue 'init_loop;
}
#[cfg(target_os = "windows")]
Some(&ProcessError::OsError { .. }) => {
println!("{:?}", e);
thread::sleep(Duration::from_millis(5000));
continue 'init_loop;
}
Some(_) | None => {
println!("{:?}", e);
thread::sleep(Duration::from_millis(5000));
continue 'main_loop;
}
}
}
let cloned_values = state.values.clone();
let values_lock = cloned_values.lock().unwrap();
let values = &*values_lock;
println!("{:?}", values.current_stars);
thread::sleep(Duration::from_millis(2000));
}
}
});
}
+6
View File
@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
ezpplauncher_lib::run()
}
+462
View File
@@ -0,0 +1,462 @@
use std::{borrow::Cow, mem::size_of};
use rosu_pp::Beatmap;
use tracy_client::*;
use eyre::Result;
use rosu_mem::process::{Process, ProcessTraits};
use crate::structs::{State, GameState, BeatmapStatus, OutputValues};
/// Here cases when key overlay is not gonna be available for reading:
/// 1. Map is not fully loaded
/// 2. If key overlay is not enabled in settings
pub fn process_key_overlay(
p: &Process,
values: &mut OutputValues,
ruleset_addr: i32,
) -> Result<()> {
let keyoverlay_ptr = p.read_i32(ruleset_addr + 0xB0)?;
if keyoverlay_ptr == 0 {
return Ok(())
}
// TODO optimize using batches?
let keyoverlay_addr = p.read_i32(
p.read_i32(keyoverlay_ptr + 0x10)? + 0x4
)?;
values.keyoverlay.k1_pressed = p.read_i8(
p.read_i32(keyoverlay_addr + 0x8)? + 0x1C
)? != 0;
values.keyoverlay.k1_count = p.read_i32(
p.read_i32(keyoverlay_addr + 0x8)? + 0x14
)? as u32;
values.keyoverlay.k2_pressed = p.read_i8(
p.read_i32(keyoverlay_addr + 0xC)? + 0x1C
)? != 0;
values.keyoverlay.k2_count = p.read_i32(
p.read_i32(keyoverlay_addr + 0xC)? + 0x14
)? as u32;
values.keyoverlay.m1_pressed = p.read_i8(
p.read_i32(keyoverlay_addr + 0x10)? + 0x1C
)? != 0;
values.keyoverlay.m1_count = p.read_i32(
p.read_i32(keyoverlay_addr + 0x10)? + 0x14
)? as u32;
values.keyoverlay.m2_pressed = p.read_i8(
p.read_i32(keyoverlay_addr + 0x14)? + 0x1C
)? != 0;
values.keyoverlay.m2_count = p.read_i32(
p.read_i32(keyoverlay_addr + 0x14)? + 0x14
)? as u32;
Ok(())
}
pub fn process_gameplay(
p: &Process,
state: &mut State,
values: &mut OutputValues,
ruleset_addr: i32,
) -> Result<()> {
let _span = span!("Gameplay data");
if values.prev_playtime > values.playtime {
values.reset_gameplay();
state.ivalues.reset();
}
values.prev_playtime = values.playtime;
if ruleset_addr == 0 {
return Ok(())
};
let gameplay_base =
p.read_i32(ruleset_addr + 0x68)?;
if gameplay_base == 0 {
return Ok(())
}
let score_base = p.read_i32(gameplay_base + 0x38)?;
let hp_base = p.read_i32(gameplay_base + 0x40)?;
// Random value but seems to work pretty well
// TODO sometimes playtime is >150 but game doesn't have
// values yet unreal to debug, occurs rarely and randomly
if values.playtime > 150 {
values.gameplay.current_hp = p.read_f64(hp_base + 0x1C)?;
values.gameplay.current_hp_smooth =
p.read_f64(hp_base + 0x14)?;
}
let hit_errors_base = p.read_i32(score_base + 0x38)?;
p.read_i32_array(
hit_errors_base,
&mut values.gameplay.hit_errors
)?;
values.gameplay.unstable_rate =
values.gameplay.calculate_unstable_rate();
// TODO batch
values.gameplay.mode = p.read_i32(score_base + 0x64)?;
// TODO batch
values.gameplay.hit_300 = p.read_i16(score_base + 0x8a)?;
values.gameplay.hit_100 = p.read_i16(score_base + 0x88)?;
values.gameplay.hit_50 = p.read_i16(score_base + 0x8c)?;
values.gameplay.username = p.read_string(score_base + 0x28)?;
// TODO batch
values.gameplay.hit_geki = p.read_i16(score_base + 0x8e)?;
values.gameplay.hit_katu = p.read_i16(score_base + 0x90)?;
values.gameplay.hit_miss = p.read_i16(score_base + 0x92)?;
let passed_objects = values.gameplay.passed_objects()?;
values.gameplay.passed_objects = passed_objects;
values.gameplay.update_accuracy();
values.gameplay.score = p.read_i32(score_base + 0x78)?;
values.gameplay.combo = p.read_i16(score_base + 0x94)?;
values.gameplay.max_combo = p.read_i16(score_base + 0x68)?;
if values.gameplay.combo < values.prev_combo
&& values.gameplay.hit_miss == values.prev_hit_miss {
values.gameplay.slider_breaks += 1;
}
values.prev_hit_miss = values.gameplay.hit_miss;
let mods_xor_base = p.read_i32(score_base + 0x1C)?;
let mods_raw = p.read_u64(mods_xor_base + 0x8)?;
let mods_xor1 = mods_raw & 0xFFFFFFFF;
let mods_xor2 = mods_raw >> 32;
// Read key overlay
process_key_overlay(
p,
values,
ruleset_addr
)?;
values.gameplay.mods = (mods_xor1 ^ mods_xor2) as u32;
values.update_readable_mods();
// Calculate pp
values.update_current_pp(&mut state.ivalues);
values.update_fc_pp(&mut state.ivalues);
values.prev_passed_objects = passed_objects;
values.prev_combo = values.gameplay.combo;
values.gameplay.grade = values.gameplay.get_current_grade();
values.update_current_bpm();
values.update_kiai();
Ok(())
}
pub fn process_reading_loop(
p: &Process,
state: &mut State
) -> Result<()> {
let _span = span!("reading loop");
let values = state.values.clone();
let mut values = values.lock().unwrap();
let menu_mods_ptr = p.read_i32(
state.addresses.menu_mods + 0x9
)?;
let menu_mods = p.read_u32(menu_mods_ptr)?;
values.menu_mods = menu_mods;
let playtime_ptr = p.read_i32(state.addresses.playtime + 0x5)?;
values.playtime = p.read_i32(playtime_ptr)?;
let beatmap_ptr = p.read_i32(state.addresses.base - 0xC)?;
let beatmap_addr = p.read_i32(beatmap_ptr)?;
let status_ptr = p.read_i32(state.addresses.status - 0x4)?;
let skin_ptr = p.read_i32(state.addresses.skin + 0x4)?;
let skin_data = p.read_i32(skin_ptr)?;
values.skin = p.read_string(skin_data + 0x44)?;
values.state = GameState::from(
p.read_u32(status_ptr)?
);
// Handle leaving `Playing` state
if values.prev_state == GameState::Playing
&& values.state != GameState::Playing {
values.reset_gameplay();
state.ivalues.reset();
values.update_stars_and_ss_pp();
}
if beatmap_addr == 0 {
return Ok(())
}
if values.state != GameState::MultiplayerLobby {
let mut beatmap_stats_buff = [0u8; size_of::<f32>() * 4];
p.read(
beatmap_addr + 0x2c,
size_of::<f32>() * 4,
&mut beatmap_stats_buff
)?;
// Safety: unwrap here because buff is already initialized
// and filled with zeros, the worst case scenario is
// ar, cs, od, hp going to be zero's
values.beatmap.ar = f32::from_le_bytes(
beatmap_stats_buff[0..4].try_into().unwrap()
);
values.beatmap.cs = f32::from_le_bytes(
beatmap_stats_buff[4..8].try_into().unwrap()
);
values.beatmap.hp = f32::from_le_bytes(
beatmap_stats_buff[8..12].try_into().unwrap()
);
values.beatmap.od = f32::from_le_bytes(
beatmap_stats_buff[12..].try_into().unwrap()
);
let plays_addr = p.read_i32(state.addresses.base - 0x33)? + 0xC;
values.plays = p.read_i32(plays_addr)?;
values.beatmap.artist = p.read_string(beatmap_addr + 0x18)?;
values.beatmap.title = p.read_string(beatmap_addr + 0x24)?;
values.beatmap.creator = p.read_string(beatmap_addr + 0x7C)?;
values.beatmap.difficulty = p.read_string(beatmap_addr + 0xAC)?;
values.beatmap.map_id = p.read_i32(beatmap_addr + 0xC8)?; // TODO batch
values.beatmap.mapset_id = p.read_i32(beatmap_addr + 0xCC)?; // TODO batch
}
values.beatmap.beatmap_status = BeatmapStatus::from(
p.read_i16(beatmap_addr + 0x12C)?
);
let mut new_map = false;
// All time values that available everywhere
values.chat_enabled = p.read_i8(
state.addresses.chat_checker - 0x20
)? != 0;
// Skin folder
let skin_data_ptr = p.read_i32(
p.read_i32(state.addresses.skin + 4)?
)?;
values.skin_folder = p.read_string(
skin_data_ptr + 68
)?;
if values.state != GameState::PreSongSelect
&& values.state != GameState::MultiplayerLobby
&& values.state != GameState::MultiplayerResultScreen {
let menu_mode_addr = p.read_i32(state.addresses.base - 0x33)?;
let beatmap_file = p.read_string(beatmap_addr + 0x90)?;
let beatmap_folder = p.read_string(beatmap_addr + 0x78)?;
let audio_file = p.read_string(beatmap_addr + 0x64)?;
values.menu_mode = p.read_i32(menu_mode_addr)?;
values.beatmap.paths.beatmap_full_path
= values.osu_path.join("Songs/");
values.beatmap.paths.beatmap_full_path.push(&beatmap_folder);
values.beatmap.paths.beatmap_full_path.push(&beatmap_file);
values.beatmap.md5 =
p.read_string(beatmap_addr + 0x6C)?;
// Check if beatmap changed
if (beatmap_folder != values.beatmap.paths.beatmap_folder
|| beatmap_file != values.beatmap.paths.beatmap_file
|| values.prev_menu_mode != values.menu_mode)
&& values.beatmap.paths.beatmap_full_path.exists() {
let current_beatmap = match Beatmap::from_path(
&values.beatmap.paths.beatmap_full_path
) {
Ok(beatmap) => {
new_map = true;
values.beatmap.paths.background_file.clone_from(
&beatmap.background.filename
);
if let Some(hobj) = beatmap.hit_objects.last() {
values.beatmap.last_obj_time = hobj.start_time;
}
if let Some(hobj) = beatmap.hit_objects.first() {
values.beatmap.first_obj_time = hobj.start_time;
}
values.beatmap.bpm = beatmap.bpm();
Some(beatmap)
},
Err(_) => {
println!("Failed to parse beatmap");
None
},
};
values.current_beatmap = current_beatmap;
values.beatmap.paths.beatmap_folder = beatmap_folder;
values.beatmap.paths.beatmap_file = beatmap_file;
values.beatmap.paths.audio_file = audio_file;
values.update_min_max_bpm();
values.update_full_paths();
values.adjust_bpm();
}
}
// store the converted map so it's not converted
// everytime it's used for pp calc
if new_map {
if let Some(map) = &values.current_beatmap {
if let Cow::Owned(converted) = map
.convert_mode(values.menu_gamemode())
{
values.current_beatmap = Some(converted);
}
}
values.update_stars_and_ss_pp();
values.update_current_pp(&mut state.ivalues);
}
let ruleset_addr = p.read_i32(
p.read_i32(state.addresses.rulesets - 0xb)? + 0x4
)?;
let audio_time_ptr = p.read_i32(state.addresses.audio_time_base + 0x9)?;
values.precise_audio_time = p.read_i32(audio_time_ptr)?;
// If this happened there is zero sense to continue
// reading because all the values depends on this
// address
if ruleset_addr == 0 {
return Ok(())
}
// Process result screen
// TODO handle situations when result screen is not ready
if values.state == GameState::ResultScreen {
let result_base = p.read_i32(ruleset_addr+ 0x38)?;
values.result_screen.username = p.read_string(result_base + 0x28)?;
let mods_xor_base = p.read_i32(result_base + 0x1C)?;
// TODO batch
let mods_xor1 = p.read_i32(mods_xor_base + 0xC)?;
let mods_xor2 = p.read_i32(mods_xor_base + 0x8)?;
values.result_screen.mods = (mods_xor1 ^ mods_xor2) as u32;
values.result_screen.mode = p.read_i32(result_base + 0x64)? as u8;
values.result_screen.score = p.read_i32(result_base + 0x78)?;
// TODO batch
values.result_screen.hit_300 = p.read_i16(result_base + 0x8A)?;
values.result_screen.hit_100 = p.read_i16(result_base + 0x88)?;
values.result_screen.hit_50 = p.read_i16(result_base + 0x8C)?;
values.result_screen.hit_geki = p.read_i16(result_base + 0x8E)?;
values.result_screen.hit_katu = p.read_i16(result_base + 0x90)?;
values.result_screen.update_accuracy();
}
// Process gameplay
if values.state == GameState::Playing {
let res = process_gameplay(
p,
state,
&mut values,
ruleset_addr
);
if let Err(e) = res {
println!("{:?}", e);
println!("Skipped gameplay reading, probably it's not ready yet");
}
}
// Handling entering `ResultScreen` state
if values.prev_state != GameState::ResultScreen
&& values.state == GameState::ResultScreen {
if values.prev_state != GameState::Playing {
values.update_current_pp(&mut state.ivalues);
}
values.update_stars_and_ss_pp();
}
// Handling entering `SongSelect` state
if values.prev_state != GameState::SongSelect
&& values.state == GameState::SongSelect {
// Reseting pp's from result screen
if values.prev_state == GameState::ResultScreen {
values.current_pp = 0.0;
}
values.update_current_pp(&mut state.ivalues);
values.update_stars_and_ss_pp();
values.adjust_bpm();
}
// Update stars when entering `Playing` state
if values.prev_state != GameState::Playing
&& values.state == GameState::Playing {
values.reset_gameplay();
values.update_stars_and_ss_pp();
values.adjust_bpm();
}
// Handle mods changes inside `SongSelect` state
if values.state == GameState::SongSelect
&& values.prev_menu_mods != values.menu_mods {
values.update_stars_and_ss_pp();
values.update_current_pp(&mut state.ivalues);
values.adjust_bpm();
}
values.prev_menu_mode = values.menu_mode;
values.prev_menu_mods = menu_mods;
values.prev_state = values.state;
Ok(())
}
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ezpplauncher",
"version": "0.1.0",
"identifier": "farm.ezpp.launcher.app",
"build": {
"beforeDevCommand": "bun run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "bun run build",
"frontendDist": "../build"
},
"app": {
"windows": [
{
"title": "ezpplauncher",
"width": 550,
"height": 350,
"decorations": false,
"resizable": false,
"maximizable": false
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
-222
View File
@@ -1,222 +0,0 @@
<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("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) => {
console.log((e as CustomEvent).detail);
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="dark:active:!bg-gray-900 !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 dark:!bg-gray-700 dark:active:!bg-gray-900 dark:hover:!bg-gray-800 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 dark:!bg-gray-700 dark:active:!bg-gray-900 dark:hover:!bg-gray-800 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 dark:!bg-gray-700 dark:active:!bg-gray-900 dark:hover:!bg-gray-800 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}
+89
View File
@@ -0,0 +1,89 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
* {
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-o-user-select: none;
user-select: none;
-webkit-user-drag: none;
-khtml-user-drag: none;
-moz-user-drag: none;
-o-user-drag: none;
user-drag: none;
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 224 71.4% 4.1%;
--muted: 220 14.3% 95.9%;
--muted-foreground: 220 8.9% 46.1%;
--popover: 0 0% 100%;
--popover-foreground: 224 71.4% 4.1%;
--card: 0 0% 100%;
--card-foreground: 224 71.4% 4.1%;
--border: 220 13% 91%;
--input: 220 13% 91%;
--primary: 220.9 39.3% 11%;
--primary-foreground: 210 20% 98%;
--secondary: 220 14.3% 95.9%;
--secondary-foreground: 220.9 39.3% 11%;
--accent: 220 14.3% 95.9%;
--accent-foreground: 220.9 39.3% 11%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 210 20% 98%;
--ring: 224 71.4% 4.1%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 224 71.4% 4.1%;
--foreground: 210 20% 98%;
--muted: 215 27.9% 16.9%;
--muted-foreground: 217.9 10.6% 64.9%;
--popover: 224 71.4% 4.1%;
--popover-foreground: 210 20% 98%;
--card: 224 71.4% 4.1%;
--card-foreground: 210 20% 98%;
--border: 215 27.9% 16.9%;
--input: 215 27.9% 16.9%;
--primary: 210 20% 98%;
--primary-foreground: 220.9 39.3% 11%;
--secondary: 215 27.9% 16.9%;
--secondary-foreground: 210 20% 98%;
--accent: 215 27.9% 16.9%;
--accent-foreground: 210 20% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 20% 98%;
--ring: 216 12.2% 83.9%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
+15
View File
@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Tauri + SvelteKit + Typescript App</title>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
-85
View File
@@ -1,85 +0,0 @@
/* Write your global styles here, in PostCSS syntax */
@tailwind base;
@tailwind components;
@tailwind utilities;
* {
font-family: "Prompt";
-webkit-user-select: none !important;
user-select: 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%;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

-5
View File
@@ -1,5 +0,0 @@
export enum Page {
Login = 0,
Launch = 1,
Settings = 2,
}
-1
View File
@@ -1 +0,0 @@
/// <reference types="svelte" />
-91
View File
@@ -1,91 +0,0 @@
<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
? $_progress.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 ? $_progress.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>
+13
View File
@@ -0,0 +1,13 @@
import ky from "ky";
const API_ENDPOINT = "https://osu.direct/api/";
export const osudirect = {
osu: async (mapId: number): Promise<string | undefined> => {
try {
return await ky(API_ENDPOINT + `osu/${mapId}?raw`).text();
} catch {
return undefined;
}
},
};
@@ -0,0 +1,16 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.FallbackProps = $props();
</script>
<AvatarPrimitive.Fallback
bind:ref
class={cn("bg-muted flex h-full w-full items-center justify-center rounded-full", className)}
{...restProps}
/>
@@ -0,0 +1,16 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.ImageProps = $props();
</script>
<AvatarPrimitive.Image
bind:ref
class={cn("aspect-square h-full w-full", className)}
{...restProps}
/>
@@ -0,0 +1,16 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.RootProps = $props();
</script>
<AvatarPrimitive.Root
bind:ref
class={cn("relative flex size-10 shrink-0 overflow-hidden rounded-full", className)}
{...restProps}
/>
+13
View File
@@ -0,0 +1,13 @@
import Root from "./avatar.svelte";
import Image from "./avatar-image.svelte";
import Fallback from "./avatar-fallback.svelte";
export {
Root,
Image,
Fallback,
//
Root as Avatar,
Image as AvatarImage,
Fallback as AvatarFallback,
};
@@ -0,0 +1,12 @@
<script lang="ts">
const prop: {beatmapId: number} = $props();
</script>
<div
class="absolute w-screen h-screen overflow-hidden pointer-events-none rounded"
>
<div
style="background: url(https://osu.direct/api/media/background/{prop.beatmapId})"
class="absolute top-0 left-0 w-full h-full !bg-cover -z-10 pointer-events-none blur opacity-10 rounded"
></div> -->
</div>
+50
View File
@@ -0,0 +1,50 @@
<script lang="ts" module>
import { type VariantProps, tv } from "tailwind-variants";
export const badgeVariants = tv({
base: "focus:ring-ring inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
variants: {
variant: {
default:
"bg-primary text-primary-foreground hover:bg-primary/80 border-transparent",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
});
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
</script>
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAnchorAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
href,
class: className,
variant = "default",
children,
...restProps
}: WithElementRef<HTMLAnchorAttributes> & {
variant?: BadgeVariant;
} = $props();
</script>
<svelte:element
this={href ? "a" : "span"}
bind:this={ref}
{href}
class={cn(badgeVariants({ variant, className }))}
{...restProps}
>
{@render children?.()}
</svelte:element>
+2
View File
@@ -0,0 +1,2 @@
export { default as Badge } from "./badge.svelte";
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
@@ -0,0 +1,74 @@
<script lang="ts" module>
import type { WithElementRef } from "bits-ui";
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
import { type VariantProps, tv } from "tailwind-variants";
export const buttonVariants = tv({
base: "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border-input bg-background hover:bg-accent hover:text-accent-foreground border",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
</script>
<script lang="ts">
import { cn } from "$lib/utils.js";
let {
class: className,
variant = "default",
size = "default",
ref = $bindable(null),
href = undefined,
type = "button",
children,
...restProps
}: ButtonProps = $props();
</script>
{#if href}
<a
bind:this={ref}
class={cn(buttonVariants({ variant, size, className }))}
{href}
{...restProps}
>
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
class={cn(buttonVariants({ variant, size, className }))}
{type}
{...restProps}
>
{@render children?.()}
</button>
{/if}
+17
View File
@@ -0,0 +1,17 @@
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
export {
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
};
@@ -0,0 +1,40 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import Check from "lucide-svelte/icons/check";
import Minus from "lucide-svelte/icons/minus";
import { cn } from "$lib/utils.js";
import type { Snippet } from "svelte";
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
children: childrenProp,
...restProps
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
children?: Snippet;
} = $props();
</script>
<DropdownMenuPrimitive.CheckboxItem
bind:ref
bind:checked
bind:indeterminate
class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<span class="absolute left-2 flex size-3.5 items-center justify-center">
{#if indeterminate}
<Minus class="size-4" />
{:else}
<Check class={cn("size-4", !checked && "text-transparent")} />
{/if}
</span>
{@render childrenProp?.()}
{/snippet}
</DropdownMenuPrimitive.CheckboxItem>
@@ -0,0 +1,26 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let {
ref = $bindable(null),
sideOffset = 4,
portalProps,
class: className,
...restProps
}: DropdownMenuPrimitive.ContentProps & {
portalProps?: DropdownMenuPrimitive.PortalProps;
} = $props();
</script>
<DropdownMenuPrimitive.Portal {...portalProps}>
<DropdownMenuPrimitive.Content
bind:ref
{sideOffset}
class={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md outline-none",
className
)}
{...restProps}
/>
</DropdownMenuPrimitive.Portal>
@@ -0,0 +1,19 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
inset,
...restProps
}: DropdownMenuPrimitive.GroupHeadingProps & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.GroupHeading
bind:ref
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...restProps}
/>
@@ -0,0 +1,23 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let {
ref = $bindable(null),
class: className,
inset,
...restProps
}: DropdownMenuPrimitive.ItemProps & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.Item
bind:ref
class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...restProps}
/>
@@ -0,0 +1,23 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { type WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
inset?: boolean;
} = $props();
</script>
<div
bind:this={ref}
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,30 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChild } from "bits-ui";
import Circle from "lucide-svelte/icons/circle";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children: childrenProp,
...restProps
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
</script>
<DropdownMenuPrimitive.RadioItem
bind:ref
class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...restProps}
>
{#snippet children({ checked })}
<span class="absolute left-2 flex size-3.5 items-center justify-center">
{#if checked}
<Circle class="size-2 fill-current" />
{/if}
</span>
{@render childrenProp?.({ checked })}
{/snippet}
</DropdownMenuPrimitive.RadioItem>
@@ -0,0 +1,16 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SeparatorProps = $props();
</script>
<DropdownMenuPrimitive.Separator
bind:ref
class={cn("bg-muted -mx-1 my-1 h-px", className)}
{...restProps}
/>
@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { type WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
class={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...restProps}
>
{@render children?.()}
</span>
@@ -0,0 +1,19 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SubContentProps = $props();
</script>
<DropdownMenuPrimitive.SubContent
bind:ref
class={cn(
"bg-popover text-popover-foreground z-50 min-w-[8rem] rounded-md border p-1 shadow-lg focus:outline-none",
className
)}
{...restProps}
/>
@@ -0,0 +1,28 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.SubTrigger
bind:ref
class={cn(
"data-[highlighted]:bg-accent data-[state=open]:bg-accent flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...restProps}
>
{@render children?.()}
<ChevronRight class="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
@@ -0,0 +1,50 @@
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
import Content from "./dropdown-menu-content.svelte";
import GroupHeading from "./dropdown-menu-group-heading.svelte";
import Item from "./dropdown-menu-item.svelte";
import Label from "./dropdown-menu-label.svelte";
import RadioItem from "./dropdown-menu-radio-item.svelte";
import Separator from "./dropdown-menu-separator.svelte";
import Shortcut from "./dropdown-menu-shortcut.svelte";
import SubContent from "./dropdown-menu-sub-content.svelte";
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
const Sub = DropdownMenuPrimitive.Sub;
const Root = DropdownMenuPrimitive.Root;
const Trigger = DropdownMenuPrimitive.Trigger;
const Group = DropdownMenuPrimitive.Group;
const RadioGroup = DropdownMenuPrimitive.RadioGroup;
export {
CheckboxItem,
Content,
Root as DropdownMenu,
CheckboxItem as DropdownMenuCheckboxItem,
Content as DropdownMenuContent,
Group as DropdownMenuGroup,
GroupHeading as DropdownMenuGroupHeading,
Item as DropdownMenuItem,
Label as DropdownMenuLabel,
RadioGroup as DropdownMenuRadioGroup,
RadioItem as DropdownMenuRadioItem,
Separator as DropdownMenuSeparator,
Shortcut as DropdownMenuShortcut,
Sub as DropdownMenuSub,
SubContent as DropdownMenuSubContent,
SubTrigger as DropdownMenuSubTrigger,
Trigger as DropdownMenuTrigger,
Group,
GroupHeading,
Item,
Label,
RadioGroup,
RadioItem,
Root,
Separator,
Shortcut,
Sub,
SubContent,
SubTrigger,
Trigger,
};
+7
View File
@@ -0,0 +1,7 @@
import Root from "./input.svelte";
export {
Root,
//
Root as Input,
};
+22
View File
@@ -0,0 +1,22 @@
<script lang="ts">
import type { HTMLInputAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
value = $bindable(),
class: className,
...restProps
}: WithElementRef<HTMLInputAttributes> = $props();
</script>
<input
bind:this={ref}
class={cn(
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:value
{...restProps}
/>
+153
View File
@@ -0,0 +1,153 @@
<script lang="ts">
import ezppLogo from "../../../../assets/logo.png";
import { osudirect } from "@/api/osudirect";
import { gameSounds, playAudio } from "@/utils";
import { BeatmapDecoder } from "osu-parsers";
import { onMount } from "svelte";
type logoProps = {
beatmapId: number;
extended: boolean;
onclick: () => void;
};
let { beatmapId, extended, onclick }: logoProps = $props();
let hovered = $state(false);
let bpm = $state(150); // 1000 * 60 / bpm
let lastTimeout: number | undefined = undefined;
onMount(async () => {
gameSounds.preload();
const beatmapData = await osudirect.osu(beatmapId);
if (beatmapData) {
const decoder = new BeatmapDecoder();
const beatmap = decoder.decodeFromString(beatmapData);
console.log(beatmap);
const audio = new Audio(
`https://osu.direct/api/media/audio/${beatmapId}`
);
audio.volume = 0.3;
// Function to play the heartbeat sound
const playHeartbeat = () => {
gameSounds.play("menuHeartbeat", {
volume: hovered ? 1 : 0.3,
});
};
// Function to synchronize the heartbeat with the song
const syncHeartbeat = () => {
const currentTime = audio.currentTime * 1000 - 25; // Convert to milliseconds
const timingPoint = beatmap.controlPoints.timingPointAt(currentTime);
const timingPointTime = timingPoint.startTime;
console.log(currentTime, timingPointTime);
if (timingPoint && bpm !== timingPoint.bpm) {
bpm = timingPoint.bpm;
if (lastTimeout) window.clearTimeout(lastTimeout);
const interval = (1000 * 60) / bpm; // Interval in milliseconds
const nextBeat = currentTime % interval; // Invert: time since last beat
// Schedule the next heartbeat at the correct time
lastTimeout = window.setTimeout(() => {
playHeartbeat();
// Clear any previous interval
if (lastTimeout) {
window.clearInterval(lastTimeout);
lastTimeout = undefined;
}
if (!lastTimeout)
lastTimeout = window.setInterval(playHeartbeat, interval);
}, nextBeat);
}
// Continue syncing
requestAnimationFrame(syncHeartbeat);
};
// Wait for audio to be ready before starting playback and syncing
audio.addEventListener("canplay", async () => {
audio.addEventListener("play", syncHeartbeat);
audio.addEventListener("ended", async () => await audio.play());
await audio.play();
syncHeartbeat();
});
}
});
</script>
<div
class="w-screen {extended
? hovered
? '-translate-y-1 scale-100'
: '-translate-y-1 scale-90'
: hovered
? 'translate-y-5 scale-150'
: 'translate-y-5 scale-125'} transition-transform select-none"
>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="logo-animation relative w-44 h-44 mx-auto cursor-pointer"
onmouseenter={() => (hovered = true)}
onmouseleave={() => (hovered = false)}
onclick={() => {
if (extended) {
gameSounds.play("menuBack");
} else {
gameSounds.play("menuHit");
}
onclick();
}}
>
<img
class="absolute pulse-logo"
style="animation-duration: {(1000 * 60) / bpm}ms;"
src={ezppLogo}
alt="logo-pulse"
/>
<img
class="absolute main-logo"
style="animation-duration: {(1000 * 60) / bpm}ms;"
src={ezppLogo}
alt="logo"
/>
</div>
</div>
<style lang="scss">
.logo-animation {
.pulse-logo {
animation: 0.5s 0.2s infinite forwards beat-pulse;
}
.main-logo {
animation: 0.5s infinite forwards beat;
}
}
@keyframes beat {
0%,
100% {
scale: 1.08;
}
90% {
scale: 1;
}
}
@keyframes beat-pulse {
0%,
10% {
scale: 0.5;
opacity: 0.5;
filter: blur(0px);
}
100% {
scale: 1.4;
opacity: 0;
filter: blur(2px);
}
}
</style>
@@ -0,0 +1,20 @@
<script lang="ts">
let {
loadingText,
progress,
indeterminate,
}: { loadingText: string; progress: number; indeterminate: boolean } =
$props();
</script>
<div
class="relative flex items-center justify-center w-[80vw] h-10 bg-gray-900/70 border rounded overflow-hidden"
>
<div
class="absolute h-full rounded top-0 left-0 bg-pink-600 transition-all"
style="width: {indeterminate
? 100
: Math.min(100, Math.max(0, progress))}%;"
></div>
<p class="text-xs z-10">{loadingText}</p>
</div>
@@ -0,0 +1,59 @@
<script lang="ts">
import Minimize from "lucide-svelte/icons/minus";
import Close from "lucide-svelte/icons/x";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { onMount } from "svelte";
onMount(() => {
const appWindow = getCurrentWindow();
document
.getElementById("titlebar-minimize")
?.addEventListener("click", () => appWindow.minimize());
document
.getElementById("titlebar-close")
?.addEventListener("click", () => appWindow.close());
});
</script>
<div data-tauri-drag-region class="titlebar z-[9999]">
<!-- <p class="mr-auto ms-2 text-sm">EZPPLauncher</p> -->
<div class="titlebar-button" id="titlebar-minimize">
<Minimize size={18} />
</div>
<div class="titlebar-button close rounded-tr" id="titlebar-close">
<Close size={18} />
</div>
</div>
<style lang="scss">
.titlebar {
height: 30px;
/* background: #040612; */
user-select: none;
display: flex;
justify-content: flex-end;
align-items: center;
position: fixed;
top: 0;
left: 0;
right: 0;
margin-bottom: 10px;
}
.titlebar-button {
display: inline-flex;
justify-content: center;
align-items: center;
width: 45px;
height: 100%;
user-select: none;
-webkit-user-select: none;
}
.titlebar-button:hover {
&.close {
background: #c22e2e;
}
background: #2d3049;
}
</style>
+21
View File
@@ -0,0 +1,21 @@
import { createAudioStore } from "@elron/svelte-audio-store";
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
const sounds = {
menuHeartbeat: "/audio/menuHeartbeat.mp3",
menuBack: "/audio/menuBack.wav",
menuHit: "/audio/menuHit.wav",
};
export const gameSounds = createAudioStore(sounds);
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export const playAudio = (path: string, volume: number) => {
const audio = new Audio(path);
audio.volume = volume;
audio.play();
};
-8
View File
@@ -1,8 +0,0 @@
import "./app.pcss";
import App from "./App.svelte";
const app = new App({
target: document.body,
});
export default app;
-55
View File
@@ -1,55 +0,0 @@
<script lang="ts">
import { Button, Checkbox } 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="dark:active:!bg-gray-900 {$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
@@ -1,220 +0,0 @@
<script lang="ts">
import { Input, Button, Spinner, Checkbox } from "flowbite-svelte";
import type { User } from "../types/user";
import type { Error } from "../types/error";
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: "Failed to login.",
},
{
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;
console.log(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"
disabled={loading}
bind:value={username}
/>
<Input
type={showPassword ? "text" : "password"}
placeholder="Password"
size="md"
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="dark:active:!bg-gray-900 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>
-88
View File
@@ -1,88 +0,0 @@
<script lang="ts">
import { Button, ButtonGroup, Input, Toggle } from "flowbite-svelte";
import { FileSearchSolid, FolderSolid } from "flowbite-svelte-icons";
import { patch, presence } 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"
);
patch.set(settingPatch ? settingPatch.val == "true" : true);
presence.set(settingPresence ? settingPresence.val == "true" : true);
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 } })
);
};
</script>
<main
class="h-[265px] flex flex-col justify-start p-3 animate-fadeIn opacity-0"
>
<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
>
</div>
<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"
class="dark:active:!bg-gray-900"
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="dark:active:!bg-gray-900 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>
</main>
+10
View File
@@ -0,0 +1,10 @@
<script lang="ts">
import Titlebar from "@/components/ui/titlebar/titlebar.svelte";
import "../app.css";
let { children } = $props();
</script>
<Titlebar />
<main>
{@render children()}
</main>
+6
View File
@@ -0,0 +1,6 @@
// Tauri doesn't have a Node.js server to do proper SSR
// so we will use adapter-static to prerender the app (SSG)
// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info
export const prerender = true;
export const ssr = false;
+95
View File
@@ -0,0 +1,95 @@
<script lang="ts">
import Background from "@/components/ui/background/background.svelte";
import Button from "@/components/ui/button/button.svelte";
import Logo from "@/components/ui/logo/logo.svelte";
import * as Avatar from "@/components/ui/avatar";
import * as DropdownMenu from "@/components/ui/dropdown-menu";
import Progressbar from "@/components/ui/progressbar/progressbar.svelte";
import Settings from "lucide-svelte/icons/settings";
import LogOut from "lucide-svelte/icons/log-out";
import Heart from "lucide-svelte/icons/heart";
import { badgeVariants } from "@/components/ui/badge";
import { twMerge } from "tailwind-merge";
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
let progress = $state(0);
let extended = $state(false);
let beatmapId = $state(3820896);
const current = WebviewWindow.getCurrent();
current.setAlwaysOnTop(true);
</script>
<div class="relative h-screen w-screen">
<Background />
<div class="absolute z-20 top-2 right-2 py-7">
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<div class="relative">
<p
class={twMerge(
badgeVariants(),
"p-0 h-5 w-5 absolute -right-0.5 -top-0.5 z-50 !bg-pink-600 border-2 border-pink-800 text-white"
)}
>
<Heart class="h-3 w-3 m-auto p-0" />
</p>
<Avatar.Root class="border-[3px] z-40">
<Avatar.AvatarFallback>U</Avatar.AvatarFallback>
<Avatar.AvatarImage src="https://a.ez-pp.farm/1001"
></Avatar.AvatarImage>
</Avatar.Root>
</div>
</DropdownMenu.Trigger>
<DropdownMenu.Content class="w-48 max-w-48 mx-2" side="bottom">
<DropdownMenu.Group>
<DropdownMenu.GroupHeading class="truncate"
>Hello, Quetzalcoatl!</DropdownMenu.GroupHeading
>
<DropdownMenu.Separator />
<DropdownMenu.Group>
<DropdownMenu.Item class="cursor-pointer">
<Settings class="mr-2 size-4" />
<span>Settings</span>
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item class="cursor-pointer">
<LogOut class="mr-2 size-4" />
<span>Log out</span>
</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
<div
class="absolute top-0 left-0 py-3 w-full h-screen flex flex-col gap-16 items-center justify-end overflow-hidden"
>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<Logo {beatmapId} {extended} onclick={() => (extended = !extended)} />
<div
class="{extended
? 'opacity-100 translate-y-0'
: 'opacity-0 translate-y-1'} flex flex-row gap-1 items-center transition-all select-none"
>
<Progressbar
loadingText="Waiting for launch..."
{progress}
indeterminate={false}
/>
<Button
onclick={() => {
console.log(progress);
if (progress >= 100) {
progress = 0;
} else {
progress += 10;
}
}}
>
Launch
</Button>
</div>
</div>
</div>
-14
View File
@@ -1,14 +0,0 @@
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 osuPath: Writable<undefined | string> = writable(undefined);
export const patch = writable(true);
export const presence = writable(true);
export const currentUser: Writable<undefined | User> = writable(undefined);
export const currentPage = writable(Page.Login);
-4
View File
@@ -1,4 +0,0 @@
export type Error = {
code: number;
message: string;
};
-5
View File
@@ -1,5 +0,0 @@
declare module "*.jpg";
declare module "*.jpeg";
declare module "*.png";
declare module "*.svg";
declare module "*.svelte";
-6
View File
@@ -1,6 +0,0 @@
export type User = {
id: number;
donor: boolean;
name: string;
email: string;
};
-3
View File
@@ -1,3 +0,0 @@
export const clamp = (val: number, min: number, max: number) => {
return Math.max(min, Math.min(val, max));
};
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More