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:
parent
892f2cea07
commit
651592c333
@ -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
4
src/lib/data.ts
Normal 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
186
src/lib/gamemode.ts
Normal 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);
|
||||
};
|
@ -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 () => {};
|
||||
|
@ -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;
|
||||
}[];
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
};
|
||||
|
@ -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,53 +127,184 @@
|
||||
<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>
|
||||
<div class="flex flex-col gap-6 h-full px-3">
|
||||
<Select.Root type="single">
|
||||
<Select.Trigger
|
||||
class="border-theme-800/90 bg-theme-900/90 !text-muted-foreground font-semibold"
|
||||
{#if $currentUser}
|
||||
<div class="flex flex-col gap-6 h-full px-3">
|
||||
<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">
|
||||
{#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">
|
||||
{#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>
|
||||
<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">
|
||||
<Circle class="text-muted-foreground" />
|
||||
osu!vn
|
||||
<Logs class="text-muted-foreground" size="16" />
|
||||
<span class="font-semibold text-muted-foreground text-sm">Mode Stats</span>
|
||||
</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>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<div class="bg-theme-900/90 border border-theme-800/90 rounded-lg p-2">
|
||||
<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>
|
||||
</div>
|
||||
<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="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>
|
||||
<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>
|
||||
<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="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>
|
||||
</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="grid grid-cols-[1fr_auto] border-t border-theme-800 pt-2">
|
||||
<span class="text-sm text-muted-foreground font-semibold">Accuracy</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>
|
||||
<span class="text-sm text-muted-foreground font-semibold">Play Count</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>
|
||||
<span class="text-sm text-muted-foreground font-semibold">Play Time</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>
|
||||
<!-- <div class="mt-auto bg-theme-900/90 border border-theme-800/90 rounded-lg p-2">
|
||||
<!-- <div class="mt-auto bg-theme-900/90 border border-theme-800/90 rounded-lg p-2">
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<Users class="text-muted-foreground" size="16" />
|
||||
@ -149,7 +313,8 @@
|
||||
<Badge class="h-5 bg-green-500/20 hover:bg-green-500/20 text-green-500">3 online</Badge>
|
||||
</div>
|
||||
</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>
|
||||
|
@ -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,24 +62,59 @@
|
||||
|
||||
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) {
|
||||
toast.success('Login successful!', {
|
||||
description: `Welcome back, ${loginResult.user.name}!`,
|
||||
});
|
||||
|
||||
try {
|
||||
const loginResult = await ezppfarm.login(username, password);
|
||||
if (loginResult && loginResult.user) {
|
||||
toast.success('Login successful!', {
|
||||
description: `Welcome back, ${loginResult.user.name}!`,
|
||||
});
|
||||
|
||||
currentUser.set(loginResult.user);
|
||||
} else {
|
||||
toast.error('Login failed!', {
|
||||
description: 'Please check your username and password.',
|
||||
currentUser.set(loginResult.user);
|
||||
} else {
|
||||
toast.error('Login failed!', {
|
||||
description: 'Please check your username and password.',
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
toast.error('Server error occurred during login.', {
|
||||
description: 'There was an issue connecting to the server. Please try again later.',
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
toast.error('Server error occurred during login.', {
|
||||
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, {
|
||||
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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}
|
||||
|
Loading…
x
Reference in New Issue
Block a user