chore(themes): add new themes api v2, add in-app theme editor/creator, macos compatibility, fix asset loading, rework settings and setup wizard
This commit is contained in:
1 parent
417f54a94a
commit
87202de656
97 files changed
+10968
-1422
No files matched your search
+622
-299
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { dev } from '$app/environment';
|
||||
import Logo from '$assets/logo.png';
|
||||
import ConfettiCannon from '$lib/components/confetti/ConfettiCannon.svelte';
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
import Checkbox from '$lib/components/ui/checkbox/checkbox.svelte';
|
||||
import Input from '$lib/components/ui/input/input.svelte';
|
||||
import Label from '$lib/components/ui/label/label.svelte';
|
||||
import { config } from '$lib/config';
|
||||
import {
|
||||
@@ -13,6 +13,7 @@
|
||||
discordPresence,
|
||||
osuBuild,
|
||||
osuStream,
|
||||
platform,
|
||||
presenceLoading,
|
||||
skinsCount,
|
||||
} from '$lib/global';
|
||||
@@ -23,7 +24,8 @@
|
||||
getSkin,
|
||||
getSkinsCount,
|
||||
getVersion,
|
||||
isValidOsuFolder,
|
||||
pickOsuInstallation,
|
||||
resolveOsuFolder,
|
||||
} from '$lib/osuUtil';
|
||||
import {
|
||||
cursorSmoothening,
|
||||
@@ -33,345 +35,666 @@
|
||||
reduceAnimations,
|
||||
} from '$lib/userSettings';
|
||||
import Launch from './Launch.svelte';
|
||||
import { Check, CircleCheckBig, CircleOff, LoaderCircle } from '@lucide/svelte';
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import { animate } from 'animejs';
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Check,
|
||||
CircleAlert,
|
||||
CircleCheckBig,
|
||||
FolderOpen,
|
||||
Info,
|
||||
LoaderCircle,
|
||||
Palette,
|
||||
RefreshCw,
|
||||
Rocket,
|
||||
Search,
|
||||
} from '@lucide/svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import Confetti from 'svelte-confetti';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
||||
let selectedStep = $state(1);
|
||||
const steps = ['Welcome', 'Locate your osu! Installation', 'Appearance Settings'];
|
||||
const steps = ['Welcome', 'osu! installation', 'Personalize'];
|
||||
|
||||
type DetectionStatus = 'detecting' | 'valid' | 'invalid' | 'missing';
|
||||
|
||||
let stepIndex = $state(0);
|
||||
let osuInstallPath = $state('');
|
||||
let manualSelect = $state(false);
|
||||
let manualSelectValid = $state(false);
|
||||
let autoDetectedOsuPath = $state(false);
|
||||
let wizardFinished = $state(false);
|
||||
let manualSelection = $state(false);
|
||||
let selectionError = $state(false);
|
||||
let detecting = $state(true);
|
||||
let detectionFinished = $state(false);
|
||||
let selecting = $state(false);
|
||||
let saving = $state(false);
|
||||
let saveError = $state<string | undefined>(undefined);
|
||||
let finished = $state(false);
|
||||
let detectionRun = 0;
|
||||
|
||||
let ezppLogo: HTMLImageElement | undefined = $state(undefined);
|
||||
const stepNumber = $derived(stepIndex + 1);
|
||||
|
||||
const logo_mouseenter = () => {
|
||||
if (ezppLogo) {
|
||||
animate(ezppLogo, {
|
||||
duration: 700,
|
||||
scale: 1.2,
|
||||
ease: (t: number) => Math.pow(2, -5 * t) * Math.sin((t - 0.075) * 20.94) + 1 - 0.0005 * t,
|
||||
});
|
||||
const status: DetectionStatus = $derived(
|
||||
detecting
|
||||
? 'detecting'
|
||||
: selectionError
|
||||
? 'invalid'
|
||||
: osuInstallPath
|
||||
? 'valid'
|
||||
: detectionFinished
|
||||
? 'missing'
|
||||
: 'detecting'
|
||||
);
|
||||
|
||||
const statusTitle = $derived(
|
||||
status === 'detecting'
|
||||
? 'Looking for osu!'
|
||||
: status === 'valid'
|
||||
? manualSelection
|
||||
? 'osu! installation selected'
|
||||
: 'osu! found'
|
||||
: status === 'invalid'
|
||||
? 'No osu! in that folder'
|
||||
: 'osu! not found yet'
|
||||
);
|
||||
|
||||
const statusDescription = $derived(
|
||||
status === 'detecting'
|
||||
? 'Checking your applications and Wine bottles…'
|
||||
: status === 'valid'
|
||||
? manualSelection
|
||||
? 'That folder contains a valid osu! installation.'
|
||||
: 'This installation was found automatically.'
|
||||
: status === 'invalid'
|
||||
? $platform === 'macos'
|
||||
? 'Pick the osu!.app wrapper, or the folder that contains osu!.exe.'
|
||||
: 'Pick the folder that contains osu!.exe.'
|
||||
: 'Browse to your installation to continue.'
|
||||
);
|
||||
|
||||
const statusBorderClass = $derived(
|
||||
status === 'valid'
|
||||
? 'border-emerald-500/25 bg-emerald-500/5'
|
||||
: status === 'invalid'
|
||||
? 'border-red-500/25 bg-red-500/5'
|
||||
: 'border-theme-800/70 bg-black/15'
|
||||
);
|
||||
|
||||
const statusIconClass = $derived(
|
||||
status === 'valid'
|
||||
? 'bg-emerald-500/10 text-emerald-400'
|
||||
: status === 'invalid'
|
||||
? 'bg-red-500/10 text-red-400'
|
||||
: status === 'missing'
|
||||
? 'bg-amber-500/10 text-amber-400'
|
||||
: 'bg-primary/10 text-primary'
|
||||
);
|
||||
|
||||
const canContinue = $derived(stepIndex !== 1 || osuInstallPath.length > 0 || dev);
|
||||
|
||||
const detectOsu = async () => {
|
||||
const run = ++detectionRun;
|
||||
detecting = true;
|
||||
selectionError = false;
|
||||
|
||||
try {
|
||||
const detectedPath = await autoDetectOsuInstallFolder();
|
||||
if (run !== detectionRun) return;
|
||||
if (detectedPath) {
|
||||
osuInstallPath = detectedPath;
|
||||
manualSelection = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not auto-detect the osu! installation.', error);
|
||||
} finally {
|
||||
if (run === detectionRun) {
|
||||
detecting = false;
|
||||
detectionFinished = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const logo_mouseleave = () => {
|
||||
if (ezppLogo) {
|
||||
animate(ezppLogo, {
|
||||
duration: 700,
|
||||
scale: 1,
|
||||
ease: (t: number) => (t - 1) ** 7 + 1,
|
||||
});
|
||||
}
|
||||
};
|
||||
const browseOsu = async () => {
|
||||
if (selecting) return;
|
||||
|
||||
const browse_osu_installation = async () => {
|
||||
const selectedPath = await open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
title: 'Select osu! Installation Folder',
|
||||
});
|
||||
detectionRun += 1;
|
||||
detecting = false;
|
||||
detectionFinished = true;
|
||||
selectionError = false;
|
||||
selecting = true;
|
||||
|
||||
if (typeof selectedPath === 'string') {
|
||||
const validFolder = await isValidOsuFolder(selectedPath);
|
||||
manualSelect = true;
|
||||
if (!validFolder) {
|
||||
manualSelectValid = false;
|
||||
try {
|
||||
const selectedPath = await pickOsuInstallation($platform);
|
||||
if (typeof selectedPath !== 'string') return;
|
||||
|
||||
const resolvedPath = await resolveOsuFolder(selectedPath);
|
||||
if (!resolvedPath) {
|
||||
osuInstallPath = '';
|
||||
manualSelection = false;
|
||||
selectionError = true;
|
||||
return;
|
||||
}
|
||||
osuInstallPath = selectedPath;
|
||||
autoDetectedOsuPath = false;
|
||||
manualSelectValid = true;
|
||||
|
||||
osuInstallPath = resolvedPath;
|
||||
manualSelection = true;
|
||||
} catch (error) {
|
||||
console.error('Could not select the osu! installation.', error);
|
||||
osuInstallPath = '';
|
||||
manualSelection = false;
|
||||
selectionError = true;
|
||||
} finally {
|
||||
selecting = false;
|
||||
}
|
||||
};
|
||||
|
||||
const nextStep = () => {
|
||||
if (stepIndex < steps.length - 1) {
|
||||
stepIndex += 1;
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
};
|
||||
|
||||
const previousStep = () => {
|
||||
if (finished) {
|
||||
finished = false;
|
||||
return;
|
||||
}
|
||||
stepIndex = Math.max(stepIndex - 1, 0);
|
||||
};
|
||||
|
||||
const saveConfig = async () => {
|
||||
await config.value<string>('osu_installation_path').set(osuInstallPath);
|
||||
osuInstallationPath.set(osuInstallPath);
|
||||
await config.value<boolean>('setup_complete').set(true);
|
||||
if (saving) return;
|
||||
saving = true;
|
||||
saveError = undefined;
|
||||
|
||||
const beatmapSetCount = await getBeatmapSetsCount(osuInstallPath);
|
||||
if (beatmapSetCount) {
|
||||
beatmapSets.set(beatmapSetCount);
|
||||
try {
|
||||
await config.value<string>('osu_installation_path').set(osuInstallPath);
|
||||
osuInstallationPath.set(osuInstallPath);
|
||||
await config.value<boolean>('setup_complete').set(true);
|
||||
|
||||
if (osuInstallPath.length > 0) {
|
||||
try {
|
||||
const [beatmapSetCount, skinCount, skin, stream, version] = await Promise.all([
|
||||
getBeatmapSetsCount(osuInstallPath),
|
||||
getSkinsCount(osuInstallPath),
|
||||
getSkin(osuInstallPath),
|
||||
getReleaseStream(osuInstallPath),
|
||||
getVersion(osuInstallPath),
|
||||
]);
|
||||
|
||||
beatmapSets.set(beatmapSetCount ?? 0);
|
||||
skinsCount.set(skinCount ?? 0);
|
||||
currentSkin.set(skin ?? '');
|
||||
osuStream.set(stream);
|
||||
osuBuild.set(version);
|
||||
} catch (error) {
|
||||
console.warn('Could not load the osu! installation details.', error);
|
||||
}
|
||||
}
|
||||
|
||||
currentView.set(Launch);
|
||||
} catch (error) {
|
||||
console.error('Could not finish the setup.', error);
|
||||
saveError = error instanceof Error ? error.message : String(error);
|
||||
saving = false;
|
||||
}
|
||||
|
||||
const skins_count = await getSkinsCount(osuInstallPath);
|
||||
if (skins_count) {
|
||||
/* skins.set(skins_count); */
|
||||
skinsCount.set(skins_count);
|
||||
} else {
|
||||
skinsCount.set(0);
|
||||
}
|
||||
|
||||
const skin: string = await getSkin(osuInstallPath);
|
||||
currentSkin.set(skin);
|
||||
|
||||
const osuReleaseStream = await getReleaseStream(osuInstallPath);
|
||||
osuStream.set(osuReleaseStream);
|
||||
const osuVersion = await getVersion(osuInstallPath);
|
||||
osuBuild.set(osuVersion);
|
||||
|
||||
currentView.set(Launch);
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
const osuPath = await autoDetectOsuInstallFolder();
|
||||
if (osuPath) {
|
||||
osuInstallPath = osuPath;
|
||||
autoDetectedOsuPath = true;
|
||||
await config.value<string>('osu_installation_path').set(osuInstallPath);
|
||||
}
|
||||
onMount(() => {
|
||||
void detectOsu();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if wizardFinished}
|
||||
<div class="relative flex flex-col items-center justify-center h-screen z-50">
|
||||
<div class="absolute h-fit w-fit top-1/2 left-1/2">
|
||||
<Confetti amount={200} y={[-1, 1.5]} x={[-2.3, 2.3]} colorArray={['#C6A0F3']} />
|
||||
{#if finished}
|
||||
<div
|
||||
class="relative flex h-screen w-full flex-col items-center justify-center overflow-hidden px-6"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-none absolute -top-32 left-1/2 h-72 w-[38rem] -translate-x-1/2 rounded-full bg-primary/15 blur-[110px]"
|
||||
></div>
|
||||
|
||||
{#if !$reduceAnimations}
|
||||
<ConfettiCannon count={820} colors={['#ff006e', '#a78bfa', '#7dd3fc', '#fbbf24']} />
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="relative z-10 w-full max-w-md overflow-hidden rounded-2xl border border-theme-800/80 bg-theme-950/75 shadow-2xl shadow-black/40 backdrop-blur-xl"
|
||||
>
|
||||
<div class="flex flex-col items-center px-7 py-8 text-center">
|
||||
<div
|
||||
class="rounded-3xl bg-primary/20 p-3 ring-1 ring-inset ring-primary/25 shadow-xl shadow-primary/10"
|
||||
>
|
||||
<img src={Logo} alt="EZPPLauncher logo" class="size-20 object-contain" />
|
||||
</div>
|
||||
<h1 class="mt-5 text-2xl font-bold tracking-tight">You're all set!</h1>
|
||||
<p class="mt-1.5 text-sm text-muted-foreground">
|
||||
Everything is configured. Time to farm some pp.
|
||||
</p>
|
||||
|
||||
{#if osuInstallPath}
|
||||
<div
|
||||
class="mt-5 w-full rounded-xl border border-theme-800/70 bg-black/20 px-3 py-2.5 text-left"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-1.5 text-[10px] font-semibold tracking-[0.14em] text-muted-foreground uppercase"
|
||||
>
|
||||
<FolderOpen class="size-3" />
|
||||
osu! installation
|
||||
</div>
|
||||
<p class="mt-1 truncate font-mono text-[11px] text-theme-200" title={osuInstallPath}>
|
||||
{osuInstallPath}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if saveError}
|
||||
<div
|
||||
class="mt-4 flex w-full items-start gap-2 rounded-lg border border-red-500/20 bg-red-500/5 p-2.5 text-left text-xs text-red-300"
|
||||
>
|
||||
<CircleAlert class="mt-0.5 size-3.5 shrink-0" />
|
||||
<span>{saveError}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-between gap-3 border-t border-theme-800/70 bg-black/20 px-5 py-3.5"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="text-muted-foreground hover:text-foreground"
|
||||
onclick={previousStep}
|
||||
disabled={saving}
|
||||
>
|
||||
<ArrowLeft />
|
||||
Back
|
||||
</Button>
|
||||
<Button onclick={saveConfig} disabled={saving || (osuInstallPath.length === 0 && !dev)}>
|
||||
{#if saving}
|
||||
<LoaderCircle class="animate-spin" />
|
||||
Saving…
|
||||
{:else}
|
||||
<Rocket />
|
||||
Start farming
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-3xl font-semibold">EZPPLauncher Setup completed!</h1>
|
||||
<p class="text-muted-foreground mt-2">You are now ready to farm some maps!</p>
|
||||
<Button class="mt-4" onclick={saveConfig}>Finish</Button>
|
||||
|
||||
<p class="relative z-10 mt-4 text-[11px] text-muted-foreground">See you on the leaderboards.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid grid-cols-[0.41fr_1fr] h-screen z-50">
|
||||
<div class="relative h-screen w-full overflow-x-hidden overflow-y-auto">
|
||||
<div
|
||||
class="w-full h-full border-r border-black/40 bg-black/40 backdrop-blur-md flex flex-col gap-6 p-3 z-50"
|
||||
class="pointer-events-none absolute -top-32 left-1/2 h-72 w-[38rem] -translate-x-1/2 rounded-full bg-primary/15 blur-[110px]"
|
||||
></div>
|
||||
|
||||
<div
|
||||
class="relative z-10 mx-auto flex min-h-full w-full max-w-2xl flex-col justify-center px-6 pt-10 pb-3"
|
||||
>
|
||||
{#each steps as step, i (step)}
|
||||
<div
|
||||
class="flex flex-row items-center gap-2 border {selectedStep === i + 1
|
||||
? 'border-primary-600/30 bg-primary-700/30'
|
||||
: selectedStep > i
|
||||
? 'border-green-800/30 bg-green-900/30'
|
||||
: 'border-black/30 bg-black/30'} rounded-lg p-2 transition-all"
|
||||
>
|
||||
<header class="mb-3 flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="flex flex-col items-center justify-center h-8 w-8 border-2 {selectedStep > i + 1
|
||||
? 'border-green-600'
|
||||
: 'border-theme-600'} rounded-full"
|
||||
class="rounded-2xl bg-primary/20 p-1 ring-1 ring-inset ring-primary/25 shadow-lg shadow-primary/10"
|
||||
>
|
||||
{#if selectedStep > i + 1}
|
||||
<Check class="mt-0.5 text-green-400" />
|
||||
{:else}
|
||||
<span class="text-lg font-semibold text-theme-100">{i + 1}</span>
|
||||
{/if}
|
||||
<img src={Logo} alt="EZPPLauncher logo" class="size-10 object-contain" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[10px] font-semibold tracking-[0.18em] text-muted-foreground uppercase">
|
||||
EZPPLauncher setup
|
||||
</p>
|
||||
<p class="text-sm font-semibold tracking-tight">Welcome aboard</p>
|
||||
</div>
|
||||
<span
|
||||
class="{selectedStep === i + 1
|
||||
? 'text-white'
|
||||
: selectedStep > i
|
||||
? 'text-green-500'
|
||||
: "'text-muted-foreground'"} transition-all text-sm font-bold">{step}</span
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex flex-col gap-6 w-full h-full backdrop-blur-md p-6 z-50">
|
||||
{#if selectedStep === 1}
|
||||
<div
|
||||
class="my-auto h-full w-full bg-black/40 rounded-lg border border-black/40 p-6 mt-5 flex flex-col items-center justify-center"
|
||||
in:fade={{ duration: $reduceAnimations ? 0 : 200 }}
|
||||
<span
|
||||
class="rounded-full border border-theme-800/80 bg-theme-950/60 px-2.5 py-1 text-[10px] font-medium text-muted-foreground"
|
||||
>
|
||||
<img
|
||||
src={Logo}
|
||||
alt="EZPPLauncher Logo"
|
||||
class="w-52 h-52 mb-2"
|
||||
bind:this={ezppLogo}
|
||||
onmouseenter={logo_mouseenter}
|
||||
onmouseleave={logo_mouseleave}
|
||||
/>
|
||||
<h1 class="text-3xl font-semibold">Welcome to EZPPLauncher!</h1>
|
||||
<p class="text-muted-foreground mt-2">
|
||||
This setup wizard will guide you through the initial setup of EZPPLauncher.
|
||||
</p>
|
||||
<div class="bg-red-800 border border-red-900 text-red-200 p-4 rounded-lg mt-4">
|
||||
Please make sure you have osu! installed on your system before proceeding.
|
||||
</div>
|
||||
</div>
|
||||
{:else if selectedStep === 2}
|
||||
<div
|
||||
class="my-auto h-full w-full bg-black/40 rounded-lg border border-black/40 p-6 mt-5 flex flex-col items-center justify-center"
|
||||
in:fade={{ duration: $reduceAnimations ? 0 : 200 }}
|
||||
>
|
||||
<h1 class="text-3xl font-semibold">Locate your osu! Installation</h1>
|
||||
<p class="text-muted-foreground mt-2">
|
||||
Please select the folder where your osu! installation is located.
|
||||
</p>
|
||||
<div class="flex flex-row w-full">
|
||||
<Input
|
||||
class="mt-4 w-full bg-theme-950 border-theme-800 border-r-0 rounded-r-none"
|
||||
type="text"
|
||||
placeholder="Path to osu! installation"
|
||||
value={osuInstallPath}
|
||||
/>
|
||||
<Button
|
||||
class="mt-4 bg-theme-950 border-theme-800 rounded-l-none"
|
||||
variant="outline"
|
||||
onclick={browse_osu_installation}>Browse</Button
|
||||
>
|
||||
</div>
|
||||
{#if !manualSelect}
|
||||
{#if autoDetectedOsuPath}
|
||||
<div
|
||||
class="flex flex-row gap-3 bg-green-800 border border-green-900 text-green-500 p-4 rounded-lg mt-4"
|
||||
>
|
||||
<CircleCheckBig />
|
||||
<span>Auto-detected osu! installation path! Please check if its correct!</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="flex flex-row gap-3 bg-red-800 border border-red-900 text-red-500 p-4 rounded-lg mt-4"
|
||||
>
|
||||
<CircleOff />
|
||||
<span>Could not auto-detect osu! installation path. Please select it manually.</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if manualSelectValid}
|
||||
<div
|
||||
class="flex flex-row gap-3 bg-green-800 border border-green-900 text-green-500 p-4 rounded-lg mt-4"
|
||||
>
|
||||
<CircleCheckBig />
|
||||
<span>Selected osu! installation path is valid!</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="flex flex-row gap-3 bg-red-800 border border-red-900 text-red-500 p-4 rounded-lg mt-4"
|
||||
>
|
||||
<CircleOff />
|
||||
<span
|
||||
>Selected osu! installation path is invalid! Please select a valid osu!
|
||||
installation.</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if selectedStep === 3}
|
||||
<div class="h-full w-full flex items-center justify-center">
|
||||
<div
|
||||
class="bg-black/40 backdrop-blur-sm py-8 px-8 rounded-lg"
|
||||
in:fade={{ duration: $reduceAnimations ? 0 : 200 }}
|
||||
Step {stepNumber} of {steps.length}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<ol class="mb-3 grid grid-cols-3 gap-1.5" aria-label="Setup progress">
|
||||
{#each steps as step, index (step)}
|
||||
<li
|
||||
class="flex items-center gap-2 rounded-lg border px-2.5 py-1.5 transition-colors {index ===
|
||||
stepIndex
|
||||
? 'border-primary/30 bg-primary/10'
|
||||
: index < stepIndex
|
||||
? 'border-theme-800/70 bg-theme-900/50'
|
||||
: 'border-theme-800/40 bg-black/10'}"
|
||||
aria-current={index === stepIndex ? 'step' : undefined}
|
||||
>
|
||||
<div class="grid grid-cols-[1fr_auto] gap-y-5 items-center px-6">
|
||||
<div class="flex flex-col">
|
||||
<Label class="text-sm" for="setting-patch">Patching</Label>
|
||||
<div class="text-muted-foreground text-xs">
|
||||
Shows misses in Relax and Autopilot <!-- {#if $platform !== 'windows'}<span
|
||||
class="text-red-500 bg-red-800/20 border border-red-600/20 p-0.5 mx-1 px-2 rounded-lg text-[0.55rem]!"
|
||||
>currently only on windows!</span
|
||||
>
|
||||
{/if} -->
|
||||
</div>
|
||||
</div>
|
||||
<Checkbox
|
||||
id="setting-patch"
|
||||
checked={$patch}
|
||||
onCheckedChange={async (e) => {
|
||||
patch.set(e);
|
||||
}}
|
||||
class="flex items-center justify-center w-5 h-5"
|
||||
></Checkbox>
|
||||
<span
|
||||
class="flex size-5 shrink-0 items-center justify-center rounded-full text-[10px] font-bold {index <
|
||||
stepIndex
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: index === stepIndex
|
||||
? 'bg-primary/20 text-primary ring-1 ring-primary/30 ring-inset'
|
||||
: 'bg-theme-800 text-muted-foreground'}"
|
||||
>
|
||||
{#if index < stepIndex}
|
||||
<Check class="size-3" />
|
||||
{:else}
|
||||
{index + 1}
|
||||
{/if}
|
||||
</span>
|
||||
<span
|
||||
class="truncate text-[11px] font-semibold {index === stepIndex
|
||||
? 'text-foreground'
|
||||
: index < stepIndex
|
||||
? 'text-theme-200'
|
||||
: 'text-muted-foreground'}">{step}</span
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
|
||||
<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"
|
||||
checked={$customCursor}
|
||||
onCheckedChange={async (e) => {
|
||||
if (!e) {
|
||||
cursorSmoothening.set(false);
|
||||
}
|
||||
customCursor.set(e);
|
||||
}}
|
||||
class="flex items-center justify-center w-5 h-5"
|
||||
></Checkbox>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<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-cursor-smoothening"
|
||||
checked={$cursorSmoothening}
|
||||
onCheckedChange={async (e) => {
|
||||
if (!$customCursor) return;
|
||||
cursorSmoothening.set(e);
|
||||
}}
|
||||
disabled={!$customCursor}
|
||||
class="flex items-center justify-center w-5 h-5"
|
||||
></Checkbox>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<Label class="text-sm" for="setting-reduce-animations">Reduce Animations</Label>
|
||||
<div class="text-muted-foreground text-xs">
|
||||
Disables some animations in the Launcher to improve performance on low-end
|
||||
devices.
|
||||
</div>
|
||||
</div>
|
||||
<Checkbox
|
||||
id="setting-reduce-animations"
|
||||
checked={$reduceAnimations}
|
||||
onCheckedChange={async (e) => {
|
||||
reduceAnimations.set(e);
|
||||
}}
|
||||
class="flex items-center justify-center w-5 h-5"
|
||||
></Checkbox>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<Label class="text-sm" for="setting-rich-presence">Discord Rich Presence</Label>
|
||||
<div class="text-muted-foreground text-xs">
|
||||
Let other discord users show what you are doing right now 👀
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
{#if $presenceLoading}
|
||||
<div class="-left-8 absolute" transition:fade>
|
||||
<LoaderCircle class="animate-spin" />
|
||||
<div
|
||||
class="overflow-hidden rounded-2xl border border-theme-800/80 bg-theme-950/75 shadow-2xl shadow-black/40 backdrop-blur-xl"
|
||||
>
|
||||
<div class="p-5">
|
||||
{#key stepIndex}
|
||||
<div in:fly={{ y: 6, duration: $reduceAnimations ? 0 : 200, opacity: 0 }}>
|
||||
{#if stepIndex === 0}
|
||||
<div class="flex min-h-[332px] flex-col">
|
||||
<div>
|
||||
<p class="text-[10px] font-semibold tracking-[0.2em] text-primary uppercase">
|
||||
Welcome
|
||||
</p>
|
||||
<h2 class="mt-1.5 text-2xl font-bold tracking-tight">Let's get you farming.</h2>
|
||||
<p class="mt-1.5 max-w-lg text-sm leading-relaxed text-muted-foreground">
|
||||
EZPPLauncher keeps osu! updated, patched and looking the way you want. This
|
||||
takes less than a minute.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
<Checkbox
|
||||
id="setting-rich-presence"
|
||||
bind:checked={$discordPresence}
|
||||
disabled={$presenceLoading}
|
||||
class="flex items-center justify-center w-5 h-5"
|
||||
></Checkbox>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-1.5">
|
||||
<div
|
||||
class="flex items-start gap-3 rounded-xl border border-theme-800/60 bg-black/15 p-2.5"
|
||||
>
|
||||
<div
|
||||
class="rounded-lg bg-primary/10 p-2 text-primary ring-1 ring-primary/15 ring-inset"
|
||||
>
|
||||
<Rocket class="size-4" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-[13px] leading-tight font-semibold">One-click launch</p>
|
||||
<p class="mt-0.5 text-xs leading-relaxed text-muted-foreground">
|
||||
Checks for updates, applies the patch and starts osu!.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-start gap-3 rounded-xl border border-theme-800/60 bg-black/15 p-2.5"
|
||||
>
|
||||
<div
|
||||
class="rounded-lg bg-primary/10 p-2 text-primary ring-1 ring-primary/15 ring-inset"
|
||||
>
|
||||
<Palette class="size-4" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-[13px] leading-tight font-semibold">Make it yours</p>
|
||||
<p class="mt-0.5 text-xs leading-relaxed text-muted-foreground">
|
||||
Themes with images, videos and custom fonts.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-start gap-3 rounded-xl border border-theme-800/60 bg-black/15 p-2.5"
|
||||
>
|
||||
<div
|
||||
class="rounded-lg bg-primary/10 p-2 text-primary ring-1 ring-primary/15 ring-inset"
|
||||
>
|
||||
<RefreshCw class="size-4" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-[13px] leading-tight font-semibold">Always up to date</p>
|
||||
<p class="mt-0.5 text-xs leading-relaxed text-muted-foreground">
|
||||
Keeps launcher files in sync before every launch.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto flex items-start gap-2.5 pt-3">
|
||||
<Info class="mt-0.5 size-3.5 shrink-0 text-primary" />
|
||||
<p class="text-xs leading-relaxed text-muted-foreground">
|
||||
Make sure osu! is installed and signed in before continuing.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else if stepIndex === 1}
|
||||
<div class="flex min-h-[332px] flex-col">
|
||||
<div>
|
||||
<p class="text-[10px] font-semibold tracking-[0.2em] text-primary uppercase">
|
||||
osu! installation
|
||||
</p>
|
||||
<h2 class="mt-1.5 text-2xl font-bold tracking-tight">Where's osu!?</h2>
|
||||
<p class="mt-1.5 max-w-lg text-sm leading-relaxed text-muted-foreground">
|
||||
{#if $platform === 'macos'}
|
||||
Point us at your osu!.app wrapper, or the folder that contains osu!.exe.
|
||||
{:else}
|
||||
Point us at the folder that contains osu!.exe.
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 rounded-xl border p-4 transition-colors {statusBorderClass}">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="rounded-full p-2 {statusIconClass}">
|
||||
{#if status === 'detecting'}
|
||||
<LoaderCircle class="size-4 animate-spin" />
|
||||
{:else if status === 'valid'}
|
||||
<CircleCheckBig class="size-4" />
|
||||
{:else if status === 'invalid'}
|
||||
<CircleAlert class="size-4" />
|
||||
{:else}
|
||||
<Search class="size-4" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm leading-tight font-semibold">{statusTitle}</p>
|
||||
<p class="mt-0.5 text-xs leading-relaxed text-muted-foreground">
|
||||
{statusDescription}
|
||||
</p>
|
||||
{#if osuInstallPath}
|
||||
<div
|
||||
class="mt-2.5 flex items-center gap-2 rounded-lg border border-theme-800/80 bg-theme-950/70 px-2.5 py-1.5"
|
||||
>
|
||||
<FolderOpen class="size-3.5 shrink-0 text-muted-foreground" />
|
||||
<span
|
||||
class="truncate font-mono text-[11px] text-theme-200"
|
||||
title={osuInstallPath}>{osuInstallPath}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3.5 flex flex-wrap items-center gap-1.5">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="border-theme-700 bg-theme-900/80 hover:bg-theme-800"
|
||||
onclick={browseOsu}
|
||||
disabled={selecting}
|
||||
>
|
||||
<FolderOpen />
|
||||
Browse…
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="text-muted-foreground hover:text-foreground"
|
||||
onclick={detectOsu}
|
||||
disabled={detecting || selecting}
|
||||
>
|
||||
<RefreshCw class={detecting ? 'animate-spin' : ''} />
|
||||
Scan again
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $platform === 'macos'}
|
||||
<div
|
||||
class="mt-3 flex items-start gap-2.5 rounded-xl border border-primary/15 bg-primary/5 p-3"
|
||||
>
|
||||
<Info class="mt-0.5 size-3.5 shrink-0 text-primary" />
|
||||
<p class="text-xs leading-relaxed text-theme-200">
|
||||
Pick <code class="rounded bg-black/25 px-1 py-0.5 font-mono text-[11px]"
|
||||
>osu!.app</code
|
||||
> directly — you don't need to open its package contents. The launcher finds osu!
|
||||
inside the Wineskin wrapper.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex min-h-[332px] flex-col">
|
||||
<div>
|
||||
<p class="text-[10px] font-semibold tracking-[0.2em] text-primary uppercase">
|
||||
Personalize
|
||||
</p>
|
||||
<h2 class="mt-1.5 text-2xl font-bold tracking-tight">Make it yours.</h2>
|
||||
<p class="mt-1.5 max-w-lg text-sm leading-relaxed text-muted-foreground">
|
||||
Choose how the launcher should look and behave.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-3 divide-y divide-theme-800/50 rounded-xl border border-theme-800/60 bg-black/15"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-6 px-4 py-1">
|
||||
<div class="min-w-0">
|
||||
<Label class="text-[13px]" for="wizard-patch">Patching</Label>
|
||||
<p class="mt-0.5 text-xs leading-relaxed text-muted-foreground">
|
||||
Shows misses in Relax and Autopilot.{#if $platform === 'macos'}{' '}Requires
|
||||
.NET in your Wine prefix.{/if}
|
||||
</p>
|
||||
</div>
|
||||
<Checkbox
|
||||
id="wizard-patch"
|
||||
class="mt-0.5"
|
||||
checked={$patch}
|
||||
onCheckedChange={(checked) => patch.set(checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start justify-between gap-6 px-4 py-1">
|
||||
<div class="min-w-0">
|
||||
<Label class="text-[13px]" for="wizard-custom-cursor"
|
||||
>Lazer-style cursor</Label
|
||||
>
|
||||
<p class="mt-0.5 text-xs leading-relaxed text-muted-foreground">
|
||||
Use a custom cursor in the launcher, like in osu!lazer.
|
||||
</p>
|
||||
</div>
|
||||
<Checkbox
|
||||
id="wizard-custom-cursor"
|
||||
class="mt-0.5"
|
||||
checked={$customCursor}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!checked) cursorSmoothening.set(false);
|
||||
customCursor.set(checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start justify-between gap-6 px-4 py-1">
|
||||
<div class="min-w-0">
|
||||
<Label class="text-[13px]" for="wizard-cursor-smoothing"
|
||||
>Cursor smoothening</Label
|
||||
>
|
||||
<p class="mt-0.5 text-xs leading-relaxed text-muted-foreground">
|
||||
Makes the custom cursor movement smoother.
|
||||
</p>
|
||||
</div>
|
||||
<Checkbox
|
||||
id="wizard-cursor-smoothing"
|
||||
class="mt-0.5"
|
||||
checked={$cursorSmoothening}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!$customCursor) return;
|
||||
cursorSmoothening.set(checked);
|
||||
}}
|
||||
disabled={!$customCursor}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start justify-between gap-6 px-4 py-1">
|
||||
<div class="min-w-0">
|
||||
<Label class="text-[13px]" for="wizard-reduce-animations"
|
||||
>Reduce animations</Label
|
||||
>
|
||||
<p class="mt-0.5 text-xs leading-relaxed text-muted-foreground">
|
||||
Disables some launcher animations on low-end devices.
|
||||
</p>
|
||||
</div>
|
||||
<Checkbox
|
||||
id="wizard-reduce-animations"
|
||||
class="mt-0.5"
|
||||
checked={$reduceAnimations}
|
||||
onCheckedChange={(checked) => reduceAnimations.set(checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start justify-between gap-6 px-4 py-1">
|
||||
<div class="min-w-0">
|
||||
<Label class="text-[13px]" for="wizard-presence"
|
||||
>Discord Rich Presence</Label
|
||||
>
|
||||
<p class="mt-0.5 text-xs leading-relaxed text-muted-foreground">
|
||||
Let other Discord users see what you are doing right now 👀
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2.5">
|
||||
{#if $presenceLoading}
|
||||
<LoaderCircle class="size-3.5 animate-spin text-muted-foreground" />
|
||||
{/if}
|
||||
<Checkbox
|
||||
id="wizard-presence"
|
||||
class="mt-0.5"
|
||||
bind:checked={$discordPresence}
|
||||
disabled={$presenceLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-between gap-3 border-t border-theme-800/70 bg-black/20 px-5 py-3"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={stepIndex === 0
|
||||
? 'invisible text-muted-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground'}
|
||||
onclick={previousStep}
|
||||
disabled={stepIndex === 0}
|
||||
>
|
||||
<ArrowLeft />
|
||||
Back
|
||||
</Button>
|
||||
<div class="flex items-center gap-3">
|
||||
{#if stepIndex === 1 && osuInstallPath.length === 0 && !dev}
|
||||
<span class="hidden text-[11px] text-muted-foreground sm:inline">
|
||||
Select an installation to continue
|
||||
</span>
|
||||
{/if}
|
||||
<Button class="group" onclick={nextStep} disabled={!canContinue}>
|
||||
{stepIndex === 0 ? 'Get started' : 'Continue'}
|
||||
<ArrowRight class="transition-transform group-hover:translate-x-0.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-auto flex flex-row items-center justify-between">
|
||||
<Button
|
||||
class="bg-theme-950 hover:bg-theme-800"
|
||||
variant="outline"
|
||||
onclick={() => (selectedStep = Math.max(selectedStep - 1, 1))}
|
||||
disabled={selectedStep <= 1}>Previous</Button
|
||||
>
|
||||
<Button
|
||||
onclick={() => {
|
||||
if (selectedStep >= steps.length) wizardFinished = true;
|
||||
else selectedStep = Math.min(selectedStep + 1, steps.length);
|
||||
}}
|
||||
disabled={selectedStep > steps.length ||
|
||||
(selectedStep === 2 && !dev && osuInstallPath.length <= 0)}
|
||||
>{selectedStep >= steps.length ? 'Finish' : 'Next'}</Button
|
||||
>
|
||||
</div>
|
||||
|
||||
<p class="mt-3 text-center text-[11px] text-muted-foreground">
|
||||
You can change all of this later in Settings.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
Reference in new issue
Block a user