chore: add custom cursor, add config system

This commit is contained in:
HorizonCode 2025-06-29 22:50:31 +02:00
parent 60e3102257
commit 807ad60d62
13 changed files with 315 additions and 37 deletions

View File

@ -13,6 +13,9 @@
"@tauri-apps/plugin-shell": "2.3.0",
"@tauri-apps/plugin-sql": "2.3.0",
"animejs": "^4.0.2",
"buffer": "^6.0.3",
"crypto": "^1.0.1",
"crypto-js": "^4.2.0",
"ky": "1.8.1",
"lucide-svelte": "0.523.0",
"osu-classes": "3.1.0",
@ -25,6 +28,7 @@
"@sveltejs/kit": "2.22.2",
"@sveltejs/vite-plugin-svelte": "5.1.0",
"@tauri-apps/cli": "2.6.1",
"@types/crypto-js": "^4.2.2",
"autoprefixer": "10.4.21",
"bits-ui": "^1.4.7",
"clsx": "2.1.1",
@ -227,6 +231,8 @@
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
"@types/crypto-js": ["@types/crypto-js@4.2.2", "", {}, "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/node": ["@types/node@20.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA=="],
@ -253,6 +259,8 @@
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
"bits-ui": ["bits-ui@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.6.4", "@floating-ui/dom": "^1.6.7", "@internationalized/date": "^3.5.6", "css.escape": "^1.5.1", "esm-env": "^1.1.2", "runed": "^0.23.2", "svelte-toolbelt": "^0.7.1", "tabbable": "^6.2.0" }, "peerDependencies": { "svelte": "^5.11.0" } }, "sha512-CXD6Orp7l8QevNDcRPLXc/b8iMVgxDWT2LyTwsdLzJKh9CxesOmPuNePSPqAxKoT59FIdU4aFPS1k7eBdbaCxg=="],
@ -263,6 +271,8 @@
"browserslist": ["browserslist@4.25.1", "", { "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw=="],
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
"buffer-builder": ["buffer-builder@0.2.0", "", {}, "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg=="],
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
@ -285,6 +295,10 @@
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"crypto": ["crypto@1.0.1", "", {}, "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig=="],
"crypto-js": ["crypto-js@4.2.0", "", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="],
"css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
@ -337,6 +351,8 @@
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"immutable": ["immutable@5.1.3", "", {}, "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg=="],
"inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="],

View File

@ -25,6 +25,9 @@
"@tauri-apps/plugin-shell": "2.3.0",
"@tauri-apps/plugin-sql": "2.3.0",
"animejs": "^4.0.2",
"buffer": "^6.0.3",
"crypto": "^1.0.1",
"crypto-js": "^4.2.0",
"ky": "1.8.1",
"lucide-svelte": "0.523.0",
"osu-classes": "3.1.0",
@ -37,6 +40,7 @@
"@sveltejs/kit": "2.22.2",
"@sveltejs/vite-plugin-svelte": "5.1.0",
"@tauri-apps/cli": "2.6.1",
"@types/crypto-js": "^4.2.2",
"autoprefixer": "10.4.21",
"bits-ui": "^1.4.7",
"clsx": "2.1.1",

24
src-tauri/Cargo.lock generated
View File

@ -836,7 +836,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@ -966,7 +966,7 @@ dependencies = [
"rustc_version",
"toml",
"vswhom",
"winreg",
"winreg 0.55.0",
]
[[package]]
@ -1073,6 +1073,7 @@ dependencies = [
name = "ezpplauncher"
version = "0.1.0"
dependencies = [
"hardware-id",
"serde",
"serde_json",
"serde_repr",
@ -1586,6 +1587,16 @@ dependencies = [
"syn 2.0.90",
]
[[package]]
name = "hardware-id"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3165b5280ce11f886e353961b966becc45a598a9440707dec3e1cdd8f07cbe7"
dependencies = [
"thiserror 1.0.69",
"winreg 0.10.1",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@ -5809,6 +5820,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [
"winapi",
]
[[package]]
name = "winreg"
version = "0.55.0"

View File

@ -26,6 +26,7 @@ serde_repr = "0.1.20"
tauri-plugin-sql = "2.3.0"
tauri-plugin-dialog = "2.3.0"
tauri-plugin-fs = "2.4.0"
hardware-id = "0.3.0"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-single-instance = "2.3.0"

View File

@ -14,6 +14,62 @@
"core:window:allow-start-dragging",
"core:window:allow-minimize",
"core:window:allow-close",
"fs:default"
"fs:default",
{
"identifier": "fs:allow-write",
"allow": [
{
"path": "$HOME/**/*"
}
]
},
{
"identifier": "fs:allow-read",
"allow": [
{
"path": "$HOME/**/*"
}
]
},
{
"identifier": "fs:allow-exists",
"allow": [
{
"path": "$HOME/**/*"
}
]
},
{
"identifier": "fs:allow-write-file",
"allow": [
{
"path": "$HOME/**/*"
}
]
},
{
"identifier": "fs:allow-read-file",
"allow": [
{
"path": "$HOME/**/*"
}
]
},
{
"identifier": "fs:allow-read-text-file",
"allow": [
{
"path": "$HOME/**/*"
}
]
},
{
"identifier": "fs:allow-mkdir",
"allow": [
{
"path": "$HOME/**/*"
}
]
}
]
}

View File

@ -1,6 +1,13 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
use hardware_id::get_id;
use tauri::Manager;
#[tauri::command]
fn get_hwid() -> String {
let hwid = get_id().unwrap();
hwid.into()
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let mut builder = tauri::Builder::default().plugin(tauri_plugin_fs::init());
@ -16,6 +23,7 @@ pub fn run() {
}
builder
.invoke_handler(tauri::generate_handler![get_hwid])
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_sql::Builder::default().build())

View File

@ -4,6 +4,9 @@
import { animate } from 'animejs';
import { onMount } from 'svelte';
let { smoothCursor = true }: { smoothCursor?: boolean } =
$props();
let mouseX = $state(0);
let mouseY = $state(0);
let rotation = $state(0);
@ -75,7 +78,7 @@
}
animate(cursor, {
duration: 180,
duration: smoothCursor ? 180 : 0,
translateX: mouseX,
translateY: mouseY - 50,
ease: (t: number) => (t - 1) ** 3 + 1,
@ -128,19 +131,12 @@
let cursorInner: HTMLDivElement;
let cursorAdditive: HTMLImageElement;
/* onMount(() => {
const processMouseMove = (e: MouseEvent) =>
handleMouseMove(e.clientX, e.clientY, e.pageX, e.pageY);
document.addEventListener('pointermove', processMouseMove);
document.addEventListener('pointerdown', handleMouseDown);
document.addEventListener('pointerup', handleMouseUp);
onMount(() => {
document.documentElement.classList.add('hiddenCursor');
return () => {
document.removeEventListener('pointermove', processMouseMove);
document.removeEventListener('pointerdown', handleMouseDown);
document.removeEventListener('pointerup', handleMouseUp);
document.documentElement.classList.remove('hiddenCursor');
};
}); */
});
</script>
<svelte:window
@ -148,8 +144,10 @@
onmousedown={handleMouseDown}
onmouseup={handleMouseUp}
/>
<div class="h-7 w-7 fixed pointer-events-none z-[99999]" bind:this={cursor}>
<div
class="h-7 w-7 fixed pointer-events-none z-[99999]"
bind:this={cursor}
>
<div class="relative">
<img class="absolute top-0 left-0" src={cursor_default} bind:this={cursorInner} alt="cursor" />
<img
@ -162,15 +160,15 @@
</div>
<style>
:global(html),
:global(body),
:global(*),
:global(*:hover),
:global(button),
:global(a),
:global(input),
:global(select),
:global(textarea) {
:global(html.hiddenCursor),
:global(html.hiddenCursor body),
:global(html.hiddenCursor *),
:global(html.hiddenCursor *:hover),
:global(html.hiddenCursor button),
:global(html.hiddenCursor a),
:global(html.hiddenCursor input),
:global(html.hiddenCursor select),
:global(html.hiddenCursor textarea) {
cursor: none !important;
}
</style>

77
src/lib/config.ts Normal file
View File

@ -0,0 +1,77 @@
import {
BaseDirectory,
exists,
mkdir,
readFile,
readTextFile,
writeFile,
} from '@tauri-apps/plugin-fs';
import * as path from '@tauri-apps/api/path';
import { invoke } from '@tauri-apps/api/core';
import { Crypto } from './crypto';
import { enc } from 'crypto-js';
export class Config {
private config: Record<string, unknown> = {};
private crypto: Crypto | undefined;
private configFilePath: string | undefined;
async init() {
const hwid: string = (await invoke('get_hwid')) ?? 'recorderinsandybridge';
this.crypto = new Crypto(hwid);
const homeDir = await path.homeDir();
const folderPath = await path.join(homeDir, '.ezpplauncher');
this.configFilePath = await path.join(folderPath, 'user_settings');
const createFolder = !(await exists(folderPath));
if (createFolder) await mkdir(folderPath);
const createConfig = !(await exists(this.configFilePath));
if (createConfig) await this.save();
else await this.load();
}
private async load() {
if (!this.configFilePath) throw Error('configFilePath not set');
if (!this.crypto) throw Error('crypto not initialized');
const fileStream = await readTextFile(this.configFilePath);
try {
const decryptedJSON = JSON.parse(this.crypto.decrypt(fileStream)) as Record<string, unknown>;
this.config = decryptedJSON;
console.log('config file loaded');
console.log(JSON.stringify(this.config));
} catch (err) {
console.log('failed to read file');
this.config = {};
await this.save();
}
}
async save() {
if (!this.configFilePath) throw Error('configFilePath not set');
if (!this.crypto) throw Error('crypto not initialized');
const encryptedJSON = this.crypto.encrypt(JSON.stringify(this.config));
console.log(this.config);
console.log('saving file...');
console.log(encryptedJSON);
await writeFile(this.configFilePath, Buffer.from(encryptedJSON), {
append: false,
});
}
value(key: string) {
console.log(this.config);
return {
set: <T>(val: T) => {
this.config[key] = val;
},
get: <T>(fallback: T): T => {
return (this.config[key] as T) ?? fallback;
},
};
}
}

37
src/lib/crypto.ts Normal file
View File

@ -0,0 +1,37 @@
import cryptojs from 'crypto-js';
export class Crypto {
private key: cryptojs.lib.WordArray;
private ivLength: number;
constructor(key: string, opts?: { ivLength?: number }) {
this.key = cryptojs.SHA256(key);
this.ivLength = opts?.ivLength ?? 16;
}
encrypt(str: string): string {
const iv = cryptojs.lib.WordArray.random(this.ivLength);
const encrypted = cryptojs.AES.encrypt(str, this.key, { iv });
const ivBase64 = iv.toString(cryptojs.enc.Base64);
const ctBase64 = encrypted.ciphertext.toString(cryptojs.enc.Base64);
return `${ivBase64}:${ctBase64}`;
}
decrypt(data: string): string {
const [ivBase64, ctBase64] = data.split(':');
if (!ivBase64 || !ctBase64) throw new Error('Invalid input format');
const iv = cryptojs.enc.Base64.parse(ivBase64);
const ciphertext = cryptojs.enc.Base64.parse(ctBase64);
const cipherParams = cryptojs.lib.CipherParams.create({
ciphertext,
});
const decrypted = cryptojs.AES.decrypt(cipherParams, this.key, { iv });
return decrypted.toString(cryptojs.enc.Utf8);
}
}

7
src/lib/userSettings.ts Normal file
View File

@ -0,0 +1,7 @@
import { writable } from 'svelte/store';
import { Config } from './config';
export const userSettings = writable<Config>(new Config());
export const customCursor = writable<boolean>(true);
export const cursorSmoothening = writable<boolean>(true);

View File

@ -6,7 +6,17 @@
import * as Select from '@/components/ui/select';
import { beatmap_sets, online_friends, server_connection_fails, server_ping } from '@/global';
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
import { LoaderCircle, Logs, Music2, Play, Users, Wifi, Gamepad2, WifiOff, Settings2 } from 'lucide-svelte';
import {
LoaderCircle,
Logs,
Music2,
Play,
Users,
Wifi,
Gamepad2,
WifiOff,
Settings2,
} from 'lucide-svelte';
import { Circle } from 'radix-icons-svelte';
import NumberFlow from '@number-flow/svelte';
import * as AlertDialog from '@/components/ui/alert-dialog';
@ -15,6 +25,7 @@
import { fade, fly, scale } from 'svelte/transition';
import { Checkbox } from '@/components/ui/checkbox';
import Label from '@/components/ui/label/label.svelte';
import { cursorSmoothening, customCursor, userSettings } from '@/userSettings';
let selectedTab = $state('settings');
let launching = $state(false);
@ -273,24 +284,48 @@
class="bg-theme-900/90 flex flex-col justify-center gap-3 border border-theme-800/90 rounded-lg"
in:scale={{ duration: 400, start: 0.98 }}
>
<div class="flex flex-row items-center gap-3 font-semibold text-xl px-3 pt-3"><Settings2 /> EZPPLauncher Settings</div>
<div class="grid grid-cols-[1fr_auto] gap-y-5 items-center border-t border-theme-800 py-3 px-6">
<div class="flex flex-row items-center gap-3 font-semibold text-xl px-3 pt-3">
<Settings2 /> EZPPLauncher Settings
</div>
<div
class="grid grid-cols-[1fr_auto] gap-y-5 items-center border-t border-theme-800 py-3 px-6"
>
<div class="flex flex-col">
<Label class="text-sm" for="setting-custom-cursor">Lazer-Style Cursor</Label>
<div class="text-muted-foreground text-xs">
Enable a custom cursor in the Launcher like in the lazer build of osu!
</div>
</div>
<Checkbox id="setting-custom-cursor" class="flex items-center justify-center w-5 h-5"
<Checkbox
id="setting-custom-cursor"
checked={$customCursor}
onCheckedChange={async (e) => {
if (!e) {
cursorSmoothening.set(false);
}
customCursor.set(e);
$userSettings.save();
}}
class="flex items-center justify-center w-5 h-5"
></Checkbox>
<div class="flex flex-col">
<Label class="text-sm" for="setting-custom-cursor">Cursor Smoothening</Label>
<Label class="text-sm" for="setting-cursor-smoothening">Cursor Smoothening</Label>
<div class="text-muted-foreground text-xs">
Makes the custom cursor movement smoother.
</div>
</div>
<Checkbox id="setting-custom-cursor" class="flex items-center justify-center w-5 h-5"
<Checkbox
id="setting-cursor-smoothening"
checked={$cursorSmoothening}
onCheckedChange={async (e) => {
if (!$customCursor) return;
cursorSmoothening.set(e);
$userSettings.save();
}}
disabled={!$customCursor}
class="flex items-center justify-center w-5 h-5"
></Checkbox>
</div>
</div>

View File

@ -4,12 +4,31 @@
import { setupValues } from '@/global';
import { onMount } from 'svelte';
import OsuCursor from '@/components/ui/osu-cursor/OsuCursor.svelte';
import { cursorSmoothening, customCursor, userSettings } from '@/userSettings';
import { Buffer } from 'buffer';
let { children } = $props();
onMount(setupValues);
onMount(async () => {
window.Buffer = Buffer;
setupValues();
await $userSettings.init();
const config_custom_cursor = $userSettings.value('custom_cursor');
const config_cursor_smoothening = $userSettings.value('cursor_smoothening');
console.log("yes", config_cursor_smoothening.get(true))
customCursor.set(config_custom_cursor.get(true));
cursorSmoothening.set(config_cursor_smoothening.get(true));
customCursor.subscribe((val) => config_custom_cursor.set(val));
cursorSmoothening.subscribe((val) => config_cursor_smoothening.set(val));
});
</script>
<OsuCursor />
{#if $customCursor}
<OsuCursor smoothCursor={$cursorSmoothening} />
{/if}
<Titlebar />
<main>

View File

@ -1,6 +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;