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.
This commit is contained in:
HorizonCode 2025-07-03 11:46:50 +02:00
parent 892f2cea07
commit 651592c333
13 changed files with 680 additions and 128 deletions

View File

@ -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<number | undefined> => {
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<EZPPUserResponse>(`${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<EZPPUserInfoResponse>(`${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;
},
};

4
src/lib/data.ts Normal file
View File

@ -0,0 +1,4 @@
import { writable } from 'svelte/store';
import type { EZPPUserInfo } from './types';
export const currentUserInfo = writable<EZPPUserInfo | undefined>(undefined);

186
src/lib/gamemode.ts Normal file
View File

@ -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);
};

View File

@ -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<Component>(Loading);
export const first_startup = writable<boolean>(false);
export const currentView = writable<Component>(Loading);
export const server_ping = writable<number | undefined>(undefined);
export const server_connection_fails = writable(0);
export const currentLoadingInfo = writable<string>('Initializing...');
export const online_friends = writable<number | undefined>(undefined);
export const firstStartup = writable<boolean>(false);
export const beatmap_sets = writable<number | undefined>(undefined);
export const serverPing = writable<number | undefined>(undefined);
export const serverConnectionFails = writable(0);
export const onlineFriends = writable<number | undefined>(undefined);
export const beatmapSets = writable<number | undefined>(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 () => {};

View File

@ -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;
}[];
};

View File

@ -3,9 +3,12 @@ import { Config } from './config';
export const userSettings = writable<Config>(new Config('user_settings', false));
export const customCursor = writable<boolean>(true);
export const cursorSmoothening = writable<boolean>(true);
export const customCursor = writable<boolean>(false);
export const cursorSmoothening = writable<boolean>(false);
export const cursorSmoothness = writable<number>(180);
export const reduceAnimations = writable<boolean>(false);
export const osuInstallationPath = writable<string>('');
export const preferredMode = writable<number>(0);
export const preferredType = writable<number>(0);

View File

@ -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();
};

View File

