chore: add custom cursor, add config system
This commit is contained in:
@@ -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
77
src/lib/config.ts
Normal 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
37
src/lib/crypto.ts
Normal 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
7
src/lib/userSettings.ts
Normal 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);
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
||||
|
||||
export const ssr = false;
|
Reference in New Issue
Block a user