chore: add custom themes

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-05-02 00:38:56 +02:00
parent b2e7d22d28
commit 8fd56ae749
15 changed files with 510 additions and 164 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

@@ -74,12 +74,18 @@
<div class="wrapper" bind:this={wrapper}>
<!-- Scrollable content -->
<div class={`content ${className}`} bind:this={container} onscroll={updateScrollbar}>
<div
class={`content ${className}`}
bind:this={container}
onscroll={updateScrollbar}
id="scrollable-content"
>
{@render children?.()}
</div>
<!-- Custom scrollbar -->
<div class="scrollbar" style="top: {topOffset}px;">
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="thumb"
bind:this={thumb}
@@ -87,6 +93,7 @@
onpointermove={drag}
onpointerup={endDrag}
onpointercancel={endDrag}
aria-controls="scrollable-content"
style="height:{thumbHeight}px; transform:translateY({thumbTop}px);"
></div>
</div>
@@ -117,8 +124,8 @@
position: absolute;
right: 2px;
bottom: 0;
width: 12px;
background: var(--color-theme-950);
width: 8px;
background: transparent;
border-radius: 999px;
}
@@ -126,7 +133,7 @@
.thumb {
position: absolute;
width: 100%;
background: var(--color-theme-800);
background: var(--color-theme-700);
border-radius: 999px;
cursor: grab;
transition: background 0.15s;
+4 -4
View File
@@ -3,8 +3,7 @@ import { ezppfarm } from './api/ezpp';
import type { Component } from 'svelte';
import Loading from '../screens/Loading.svelte';
import type { Release } from './types';
import { THEMES } from './themes';
import type { Theme } from './themes';
export const currentView = writable<Component>(Loading);
export const platform = writable<string>('');
@@ -25,8 +24,9 @@ export const currentLoadingInfo = writable<string>('Initializing...');
export const firstStartup = writable<boolean>(false);
export const launching = writable<boolean>(false);
export const theme = writable<(typeof THEMES)[number]>(THEMES[0]);
export const theme_video = writable<HTMLVideoElement | undefined>(undefined);
export const custom_themes = writable<Theme[]>([]);
export const custom_theme_container = writable<HTMLElement | undefined>(undefined);
export const active_custom_theme = writable<Theme | undefined>(undefined);
export const serverPing = writable<number | undefined>(undefined);
export const serverConnectionFails = writable(0);
+257 -40
View File
@@ -1,43 +1,260 @@
import executionclap_video from '$assets/themes/execution_clap/video.webm';
import lostumbrella_video from '$assets/themes/lost_umbrella/video.webm';
import nyancat_image from '$assets/themes/nyan_cat/image.gif';
import nyancat_audio from '$assets/themes/nyan_cat/audio.mp3';
import * as path from '@tauri-apps/api/path';
import * as fs from '@tauri-apps/plugin-fs';
import { convertFileSrc } from '@tauri-apps/api/core';
import { active_custom_theme, custom_themes } from './global';
import { setGlobalVolume } from './utils';
import { betterFetch } from '@better-fetch/fetch';
import zip from 'jszip';
export const THEMES = [
{
name: 'default',
display_name: 'Default',
assets: {
background_video: undefined,
background_image: undefined,
audio: undefined,
export type Theme = {
name: string;
version: string;
author: string;
scriptUrl: string;
assets: string;
preview: string;
status: 'installed' | 'downloading' | 'extracting' | 'not-installed';
progress: number;
};
export const getDownloadableThemes = async () => {
const themes: Theme[] = [];
const downloadableThemes = await betterFetch<
{
name: string;
version: string;
author: string;
}[]
>('https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/themes.json', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
},
},
{
name: 'execution_clap',
display_name: 'Execution Clap',
assets: {
background_video: executionclap_video,
background_image: undefined,
audio: undefined,
});
if (downloadableThemes.data) {
for (const theme of downloadableThemes.data) {
themes.push({
name: theme.name,
version: theme.version,
author: theme.author,
scriptUrl: '',
assets: '',
preview: `https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/previews/${toSafeName(theme.name)}.png`,
status: 'not-installed',
progress: 0,
});
}
} else {
console.error('Failed to fetch downloadable themes');
console.error(downloadableThemes.error);
}
return themes;
};
export const getThemes = async (): Promise<Theme[]> => {
const themes: Theme[] = [];
themes.push({
author: 'EZPPFarm',
name: 'Default',
version: '1.0.0',
scriptUrl: '',
assets: '',
preview: '',
status: 'installed',
progress: 0,
});
const folderPath = await path.join(await path.homeDir(), '.ezpplauncher', 'themes');
if (await fs.exists(folderPath)) {
const themeFolders = await fs.readDir(folderPath);
for (const themeFolder of themeFolders) {
const themeFolderPath = await path.join(folderPath, themeFolder.name);
const themeConfigFilePath = await path.join(folderPath, themeFolder.name, 'theme.json');
if (await fs.exists(themeConfigFilePath)) {
const themeConfig = await fs.readTextFile(themeConfigFilePath);
const theme = JSON.parse(themeConfig) as {
name: string;
version: string;
apiVersion: string;
author: string;
entry: string;
style: string;
preview: string;
};
const scriptFile = await path.join(folderPath, themeFolder.name, theme.entry);
if (await fs.exists(scriptFile)) {
themes.push({
name: theme.name,
version: theme.version,
author: theme.author,
scriptUrl: convertFileSrc(await path.normalize(`${themeFolderPath}/${theme.entry}`)),
assets: convertFileSrc(await path.normalize(`${themeFolderPath}/assets`)),
preview: convertFileSrc(
await path.normalize(`${themeFolderPath}/assets/${theme.preview}`)
),
status: 'installed',
progress: 0,
});
}
}
}
}
return themes;
};
function combineChunks(chunks: Uint8Array[], totalLength: number): Uint8Array {
const result = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return result;
}
export const downloadTheme = async (theme: Theme): Promise<boolean> => {
if (theme.status !== 'not-installed') return false;
custom_themes.update((themes) => {
const matchedTheme = themes.find((t) => t.name === theme.name);
if (matchedTheme) {
matchedTheme.status = 'downloading';
}
return themes;
});
const downloadUrl = `https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/themes/${toSafeName(
theme.name
)}.ezpplauncher-theme`;
const downloadReq = await fetch(downloadUrl, {
method: 'GET',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
},
},
{
name: 'lost_umbrella',
display_name: 'Lost Umbrella',
assets: {
background_video: lostumbrella_video,
background_image: undefined,
audio: undefined,
},
},
{
name: 'nyan_cat',
display_name: 'Nyan Cat',
assets: {
background_video: undefined,
background_image: nyancat_image,
audio: nyancat_audio,
},
},
];
});
if (!downloadReq.ok || downloadReq.body === null) {
console.log(downloadUrl);
custom_themes.update((themes) => {
const matchedTheme = themes.find((t) => t.name === theme.name);
if (matchedTheme) {
matchedTheme.status = 'not-installed';
}
return themes;
});
return false;
}
const total = Number(downloadReq.headers.get('content-length')) || 0;
const reader = downloadReq.body.getReader();
let received = 0;
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
received += value.length;
const progress = received / total;
custom_themes.update((themes) => {
const matchedTheme = themes.find((t) => t.name === theme.name);
if (matchedTheme) {
matchedTheme.progress = progress;
}
return themes;
});
}
custom_themes.update((themes) => {
const matchedTheme = themes.find((t) => t.name === theme.name);
if (matchedTheme) {
matchedTheme.status = 'extracting';
}
return themes;
});
const baseThemeFolder = await path.join(await path.homeDir(), '.ezpplauncher', 'themes');
const themeFolder = await path.join(baseThemeFolder, toSafeName(theme.name));
if (!(await fs.exists(baseThemeFolder))) await fs.mkdir(baseThemeFolder, { recursive: true });
if (!(await fs.exists(themeFolder))) await fs.mkdir(themeFolder, { recursive: true });
const zipFile = await zip.loadAsync(combineChunks(chunks, total));
const totalFiles = Object.keys(zipFile.files).length;
let extractedFiles = 0;
for (const zipEntry of Object.keys(zipFile.files)) {
const file = zipFile.file(zipEntry);
if (file) {
try {
const fileData = await file.async('uint8array');
const filePath = await path.join(themeFolder, zipEntry);
const dirPath = await path.dirname(filePath);
if (!(await fs.exists(dirPath))) await fs.mkdir(dirPath, { recursive: true });
await new Promise((res) => setTimeout(res, 250));
await fs.writeFile(filePath, fileData);
extractedFiles++;
custom_themes.update((themes) => {
const matchedTheme = themes.find((t) => t.name === theme.name);
if (matchedTheme) {
matchedTheme.progress = extractedFiles / totalFiles;
}
return themes;
});
} catch (err) {
console.log(err);
}
}
}
const themeConfig = await fs.readTextFile(await path.join(themeFolder, 'theme.json'));
const themeConfigObj = JSON.parse(themeConfig) as {
name: string;
version: string;
apiVersion: string;
author: string;
entry: string;
style: string;
preview: string;
};
const themeScript = convertFileSrc(
await path.normalize(`${themeFolder}/${themeConfigObj.entry}`)
);
const themeAssets = convertFileSrc(await path.normalize(`${themeFolder}/assets`));
const themePreview = convertFileSrc(
await path.normalize(`${themeFolder}/assets/${themeConfigObj.preview}`)
);
custom_themes.update((themes) => {
const matchedTheme = themes.find((t) => t.name === theme.name);
if (matchedTheme) {
matchedTheme.scriptUrl = themeScript;
matchedTheme.assets = themeAssets;
matchedTheme.preview = themePreview;
matchedTheme.name = themeConfigObj.name;
matchedTheme.version = themeConfigObj.version;
matchedTheme.author = themeConfigObj.author;
matchedTheme.status = 'installed';
}
return themes;
});
return true;
};
export const loadTheme = async (theme: Theme, themeContainer: HTMLElement) => {
active_custom_theme.set(theme);
themeContainer.innerHTML = '';
if (theme.scriptUrl.length > 0) {
const themeScript = await import(theme.scriptUrl);
/* @vite-ignore */
themeScript.mountTheme(themeContainer, { assets: theme.assets });
}
setGlobalVolume(0.15);
};
export const toSafeName = (name: string) => {
return name
.toLowerCase()
.replace(/[^a-z0-9]/g, '_')
.replace(/-+/g, '_');
};
+45
View File
@@ -99,3 +99,48 @@ export const urlIsValidImage = async (url: string) => {
return false;
}
};
export function toURL(data: Buffer, type: string) {
return URL.createObjectURL(new Blob([new Uint8Array(data)], { type }));
}
export function setGlobalVolume(volume: number) {
const audioElements = document.querySelectorAll('audio');
audioElements.forEach((audio) => {
audio.volume = volume;
});
const videoElements = document.querySelectorAll('video');
videoElements.forEach((video) => {
video.volume = volume;
});
}
export function fadeGlobalVolume(volumeFrom: number, volumeTo: number, duration: number) {
const audioElements = document.querySelectorAll('audio');
const videoElements = document.querySelectorAll('video');
const startTime = performance.now();
function animate() {
const currentTime = performance.now();
const elapsedTime = currentTime - startTime;
const progress = Math.min(elapsedTime / duration, 1);
const interpolatedVolume = volumeFrom + (volumeTo - volumeFrom) * progress;
audioElements.forEach((audio) => {
audio.volume = interpolatedVolume;
});
videoElements.forEach((video) => {
video.volume = interpolatedVolume;
});
if (progress < 1) {
requestAnimationFrame(animate);
}
}
animate();
}
+42 -33
View File
@@ -5,15 +5,16 @@
import Titlebar from '@/components/ui/titlebar/titlebar.svelte';
import * as AlertDialog from '@/components/ui/alert-dialog';
import {
active_custom_theme,
currentLoadingInfo,
custom_theme_container,
custom_themes,
discordPresence,
firstStartup,
launcherVersion,
platform,
presenceLoading,
setupValues,
theme,
theme_video,
trackingEnabled,
} from '@/global';
import { onMount } from 'svelte';
@@ -36,8 +37,9 @@
import '@fontsource/sora';
import '@fontsource/space-mono';
import { getDownloadableThemes, getThemes, loadTheme } from '@/themes';
import { sileo } from 'sileo';
import AnimatedBg from '@/components/ui/animated-bg/AnimatedBg.svelte';
import NyanCatBg from '@/components/ui/animated-bg/NyanCatBg.svelte';
let { children } = $props();
@@ -103,6 +105,32 @@
firstStartup.set(isFirstStartup);
$userAuth.init();
try {
currentLoadingInfo.set('Loading themes...');
const downloadableThemes = await getDownloadableThemes();
const themes = await getThemes();
const combinedThemes = [...themes];
for (const theme of downloadableThemes) {
if (!combinedThemes.find((t) => t.name === theme.name)) combinedThemes.push(theme);
}
custom_themes.set(combinedThemes);
const activeTheme = $userSettings.value('theme').get('Default');
const theme = themes.find((theme) => theme.name === activeTheme);
if (!theme) {
$userSettings.value('theme').set('Default');
await $userSettings.save();
loadTheme(themes[0], $custom_theme_container!);
} else {
loadTheme(theme, $custom_theme_container!);
}
} catch {
sileo.error({
title: 'An error occured!',
description: 'Failed to load theme',
});
}
currentLoadingInfo.set('Loading config...');
const config_patching = $userSettings.value('patching');
const config_custom_cursor = $userSettings.value('custom_cursor');
@@ -181,38 +209,19 @@
</AlertDialog.Content>
</AlertDialog.Root>
<main data-theme={$theme.name ?? 'default'}>
<main>
<div class="opacity-30">
{#if $theme}
{#if $theme.assets.background_video}
{#key $theme.name}
<div transition:fade={{ duration: $reduceAnimations ? 0 : 500 }}>
<video
class="absolute z-0 top-0 left-0 w-full h-full object-cover object-center aspect-video"
autoplay
playsinline
loop
volume={0.15}
bind:this={$theme_video}
>
<source src={$theme.assets.background_video} />
</video>
</div>
{/key}
{:else if $theme.name === 'nyan_cat'}
<div transition:fade={{ duration: $reduceAnimations ? 0 : 500 }}>
<NyanCatBg />
</div>
{:else}
<div transition:fade={{ duration: $reduceAnimations ? 0 : 500 }}>
<AnimatedBg />
</div>
{/if}
{:else}
<div transition:fade={{ duration: $reduceAnimations ? 0 : 500 }}>
<div transition:fade={{ duration: $reduceAnimations ? 0 : 500 }}>
<div
class={$active_custom_theme && $active_custom_theme.name === 'Default'
? 'hidden'
: 'absolute z-0 top-0 left-0 w-full h-full object-cover object-center aspect-video'}
bind:this={$custom_theme_container}
></div>
{#if ($active_custom_theme && $active_custom_theme.name === 'Default') || !$active_custom_theme}
<AnimatedBg />
</div>
{/if}
{/if}
</div>
</div>
{@render children()}
</main>
+96 -73
View File
@@ -1,12 +1,16 @@
<script lang="ts">
import Logo from '$assets/logo.png';
import DefaultThemePreview from '$assets/default_preview.png';
import * as Avatar from '@/components/ui/avatar';
import Badge from '@/components/ui/badge/badge.svelte';
import Button from '@/components/ui/button/button.svelte';
import * as Select from '@/components/ui/select';
import {
active_custom_theme,
beatmapSets,
currentSkin,
custom_theme_container,
custom_themes,
discordPresence,
launcherStream,
launcherStreams,
@@ -21,8 +25,6 @@
serverPing,
skins,
skinsCount,
theme,
theme_video,
trackingEnabled,
} from '@/global';
import {
@@ -40,12 +42,14 @@
ArrowRight,
Settings,
House,
Paintbrush,
} from 'lucide-svelte';
import NumberFlow from '@number-flow/svelte';
import * as AlertDialog from '@/components/ui/alert-dialog';
import Progress from '@/components/ui/progress/progress.svelte';
import {
compareBuildNumbers,
fadeGlobalVolume,
formatBytes,
numberHumanReadable,
openURL,
@@ -111,7 +115,8 @@
import DownloadButton from '@/components/ui/download-button/DownloadButton.svelte';
import { animate } from 'animejs';
import { sileo } from 'sileo';
import { THEMES } from '@/themes';
import ScrollContainer from '@/components/ui/scroll-container/ScrollContainer.svelte';
import { downloadTheme, loadTheme } from '@/themes';
let selectedView = $state('home');
let progress = $state(-1);
@@ -437,23 +442,7 @@
await new Promise((res) => setTimeout(res, 1500));
launchInfo = 'Launching osu!...';
if ($theme_video) {
const video = $theme_video;
const fadeOutDuration = 2000; // 2 seconds
const fadeOutSteps = 20;
const fadeOutStepTime = fadeOutDuration / fadeOutSteps;
const volumeStep = video.volume / fadeOutSteps;
const fadeOutInterval = setInterval(() => {
if (video.volume > 0) {
video.volume = Math.max(0, video.volume - volumeStep);
} else {
clearInterval(fadeOutInterval);
video.pause();
video.currentTime = 0;
}
}, fadeOutStepTime);
}
fadeGlobalVolume(0.15, 0, 2000);
await replaceUIFiles(osuPath, false);
await new Promise((res) => setTimeout(res, 1000));
@@ -565,26 +554,7 @@
cleanup = true;
launchInfo = 'Cleaning up...';
if ($theme_video) {
const video = $theme_video;
video.play();
video.currentTime = 0;
video.volume = 0;
const fadeInVolume = 0.15;
const fadeInDuration = 2000; // 2 seconds
const fadeInSteps = 20;
const fadeInStepTime = fadeInDuration / fadeInSteps;
const volumeStep = (fadeInVolume - video.volume) / fadeInSteps;
const fadeInInterval = setInterval(() => {
if (video.volume < fadeInVolume) {
video.volume = Math.min(fadeInVolume, video.volume + volumeStep);
} else {
clearInterval(fadeInInterval);
}
}, fadeInStepTime);
}
fadeGlobalVolume(0, 0.15, 2000);
await getCurrentWindow().show();
if (presenceUpdater) {
@@ -944,6 +914,18 @@
>
<House class="text-theme-200 !size-5" />
</Button>
<Button
class="flex size-12 items-center gap-2 ring-1 ring-inset ring-white/15 {selectedView ===
'themes'
? 'bg-primary/50'
: 'bg-black/20 border-black/20'} hover:bg-primary/50 rounded-[0.85rem] p-3 mt-3"
disabled={$launching}
onclick={() => {
if (!$launching) selectedView = 'themes';
}}
>
<Paintbrush class="text-theme-200 !size-5" />
</Button>
<Button
class="flex size-12 items-center gap-2 ring-1 ring-inset ring-white/15 {selectedView ===
'settings'
@@ -1484,39 +1466,6 @@
</Select.Root>
</div>
{/if}
<div class="flex flex-col">
<Label class="text-sm" for="setting-custom-cursor">launcher theme</Label>
<div class="text-muted-foreground text-xs">select the theme of this interface</div>
</div>
<div class="flex flex-row w-full">
<Select.Root
type="single"
value={$theme.name ?? 'default'}
onValueChange={async (them) => {
const newTheme = THEMES.find((theme) => theme.name === them);
if (newTheme) theme.set(newTheme);
$userSettings.value('launcherTheme').set(them);
await $userSettings.save();
}}
>
<Select.Trigger
class="border-theme-800 bg-theme-950 !text-muted-foreground font-semibold"
>
<div class="flex flex-row items-center gap-2 font-normal text-foreground">
{$theme.display_name}
</div>
</Select.Trigger>
<Select.Content class="bg-theme-950 border border-theme-950 rounded-lg">
{#each THEMES as theme (theme.name)}
<Select.Item value={theme.name}>
<div class="flex flex-row gap-2 items-center">
{theme.display_name}
</div>
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
</div>
</div>
{:else if selectedView === 'login'}
@@ -1570,6 +1519,80 @@
</Button>
</form>
</div>
{:else if selectedView === 'themes'}
<div
class="h-[100vh] w-full flex flex-col items-center bg-black/20 backdrop-blur-sm"
in:fly={{
duration: $reduceAnimations ? 0 : 400,
delay: $reduceAnimations ? 0 : 400,
y: 5,
opacity: 0,
}}
out:fly={{ duration: $reduceAnimations ? 0 : 400, y: -5, opacity: 0 }}
>
<ScrollContainer class="pt-8" topOffset={45}>
<div class="grid w-full gap-1 grid-cols-3 p-3 pr-5">
{#each $custom_themes as theme (theme.name)}
<div
class="group overflow-hidden rounded-3xl border {$active_custom_theme &&
$active_custom_theme.name === theme.name
? 'border-primary'
: 'border-theme-800'} bg-theme-950 transition hover:border-white/20 h-[295px]"
>
<img
src={theme.preview || DefaultThemePreview}
alt="Preview of {theme.name}"
class="mb-2 h-40 w-full rounded-t-3xl object-cover object-center pointer-events-none select-none"
/>
<div class="flex flex-col gap-1 p-3">
<div class="mb-4 flex items-center justify-between gap-2">
<div class="flex flex-col items-start">
<span class="text-sm font-semibold">{theme.name}</span>
<span class="text-xs text-muted-foreground">by {theme.author}</span>
</div>
<span class="text-xs text-muted-foreground">v{theme.version}</span>
</div>
<Button
disabled={($active_custom_theme && $active_custom_theme.name === theme.name) ||
theme.status === 'downloading' ||
theme.status === 'extracting'}
onclick={() => {
if (theme.status === 'installed') {
if ($custom_theme_container) {
loadTheme(theme, $custom_theme_container);
$userSettings.value('theme').set(theme.name);
$userSettings.save();
} else {
sileo.error({
title: 'Uhhm..',
description: 'Failed to apply theme.',
});
}
} else {
downloadTheme(theme);
}
}}
>
{#if $active_custom_theme && $active_custom_theme.name === theme.name}
Theme in use
{:else if theme.status !== 'installed'}
{#if theme.status === 'downloading'}
Downloading Theme... ({Math.round(theme.progress * 100)}%)
{:else if theme.status === 'extracting'}
Extracting Theme... ({Math.round(theme.progress * 100)}%)
{:else}
Download Theme
{/if}
{:else}
Use Theme
{/if}
</Button>
</div>
</div>
{/each}
</div>
</ScrollContainer>
</div>
{/if}
</div>
</div>
+5 -7
View File
@@ -15,7 +15,6 @@
osuStream,
skins,
skinsCount,
theme,
} from '@/global';
import {
cursorSmoothness,
@@ -43,7 +42,6 @@
import { git } from '@/api/git';
import { sileo } from 'sileo';
import type { EZPPUser } from '@/types';
import { THEMES } from '@/themes';
let ezppLogo: HTMLImageElement;
let spinnerCircle: SVGCircleElement;
@@ -133,9 +131,9 @@
}
if (!$firstStartup) {
const launcherTheme = $userSettings.value('launcherTheme').get('default');
/* const launcherTheme = $userSettings.value('launcherTheme').get('default');
const them = THEMES.find((theme) => theme.name === launcherTheme);
if (them) theme.set(them);
if (them) theme.set(them); */
currentLoadingInfo.set('Checking osu installation path...');
const validFolder = await isValidOsuFolder($osuInstallationPath);
if (!validFolder) {
@@ -256,8 +254,8 @@
});
</script>
<div class="flex flex-col items-center justify-center h-[100vh] w-full">
<div class="relative w-80 h-80 flex items-center justify-center">
<div class="flex flex-col items-center justify-center h-[100vh] w-full z-10">
<div class="relative w-80 h-80 flex items-center justify-center z-10">
<svg
class="absolute top-0 left-0 w-full h-full animate-spin"
style="animation-duration: 5s;"
@@ -283,5 +281,5 @@
bind:this={ezppLogo}
/>
</div>
<span class="text-theme-200 text-sm mt-5">{$currentLoadingInfo}</span>
<span class="text-theme-200 text-sm mt-5 z-10">{$currentLoadingInfo}</span>
</div>