@ -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);
}
}
};
</script>
@ -94,27 +127,92 @@
<div class="flex flex-row gap-2">
<!-- <Badge variant="destructive">Owner</Badge> -->
{#if !$currentUser}
<Button variant="outline" size="sm" onclick={() => current_view.set(Login)}>Login</Button>
<Button variant="outline" size="sm" onclick={() => currentView.set(Login)}>
<LogIn size={16} />
Login
</Button>
{:else}
<Button
variant="outline"
size="sm"
onclick={async () => {
$userAuth.value('username').del();
$userAuth.value('password').del();
await $userAuth.save();
toast.success('Logout successful!', {
description: 'See you soon!',
});
currentUser.set(undefined);
currentUserInfo.set(undefined);
}}
>
<LogOut size={16} />
Logout
</Button>
{/if}
</div>
</div>
{#if $currentUser}
<div class="flex flex-col gap-6 h-full px-3">
<Select.Root type="single">
<div
in:scale={{ duration: $reduceAnimations ? 0 : 400, start: 0.98 }}
out:scale={{ duration: $reduceAnimations ? 0 : 400, start: 0.98 }}
>
<Select.Root
type="single"
value={selectedGamemode.toFixed()}
onValueChange={updateGamemode}
>
<Select.Trigger
class="border-theme-800/90 bg-theme-900/90 !text-muted-foreground font-semibold"
>
<div class="flex flex-row items-center gap-2">
<Circle class="text-muted-foreground" />
osu!vn
{#if selectedMode === 0}
<Circle size={16} class="text-theme-200" />
{:else if selectedMode === 1}
<Drum size={16} class="text-theme-200" />
{:else if selectedMode === 2}
<Cherry size={16} class="text-theme-200" />
{:else if selectedMode === 3}
<Piano size={16} class="text-theme-200" />
{/if}
{getGamemodeName(modeIntToStr(selectedMode), typeIntToStr(selectedType))}
</div>
</Select.Trigger>
<Select.Content class="bg-theme-950 border border-theme-900 rounded-lg">
<Select.Item value="light">osu!vn</Select.Item>
<Select.Item value="dark">osu!rx</Select.Item>
<Select.Item value="system">osu!ap</Select.Item>
{#each validModeTypeCombinationsSorted as gamemode}
{@const gamemod = getModeAndTypeFromGamemode(gamemode)}
<Select.Item value={gamemode.toFixed()}>
<div class="flex flex-row gap-2 items-center">
{#if gamemod.mode === 0}
<Circle size={16} class="text-theme-200" />
{:else if gamemod.mode === 1}
<Drum size={16} class="text-theme-200" />
{:else if gamemod.mode === 2}
<Cherry size={16} class="text-theme-200" />
{:else if gamemod.mode === 3}
<Piano size={16} class="text-theme-200" />
{/if}
{getGamemodeName(modeIntToStr(gamemod.mode), typeIntToStr(gamemod.type))}
</div>
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
<div class="bg-theme-900/90 border border-theme-800/90 rounded-lg p-2">
</div>
<div
class="bg-theme-900/90 border border-theme-800/90 rounded-lg p-2"
in:scale={{
duration: $reduceAnimations ? 0 : 400,
delay: $reduceAnimations ? 0 : 50,
start: 0.98,
}}
out:scale={{
duration: $reduceAnimations ? 0 : 400,
delay: $reduceAnimations ? 0 : 50,
start: 0.98,
}}
>
<div class="flex flex-row items-center gap-2">
<Logs class="text-muted-foreground" size="16" />
<span class="font-semibold text-muted-foreground text-sm">Mode Stats</span>
@ -122,22 +220,88 @@
<div class="grid grid-cols-2 mt-2 border-t border-theme-800 pt-2 pb-2">
<div class="flex flex-col gap-0.5">
<span class="text-sm text-muted-foreground font-semibold">Rank</span>
<span class="text-lg font-semibold text-theme-50">#727</span>
<div class="flex items-center h-full text-lg font-semibold text-theme-50">
{#if $currentUserInfo}
<div in:fade>
<NumberFlow
trend={0}
prefix="#"
value={$currentUserInfo.stats[selectedGamemode].rank ?? 0}
></NumberFlow>
</div>
{:else}
<div in:fade>
<LoaderCircle class="animate-spin" size={21} />
</div>
{/if}
</div>
</div>
<div class="flex flex-col gap-0.5">
<span class="text-sm text-muted-foreground font-semibold">PP</span>
<span class="text-lg font-semibold text-theme-50">727</span>
<div class="flex items-center h-full text-lg font-semibold text-theme-50">
{#if $currentUserInfo}
<div in:fade>
<NumberFlow trend={0} value={$currentUserInfo.stats[selectedGamemode].pp ?? 0}
></NumberFlow>
</div>
{:else}
<div in:fade>
<LoaderCircle class="animate-spin" size={21} />
</div>
{/if}
</div>
</div>
</div>
<div class="grid grid-cols-[1fr_auto] border-t border-theme-800 pt-2">
<span class="text-sm text-muted-foreground font-semibold">Accuracy</span>
<span class="text-sm font-semibold text-end text-theme-50">72.72%</span>
<div
class="flex items-center flex-row-reverse h-full text-sm text-end font-semibold text-theme-50"
>
{#if $currentUserInfo}
<div in:fade>
<NumberFlow
trend={0}
suffix="%"
value={$currentUserInfo.stats[selectedGamemode].acc.toFixed(2) ?? 0}
></NumberFlow>
</div>
{:else}
<div in:fade>
<LoaderCircle class="animate-spin" size={21} />
</div>
{/if}
</div>
<span class="text-sm text-muted-foreground font-semibold">Play Count</span>
<span class="text-sm font-semibold text-end text-theme-50">727</span>
<div
class="flex items-center flex-row-reverse h-full text-sm text-end font-semibold text-theme-50"
>
{#if $currentUserInfo}
<div in:fade>
<NumberFlow trend={0} value={$currentUserInfo.stats[selectedGamemode].plays ?? 0}
></NumberFlow>
</div>
{:else}
<div in:fade>
<LoaderCircle class="animate-spin" size={21} />
</div>
{/if}
</div>
<span class="text-sm text-muted-foreground font-semibold">Play Time</span>
<span class="text-sm font-semibold text-end text-theme-50">727h</span>
<div
class="flex items-center flex-row-reverse h-full text-sm text-end font-semibold text-theme-50"
>
{#if $currentUserInfo}
<div in:fade>
{formatTimeReadable($currentUserInfo.stats[selectedGamemode].playtime ?? 0)}
</div>
{:else}
<div in:fade>
<LoaderCircle class="animate-spin" size={21} />
</div>
{/if}
</div>
</div>
</div>
<!-- <div class="mt-auto bg-theme-900/90 border border-theme-800/90 rounded-lg p-2">
@ -150,6 +314,7 @@
</div>
</div> -->
</div>
{/if}
</div>
<div class="flex flex-col gap-6 w-full h-full bg-theme-900/40 p-6">
<div
@ -188,21 +353,21 @@
</div>
<div class="relative font-bold text-xl text-blue-400">
<div
class="absolute top-1 left-1/2 -translate-x-1/2 {!$beatmap_sets
class="absolute top-1 left-1/2 -translate-x-1/2 {!$beatmapSets
? 'opacity-100'
: 'opacity-0'} transition-opacity duration-1000"
>
<LoaderCircle class="animate-spin" />
</div>
<div
class="{!$beatmap_sets
class="{!$beatmapSets
? 'opacity-0'
: 'opacity-100'} transition-opacity duration-1000"
>
{#if $reduceAnimations}
<span>{numberHumanReadable($beatmap_sets ?? 0)}</span>
<span>{numberHumanReadable($beatmapSets ?? 0)}</span>
{:else}
<NumberFlow value={$beatmap_sets ?? 0} trend={0} />
<NumberFlow value={$beatmapSets ?? 0} trend={0} />
{/if}
</div>
</div>
@ -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"
>
<div
class="flex items-center justify-center p-2 rounded-lg {$server_connection_fails > 1
class="flex items-center justify-center p-2 rounded-lg {$serverConnectionFails > 1
? 'bg-red-500/20'
: 'bg-green-500/20'}"
>
{#if $server_connection_fails > 1}
{#if $serverConnectionFails > 1}
<WifiOff class="text-red-500" size="26" />
{:else}
<Wifi class="text-green-500" size="26" />
{/if}
</div>
<div
class="relative font-bold text-xl {$server_connection_fails > 1
class="relative font-bold text-xl {$serverConnectionFails > 1
? 'text-red-400'
: 'text-green-400'}"
>
<div
class="absolute top-1 left-1/2 -translate-x-1/2 {!$server_ping ||
$server_connection_fails > 1
class="absolute top-1 left-1/2 -translate-x-1/2 {!$serverPing ||
$serverConnectionFails > 1
? 'opacity-100'
: 'opacity-0'} transition-opacity duration-1000"
>
<LoaderCircle class="animate-spin" />
</div>
<div
class="{!$server_ping || $server_connection_fails > 1
class="{!$serverPing || $serverConnectionFails > 1
? 'opacity-0'
: 'opacity-100'} transition-opacity duration-1000"
>
{#if $reduceAnimations}
<span>{$server_ping}ms</span>
<span>{$serverPing}ms</span>
{:else}
<NumberFlow value={$server_ping ?? 0} trend={0} suffix="ms" />
<NumberFlow value={$serverPing ?? 0} trend={0} suffix="ms" />
{/if}
</div>
</div>
@ -289,6 +454,7 @@
</div>
<Button
size="lg"
disabled={launching || $osuInstallationPath === ''}
onclick={() => {
launching = true;
setTimeout(() => {
@ -297,7 +463,7 @@
}}
>
<Play />
Launch {$server_connection_fails > 1 ? 'offline' : ''}
Launch {$serverConnectionFails > 1 ? 'offline' : ''}
</Button>
</div>
<div
@ -320,7 +486,7 @@
<span class="text-sm text-muted-foreground font-semibold">Beatmap Sets</span>
<span class="text-sm font-semibold text-end text-theme-50"
>{numberHumanReadable($beatmap_sets ?? 0)}</span
>{numberHumanReadable($beatmapSets ?? 0)}</span
>
<span class="text-sm text-muted-foreground font-semibold">Skins</span>

View File

@ -1,8 +1,14 @@
<script lang="ts">
import Logo from '$assets/logo.png';
import { estimateRefreshRate } from '@/displayUtils';
import { current_view, first_startup } from '@/global';
import { cursorSmoothness } from '@/userSettings';
import { beatmapSets, currentLoadingInfo, currentView, firstStartup } from '@/global';
import {
cursorSmoothness,
osuInstallationPath,
preferredMode,
preferredType,
userSettings,
} from '@/userSettings';
import { animate, utils } from 'animejs';
import { onMount } from 'svelte';
import SetupWizard from './SetupWizard.svelte';
@ -10,6 +16,8 @@
import { currentUser, userAuth } from '@/userAuthentication';
import { ezppfarm } from '@/api/ezpp';
import { toast } from 'svelte-sonner';
import { currentUserInfo } from '@/data';
import { invoke } from '@tauri-apps/api/core';
let ezppLogo: HTMLImageElement;
let spinnerCircle: SVGCircleElement;
@ -54,7 +62,8 @@
const username = $userAuth.value('username').get('');
const password = $userAuth.value('password').get('');
if (username.length > 0 && password.length > 0) {
currentLoadingInfo.set('Logging in...');
try {
const loginResult = await ezppfarm.login(username, password);
if (loginResult && loginResult.user) {
@ -73,6 +82,40 @@
description: 'There was an issue connecting to the server. Please try again later.',
});
}
}
if ($currentUser) {
currentLoadingInfo.set('Loading user info...');
const userInfo = await ezppfarm.getUserInfo($currentUser.id);
if (userInfo) {
currentUserInfo.set(userInfo.player);
preferredMode.set(userInfo.player.info.preferred_mode);
preferredType.set(userInfo.player.info.preferred_type);
}
}
if (!$firstStartup) {
currentLoadingInfo.set('Checking osu installation path...');
const validFolder: boolean = await invoke('valid_osu_folder', {
folder: $osuInstallationPath,
});
if (!validFolder) {
osuInstallationPath.set('');
$userSettings.value('osu_installation_path').del();
await $userSettings.save();
toast.error('Oops...', {
description: 'Your previously set osu! installation path seems to be invalid.',
});
} else {
currentLoadingInfo.set('Counting beatmapsets...');
const beatmapSetCount: number | null = await invoke('get_beatmapsets_count', {
folder: $osuInstallationPath,
});
if (beatmapSetCount) {
beatmapSets.set(beatmapSetCount);
}
}
}
animate(ezppLogo, {
opacity: [1, 0],
@ -88,8 +131,8 @@
onComplete: () => {},
});
setTimeout(() => {
if ($first_startup) current_view.set(SetupWizard);
else current_view.set(Launch);
if ($firstStartup) currentView.set(SetupWizard);
else currentView.set(Launch);
}, 250);
};
@ -144,4 +187,5 @@
bind:this={ezppLogo}
/>
</div>
<span class="text-theme-200 font-semibold">{$currentLoadingInfo}</span>
</div>

View File

@ -4,12 +4,14 @@
import Button from '@/components/ui/button/button.svelte';
import Input from '@/components/ui/input/input.svelte';
import Label from '@/components/ui/label/label.svelte';
import { current_view } from '@/global';
import { currentView } from '@/global';
import { currentUser, userAuth } from '@/userAuthentication';
import { animate } from 'animejs';
import { LoaderCircle } from 'lucide-svelte';
import { toast } from 'svelte-sonner';
import Launch from './Launch.svelte';
import { currentUserInfo } from '@/data';
import { preferredMode, preferredType } from '@/userSettings';
let username = $state('');
let password = $state('');
@ -52,7 +54,7 @@
await $userAuth.save();
currentUser.set(loginResult.user);
current_view.set(Launch);
currentView.set(Launch);
} else {
toast.error('Login failed!', {
description: 'Please check your username and password.',
@ -65,6 +67,16 @@
});
isLoading = false;
}
if ($currentUser) {
const userInfo = await ezppfarm.getUserInfo($currentUser.id);
if (userInfo) {
currentUserInfo.set(userInfo.player);
preferredMode.set(userInfo.player.info.preferred_mode);
preferredType.set(userInfo.player.info.preferred_type);
}
}
};
</script>

View File

@ -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

View File

@ -1,7 +1,7 @@
<script lang="ts">
import '../app.css';
import Titlebar from '@/components/ui/titlebar/titlebar.svelte';
import { first_startup, setupValues } from '@/global';
import { currentLoadingInfo, firstStartup, setupValues } from '@/global';
import { onMount } from 'svelte';
import OsuCursor from '@/components/ui/osu-cursor/OsuCursor.svelte';
import {
@ -68,9 +68,10 @@
window.Buffer = Buffer;
disableReload();
setupValues();
const firstStartup = await $userSettings.init();
const isFirstStartup = await $userSettings.init();
$userAuth.init();
currentLoadingInfo.set('Loading config...');
const config_custom_cursor = $userSettings.value('custom_cursor');
const config_cursor_smoothening = $userSettings.value('cursor_smoothening');
const config_reduce_animations = $userSettings.value('reduce_animations');
@ -85,7 +86,7 @@
cursorSmoothening.subscribe((val) => config_cursor_smoothening.set(val));
reduceAnimations.subscribe((val) => config_reduce_animations.set(val));
first_startup.set(firstStartup);
firstStartup.set(isFirstStartup);
});
</script>

View File

@ -1,8 +1,8 @@
<script lang="ts">
import { current_view } from '@/global';
import { currentView } from '@/global';
import { fade } from 'svelte/transition';
const View = $derived($current_view);
const View = $derived($currentView);
</script>
{#key View}