From 651592c333d3c6d833561fd96f406818e76d20f9 Mon Sep 17 00:00:00 2001 From: HorizonCode Date: Thu, 3 Jul 2025 11:46:50 +0200 Subject: [PATCH] feat: Refactor API interactions and enhance user settings management - Updated ezpp API to include user info retrieval and improved error handling. - Introduced new writable stores for current user info and loading states. - Added gamemode enums and utility functions for better gamemode handling. - Refactored global state management to use consistent naming conventions. - Enhanced loading and login components to provide better user feedback. - Updated user settings to include preferred mode and type. - Improved layout and page components for better state management and user experience. --- src/lib/api/ezpp.ts | 44 ++++-- src/lib/data.ts | 4 + src/lib/gamemode.ts | 186 +++++++++++++++++++++++ src/lib/global.ts | 35 +++-- src/lib/types.ts | 91 +++++++++++ src/lib/userSettings.ts | 7 +- src/lib/utils.ts | 39 ++++- src/pages/Launch.svelte | 284 +++++++++++++++++++++++++++-------- src/pages/Loading.svelte | 80 +++++++--- src/pages/Login.svelte | 16 +- src/pages/SetupWizard.svelte | 11 +- src/routes/+layout.svelte | 7 +- src/routes/+page.svelte | 4 +- 13 files changed, 680 insertions(+), 128 deletions(-) create mode 100644 src/lib/data.ts create mode 100644 src/lib/gamemode.ts diff --git a/src/lib/api/ezpp.ts b/src/lib/api/ezpp.ts index 13709dd..9d4f8d6 100644 --- a/src/lib/api/ezpp.ts +++ b/src/lib/api/ezpp.ts @@ -1,14 +1,22 @@ -import type { EZPPUser } from '@/types'; +import type { EZPPUser, EZPPUserInfoResponse, EZPPUserResponse } from '@/types'; import { betterFetch } from '@better-fetch/fetch'; const BANCHO_ENDPOINT = 'https://c.ez-pp.farm/'; +const API_ENDPOINT = 'https://api.ez-pp.farm/'; const ENDPOINT = 'https://ez-pp.farm/'; +const timeout = 5000; // 5 seconds; + export const ezppfarm = { ping: async (): Promise => { try { const start = Date.now(); - const request = await betterFetch(BANCHO_ENDPOINT); + const request = await betterFetch(BANCHO_ENDPOINT, { + timeout, + headers: { + 'User-Agent': 'EZPPLauncher', + }, + }); if (request.error) return undefined; const ping = Date.now() - start; return ping; @@ -23,32 +31,22 @@ export const ezppfarm = { | { code: number; message: string; - user?: { - id: number; - donor: boolean; - name: string; - email: string; - }; + user?: EZPPUser; } | undefined > => { - const request = await betterFetch<{ - code: number; - message: string; - user?: EZPPUser; - }>('https://ez-pp.farm/login/check', { + const request = await betterFetch(`${ENDPOINT}login/check`, { method: 'POST', + timeout, body: JSON.stringify({ username: username, password: password, }), headers: { 'Content-Type': 'application/json', - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0', + 'User-Agent': 'EZPPLauncher', }, }); - console.log(request.error); if (request.error) { if (request.error.status >= 500 && request.error.status < 600) throw new Error('Server not reachable'); @@ -56,4 +54,18 @@ export const ezppfarm = { } return request.data; }, + getUserInfo: async (userId: number) => { + const request = await betterFetch(`${API_ENDPOINT}v1/get_player_info`, { + timeout, + query: { + id: userId, + scope: 'all', + }, + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'EZPPLauncher', + }, + }); + return request.error ? undefined : request.data; + }, }; diff --git a/src/lib/data.ts b/src/lib/data.ts new file mode 100644 index 0000000..3fc5cda --- /dev/null +++ b/src/lib/data.ts @@ -0,0 +1,4 @@ +import { writable } from 'svelte/store'; +import type { EZPPUserInfo } from './types'; + +export const currentUserInfo = writable(undefined); diff --git a/src/lib/gamemode.ts b/src/lib/gamemode.ts new file mode 100644 index 0000000..af3e47d --- /dev/null +++ b/src/lib/gamemode.ts @@ -0,0 +1,186 @@ +export enum Gamemodes { + VANILLA_OSU = 0, + VANILLA_TAIKO = 1, + VANILLA_CATCH = 2, + VANILLA_MANIA = 3, + + RELAX_OSU = 4, + RELAX_TAIKO = 5, + RELAX_CATCH = 6, + + AUTOPILOT_OSU = 8, +} + +export enum Mode { + OSU = 0, + TAIKO = 1, + CATCH = 2, + MANIA = 3, +} + +export enum Type { + VANILLA = 0, + RELAX = 4, + AUTOPILOT = 8, +} + +export const validModes = [Mode.OSU, Mode.TAIKO, Mode.CATCH, Mode.MANIA]; +export const validTypes = [Type.VANILLA, Type.RELAX, Type.AUTOPILOT]; +export const validModeTypeCombinations = [0, 1, 2, 3, 4, 5, 6, 8]; +export const validModeTypeCombinationsSorted = [0, 4, 8, 1, 5, 2, 6, 3]; + +export const validMode = (modeStr: string) => modeStrToInt(modeStr) !== undefined; +export const validType = (typeStr: string) => typeStrToInt(typeStr) !== undefined; + +export const modeStrToInt = (modeStr: 'osu' | 'taiko' | 'catch' | 'mania' | string) => { + switch (modeStr) { + case 'taiko': + return Mode.TAIKO; + case 'catch': + return Mode.CATCH; + case 'mania': + return Mode.MANIA; + case 'osu': + return Mode.OSU; + } + return undefined; +}; + +export const modeIntToStr = (modeInt: number) => { + switch (modeInt) { + case Mode.TAIKO: + return 'taiko'; + case Mode.CATCH: + return 'catch'; + case Mode.MANIA: + return 'mania'; + case Mode.OSU: + return 'osu'; + } + return undefined; +}; + +export const typeStrToInt = (typeStr: 'vanilla' | 'relax' | 'autopilot' | string) => { + switch (typeStr) { + case 'relax': + return Type.RELAX; + case 'autopilot': + return Type.AUTOPILOT; + case 'vanilla': + return Type.VANILLA; + } + return undefined; +}; + +export const typeIntToStr = (typeInt: number) => { + switch (typeInt) { + case Type.RELAX: + return 'relax'; + case Type.AUTOPILOT: + return 'autopilot'; + case Type.VANILLA: + return 'vanilla'; + } + return undefined; +}; + +export const getGamemodeInt = ( + mode: 'osu' | 'taiko' | 'catch' | 'mania' | string | undefined, + type: 'vanilla' | 'relax' | 'autopilot' | string | undefined +) => { + let modee = 0; + switch (mode) { + case 'taiko': + modee += Mode.TAIKO; + break; + case 'catch': + modee += Mode.CATCH; + break; + case 'mania': + modee += Mode.MANIA; + break; + } + + switch (type) { + case 'relax': + modee += Type.RELAX; + break; + case 'autopilot': + modee += Type.AUTOPILOT; + break; + } + + return modee; +}; + +export const getGamemodeName = ( + mode: 'osu' | 'taiko' | 'catch' | 'mania' | string | undefined, + type: 'vanilla' | 'relax' | 'autopilot' | string | undefined +) => { + let modeStr = ''; + switch (mode) { + case 'taiko': + modeStr += 'taiko!'; + break; + case 'catch': + modeStr += 'catch!'; + break; + case 'mania': + modeStr += 'mania!'; + break; + default: + modeStr += 'osu!'; + break; + } + + switch (type) { + case 'relax': + modeStr += 'rx'; + break; + case 'autopilot': + modeStr += 'ap'; + break; + default: + modeStr += 'vn'; + break; + } + + return modeStr; +}; + +export const getModeAndTypeFromGamemode = (gamemode: number) => { + let mode = Mode.OSU; + let type = Type.VANILLA; + const vanillaMode = gamemode % 4; + + switch (vanillaMode) { + case Mode.TAIKO: + mode = Mode.TAIKO; + break; + case Mode.CATCH: + mode = Mode.CATCH; + break; + case Mode.MANIA: + mode = Mode.MANIA; + break; + } + + const typee = gamemode - vanillaMode; + switch (typee) { + case Type.RELAX: + type = Type.RELAX; + break; + case Type.AUTOPILOT: + type = Type.AUTOPILOT; + break; + } + + return { + mode, + type, + }; +}; + +export const isValidGamemode = (gamemodeInt: number) => { + return validModeTypeCombinations.includes(gamemodeInt); +}; diff --git a/src/lib/global.ts b/src/lib/global.ts index 5fc36ba..ff9f47b 100644 --- a/src/lib/global.ts +++ b/src/lib/global.ts @@ -3,15 +3,18 @@ import { ezppfarm } from './api/ezpp'; import type { Component } from 'svelte'; import Loading from '../pages/Loading.svelte'; -export const current_view = writable(Loading); -export const first_startup = writable(false); +export const currentView = writable(Loading); -export const server_ping = writable(undefined); -export const server_connection_fails = writable(0); +export const currentLoadingInfo = writable('Initializing...'); -export const online_friends = writable(undefined); +export const firstStartup = writable(false); -export const beatmap_sets = writable(undefined); +export const serverPing = writable(undefined); +export const serverConnectionFails = writable(0); + +export const onlineFriends = writable(undefined); + +export const beatmapSets = writable(undefined); export const setupValues = () => { updatePing(); @@ -27,23 +30,19 @@ export const setupValues = () => { }; const updatePing = async () => { - const serverPing = await ezppfarm.ping(); - if (!serverPing) { - server_connection_fails.update((num) => num + 1); + const currentServerPing = await ezppfarm.ping(); + if (!currentServerPing) { + serverConnectionFails.update((num) => num + 1); } else { - server_connection_fails.set(0); - server_ping.set(serverPing); + serverConnectionFails.set(0); + serverPing.set(currentServerPing); } }; const updateFriends = async () => { await new Promise((res) => setTimeout(res, Math.random() * 300)); - const onlineFriends = Math.round(Math.random() * 10); - online_friends.set(onlineFriends); + const currentOnlineFriends = Math.round(Math.random() * 10); + onlineFriends.set(currentOnlineFriends); }; -const updateBeatmapSets = async () => { - await new Promise((res) => setTimeout(res, Math.random() * 1500)); - const beatmapSets = Math.round(Math.random() * 5000); - beatmap_sets.set(beatmapSets); -}; +const updateBeatmapSets = async () => {}; diff --git a/src/lib/types.ts b/src/lib/types.ts index b2ff5a6..9b58f84 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,6 +1,97 @@ +export type EZPPUserResponse = { + code: number; + message: string; + user?: EZPPUser; +}; + export type EZPPUser = { id: number; donor: boolean; name: string; email: string; }; + +export type EZPPUserInfoResponse = { + status: string; + player: EZPPUserInfo; +}; + +export type EZPPUserInfo = { + info: { + id: number; + name: string; + safe_name: string; + priv: number; + country: string; + silence_end: number; + donor_end: number; + creation_time: number; + latest_activity: number; + clan_id: number; + clan_priv: number; + preferred_mode: number; + preferred_type: number; + play_style: number; + custom_badge_enabled: number; + custom_badge_name: string; + custom_badge_icon: string; + custom_badge_color: string; + userpage_content: string; + recentFailed: number; + social_discord: string; + social_youtube: string; + social_twitter: string; + social_twitch: string; + social_github: string; + social_osu: string; + clan: { + id: number; + name: string; + tag: string; + owner: number; + created_at: Date; + }; + username_history: string[]; + }; + stats: { + [key: string]: { + id: number; + mode: number; + tscore: number; + rscore: number; + pp: number; + plays: number; + playtime: number; + acc: number; + max_combo: number; + total_hits: number; + replay_views: number; + xh_count: number; + x_count: number; + sh_count: number; + s_count: number; + a_count: number; + level: number; + level_progress: number; + rank: number; + country_rank: number; + history: { + pp: number[]; + }; + }; + }; + events: { + userId: number; + name: string; + mapId: number; + setId: number; + artist: string; + title: string; + version: string; + mode: number; + rank: number; + grade: string; + event: 'GAINED' | 'LOST'; + time: Date; + }[]; +}; diff --git a/src/lib/userSettings.ts b/src/lib/userSettings.ts index f6c1fe8..7a77033 100644 --- a/src/lib/userSettings.ts +++ b/src/lib/userSettings.ts @@ -3,9 +3,12 @@ import { Config } from './config'; export const userSettings = writable(new Config('user_settings', false)); -export const customCursor = writable(true); -export const cursorSmoothening = writable(true); +export const customCursor = writable(false); +export const cursorSmoothening = writable(false); export const cursorSmoothness = writable(180); export const reduceAnimations = writable(false); export const osuInstallationPath = writable(''); + +export const preferredMode = writable(0); +export const preferredType = writable(0); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index b0c4bb8..045199e 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,11 +1,11 @@ -import { createAudioStore } from "@elron/svelte-audio-store"; -import { type ClassValue, clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; +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", + menuHeartbeat: '/audio/menuHeartbeat.mp3', + menuBack: '/audio/menuBack.wav', + menuHit: '/audio/menuHit.wav', }; export const gameSounds = createAudioStore(sounds); @@ -23,3 +23,30 @@ export const playAudio = (path: string, volume: number) => { audio.volume = volume; audio.play(); }; + +export const isNumber = (value: unknown) => { + if (typeof value === 'number' || typeof value === 'string') { + return value.toString().match(/^-?\d+(\.\d+)?$/) !== null; + } + return false; +}; + +export const formatTimeReadable = (initialSeconds: number) => { + let seconds = initialSeconds; + + const days = Math.floor(seconds / (24 * 3600)); + seconds -= days * 24 * 3600; + + const hours = Math.floor(seconds / 3600); + seconds -= hours * 3600; + + const minutes = Math.floor(seconds / 60); + + let result = ''; + + if (days > 0) result += `${days}d `; + if (hours > 0) result += `${hours}h `; + result += `${minutes}m`; + + return result.trim(); +}; diff --git a/src/pages/Launch.svelte b/src/pages/Launch.svelte index e2c85f3..a9f33bf 100644 --- a/src/pages/Launch.svelte +++ b/src/pages/Launch.svelte @@ -4,7 +4,7 @@ import Badge from '@/components/ui/badge/badge.svelte'; import Button from '@/components/ui/button/button.svelte'; import * as Select from '@/components/ui/select'; - import { beatmap_sets, current_view, server_connection_fails, server_ping } from '@/global'; + import { beatmapSets, currentView, serverConnectionFails, serverPing } from '@/global'; import { LoaderCircle, Logs, @@ -14,19 +14,26 @@ Gamepad2, WifiOff, Settings2, + Drum, + Cherry, + Piano, + Circle, + LogOut, + LogIn, } from 'lucide-svelte'; - import { Circle } from 'radix-icons-svelte'; import NumberFlow from '@number-flow/svelte'; import * as AlertDialog from '@/components/ui/alert-dialog'; import Progress from '@/components/ui/progress/progress.svelte'; - import { numberHumanReadable } from '@/utils'; - import { scale } from 'svelte/transition'; + import { formatTimeReadable, numberHumanReadable } from '@/utils'; + import { fade, scale } from 'svelte/transition'; import { Checkbox } from '@/components/ui/checkbox'; import Label from '@/components/ui/label/label.svelte'; import { cursorSmoothening, customCursor, osuInstallationPath, + preferredMode, + preferredType, reduceAnimations, userSettings, } from '@/userSettings'; @@ -35,11 +42,30 @@ import { invoke } from '@tauri-apps/api/core'; import { toast } from 'svelte-sonner'; import Login from './Login.svelte'; - import { currentUser } from '@/userAuthentication'; + import { currentUser, userAuth } from '@/userAuthentication'; + import { + getGamemodeInt, + getGamemodeName, + getModeAndTypeFromGamemode, + modeIntToStr, + typeIntToStr, + validModeTypeCombinationsSorted, + } from '@/gamemode'; + import { currentUserInfo } from '@/data'; let selectedTab = $state('home'); let launching = $state(false); + let selectedGamemode = $derived( + getGamemodeInt(modeIntToStr($preferredMode), typeIntToStr($preferredType)) + ); + let selectedMode = $derived(getModeAndTypeFromGamemode(selectedGamemode).mode); + let selectedType = $derived(getModeAndTypeFromGamemode(selectedGamemode).type); + + const updateGamemode = (newGamemode: string) => { + selectedGamemode = Number(newGamemode); + }; + const browse_osu_installation = async () => { const selectedPath = await open({ directory: true, @@ -48,9 +74,9 @@ }); if (typeof selectedPath === 'string') { - /* if (selectedPath === $osuInstallationPath) { + if (selectedPath === $osuInstallationPath) { return; - } */ + } const validFolder: boolean = await invoke('valid_osu_folder', { folder: selectedPath }); if (!validFolder) { toast.error( @@ -62,6 +88,13 @@ $userSettings.value('osu_installation_path').set(selectedPath); $userSettings.save(); toast.success('osu! installation path set successfully.'); + + const beatmapSetCount: number | null = await invoke('get_beatmapsets_count', { + folder: selectedPath, + }); + if (beatmapSetCount) { + beatmapSets.set(beatmapSetCount); + } } }; @@ -94,53 +127,184 @@
{#if !$currentUser} - + + {:else} + {/if}
-
- - +
+ + +
+ {#if selectedMode === 0} + + {:else if selectedMode === 1} + + {:else if selectedMode === 2} + + {:else if selectedMode === 3} + + {/if} + {getGamemodeName(modeIntToStr(selectedMode), typeIntToStr(selectedType))} +
+
+ + {#each validModeTypeCombinationsSorted as gamemode} + {@const gamemod = getModeAndTypeFromGamemode(gamemode)} + +
+ {#if gamemod.mode === 0} + + {:else if gamemod.mode === 1} + + {:else if gamemod.mode === 2} + + {:else if gamemod.mode === 3} + + {/if} + {getGamemodeName(modeIntToStr(gamemod.mode), typeIntToStr(gamemod.type))} +
+
+ {/each} +
+
+
+
- - osu!vn + + Mode Stats
- - - osu!vn - osu!rx - osu!ap - - -
-
- - Mode Stats -
-
-
- Rank - #727 +
+
+ Rank +
+ {#if $currentUserInfo} +
+ +
+ {:else} +
+ +
+ {/if} +
+
+
+ PP +
+ {#if $currentUserInfo} +
+ +
+ {:else} +
+ +
+ {/if} +
+
-
- PP - 727 -
-
-
- Accuracy - 72.72% +
+ Accuracy +
+ {#if $currentUserInfo} +
+ +
+ {:else} +
+ +
+ {/if} +
- Play Count - 727 + Play Count +
+ {#if $currentUserInfo} +
+ +
+ {:else} +
+ +
+ {/if} +
- Play Time - 727h + Play Time +
+ {#if $currentUserInfo} +
+ {formatTimeReadable($currentUserInfo.stats[selectedGamemode].playtime ?? 0)} +
+ {:else} +
+ +
+ {/if} +
+
-
- -
+
+ {/if}
{#if $reduceAnimations} - {numberHumanReadable($beatmap_sets ?? 0)} + {numberHumanReadable($beatmapSets ?? 0)} {:else} - + {/if}
@@ -249,38 +414,38 @@ class="bg-theme-800/90 border border-theme-700/90 rounded-lg px-2 py-4 w-full flex flex-col gap-1 items-center justify-center" >
1 ? 'bg-red-500/20' : 'bg-green-500/20'}" > - {#if $server_connection_fails > 1} + {#if $serverConnectionFails > 1} {:else} {/if}
1 ? 'text-red-400' : 'text-green-400'}" >
1 ? 'opacity-100' : 'opacity-0'} transition-opacity duration-1000" >
1 ? 'opacity-0' : 'opacity-100'} transition-opacity duration-1000" > {#if $reduceAnimations} - {$server_ping}ms + {$serverPing}ms {:else} - + {/if}
@@ -289,6 +454,7 @@
Beatmap Sets {numberHumanReadable($beatmap_sets ?? 0)}{numberHumanReadable($beatmapSets ?? 0)} Skins diff --git a/src/pages/Loading.svelte b/src/pages/Loading.svelte index 428f3f7..ec03a2e 100644 --- a/src/pages/Loading.svelte +++ b/src/pages/Loading.svelte @@ -1,8 +1,14 @@ diff --git a/src/pages/SetupWizard.svelte b/src/pages/SetupWizard.svelte index a046214..3243450 100644 --- a/src/pages/SetupWizard.svelte +++ b/src/pages/SetupWizard.svelte @@ -11,7 +11,7 @@ import Checkbox from '@/components/ui/checkbox/checkbox.svelte'; import { cursorSmoothening, customCursor, reduceAnimations, userSettings } from '@/userSettings'; import Label from '@/components/ui/label/label.svelte'; - import { current_view } from '@/global'; + import { beatmapSets, currentView } from '@/global'; import Launch from './Launch.svelte'; import Confetti from 'svelte-confetti'; @@ -65,6 +65,13 @@ autoDetectedOsuPath = false; manualSelectValid = true; $userSettings.value('osu_installation_path').set(osuInstallationPath); + + const beatmapSetCount: number | null = await invoke('get_beatmapsets_count', { + folder: osuInstallationPath, + }); + if (beatmapSetCount) { + beatmapSets.set(beatmapSetCount); + } } }; @@ -90,7 +97,7 @@ class="mt-4" onclick={async () => { await $userSettings.save(); - current_view.set(Launch); + currentView.set(Launch); }} > Finish diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 56b7e98..36cdb66 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,7 +1,7 @@ diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 441ced2..489084e 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,8 +1,8 @@ {#key View}