Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e0ef81bcb | |||
| 96680ff8b5 |
@@ -185,6 +185,75 @@ pub async fn get_beatmapsets_count(folder: String) -> Option<u64> {
|
|||||||
return Some(count);
|
return Some(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SkinInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub author: Option<String>,
|
||||||
|
pub modified: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_skins(folder: String) -> Option<Vec<SkinInfo>> {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::UNIX_EPOCH;
|
||||||
|
use tokio::fs;
|
||||||
|
|
||||||
|
let skins_folder = PathBuf::from(folder).join("Skins");
|
||||||
|
|
||||||
|
if !skins_folder.exists() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut entries = fs::read_dir(skins_folder).await.ok()?;
|
||||||
|
|
||||||
|
let mut skins = Vec::new();
|
||||||
|
|
||||||
|
while let Some(entry) = entries.next_entry().await.ok()? {
|
||||||
|
if !entry.file_type().await.ok()?.is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dir_path = entry.path();
|
||||||
|
let skin_ini = dir_path.join("skin.ini");
|
||||||
|
|
||||||
|
if !skin_ini.exists() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
|
||||||
|
let author = fs::read_to_string(&skin_ini)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|content| {
|
||||||
|
content.lines().find_map(|line| {
|
||||||
|
let (key, value) = line.split_once(':')?;
|
||||||
|
key.trim()
|
||||||
|
.eq_ignore_ascii_case("Author")
|
||||||
|
.then(|| value.trim().to_string())
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let modified = entry
|
||||||
|
.metadata()
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|m| m.modified().ok())
|
||||||
|
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
|
||||||
|
.map(|d| d.as_secs())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
skins.push(SkinInfo {
|
||||||
|
name,
|
||||||
|
author,
|
||||||
|
modified,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(skins)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_skins_count(folder: String) -> Option<u64> {
|
pub async fn get_skins_count(folder: String) -> Option<u64> {
|
||||||
let path = PathBuf::from(folder);
|
let path = PathBuf::from(folder);
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ use crate::commands::{
|
|||||||
check_for_corruption, download_ezpp_launcher_update, download_ezpp_launcher_update_files,
|
check_for_corruption, download_ezpp_launcher_update, download_ezpp_launcher_update_files,
|
||||||
encrypt_string, exit, find_osu_installation, get_beatmapsets_count,
|
encrypt_string, exit, find_osu_installation, get_beatmapsets_count,
|
||||||
get_ezpp_launcher_update_files, get_hwid, get_launcher_version, get_osu_release_stream,
|
get_ezpp_launcher_update_files, get_hwid, get_launcher_version, get_osu_release_stream,
|
||||||
get_osu_skin, get_osu_version, get_platform, get_skins_count, has_net8, has_osuwinello,
|
get_osu_skin, get_osu_version, get_platform, get_skins, get_skins_count, has_net8,
|
||||||
has_wmctrl, install_ezpp_launcher_update, is_osu_running, open_url_in_browser,
|
has_osuwinello, has_wmctrl, install_ezpp_launcher_update, is_osu_running, open_url_in_browser,
|
||||||
presence_connect, presence_disconnect, presence_is_connected, presence_update_status,
|
presence_connect, presence_disconnect, presence_is_connected, presence_update_status,
|
||||||
presence_update_user, replace_ui_files, run_osu, run_osu_updater, set_osu_config_values,
|
presence_update_user, replace_ui_files, run_osu, run_osu_updater, set_osu_config_values,
|
||||||
set_osu_user_config_values, valid_osu_folder,
|
set_osu_user_config_values, valid_osu_folder,
|
||||||
@@ -37,6 +37,7 @@ pub fn run() {
|
|||||||
find_osu_installation,
|
find_osu_installation,
|
||||||
valid_osu_folder,
|
valid_osu_folder,
|
||||||
get_beatmapsets_count,
|
get_beatmapsets_count,
|
||||||
|
get_skins,
|
||||||
get_skins_count,
|
get_skins_count,
|
||||||
get_osu_version,
|
get_osu_version,
|
||||||
get_osu_release_stream,
|
get_osu_release_stream,
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
// Props
|
||||||
|
let { children, class: className = '', topOffset = 0 } = $props();
|
||||||
|
|
||||||
|
let wrapper: HTMLDivElement;
|
||||||
|
let container: HTMLDivElement;
|
||||||
|
let thumb: HTMLDivElement;
|
||||||
|
|
||||||
|
// Thumb state
|
||||||
|
let thumbHeight = $state(20);
|
||||||
|
let thumbTop = $state(0);
|
||||||
|
|
||||||
|
// Drag state
|
||||||
|
let dragging = false;
|
||||||
|
let startY = 0;
|
||||||
|
let startScroll = 0;
|
||||||
|
|
||||||
|
/** Update thumb height & position */
|
||||||
|
function updateScrollbar() {
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = container;
|
||||||
|
const scrollable = scrollHeight - clientHeight;
|
||||||
|
|
||||||
|
if (scrollable <= 0) {
|
||||||
|
thumbHeight = 0;
|
||||||
|
thumbTop = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
thumbHeight = Math.max((clientHeight / scrollHeight) * clientHeight, 24);
|
||||||
|
|
||||||
|
const trackHeight = clientHeight - thumbHeight - topOffset;
|
||||||
|
thumbTop = (scrollTop / scrollable) * trackHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Start dragging the thumb */
|
||||||
|
function startDrag(e: PointerEvent) {
|
||||||
|
dragging = true;
|
||||||
|
startY = e.clientY;
|
||||||
|
startScroll = container.scrollTop;
|
||||||
|
thumb.setPointerCapture(e.pointerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Drag the thumb */
|
||||||
|
function drag(e: PointerEvent) {
|
||||||
|
if (!dragging) return;
|
||||||
|
|
||||||
|
const delta = e.clientY - startY;
|
||||||
|
const { scrollHeight, clientHeight } = container;
|
||||||
|
const scrollable = scrollHeight - clientHeight;
|
||||||
|
const trackHeight = clientHeight - thumbHeight - topOffset;
|
||||||
|
|
||||||
|
container.scrollTop = startScroll + (delta / trackHeight) * scrollable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** End dragging */
|
||||||
|
function endDrag(e: PointerEvent) {
|
||||||
|
dragging = false;
|
||||||
|
thumb.releasePointerCapture(e.pointerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reactive effect to update scrollbar
|
||||||
|
$effect(() => updateScrollbar());
|
||||||
|
|
||||||
|
// ResizeObserver for dynamic content
|
||||||
|
$effect(() => {
|
||||||
|
if (!container) return;
|
||||||
|
const observer = new ResizeObserver(updateScrollbar);
|
||||||
|
observer.observe(container);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrapper" bind:this={wrapper}>
|
||||||
|
<!-- Scrollable content -->
|
||||||
|
<div class={`content ${className}`} bind:this={container} onscroll={updateScrollbar}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom scrollbar -->
|
||||||
|
<div class="scrollbar" style="top: {topOffset}px;">
|
||||||
|
<div
|
||||||
|
class="thumb"
|
||||||
|
bind:this={thumb}
|
||||||
|
onpointerdown={startDrag}
|
||||||
|
onpointermove={drag}
|
||||||
|
onpointerup={endDrag}
|
||||||
|
onpointercancel={endDrag}
|
||||||
|
style="height:{thumbHeight}px; transform:translateY({thumbTop}px);"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollable content */
|
||||||
|
.content {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
.content::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome, Safari, Edge */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar track */
|
||||||
|
.scrollbar {
|
||||||
|
position: absolute;
|
||||||
|
right: 2px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 12px;
|
||||||
|
background: var(--color-theme-950);
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Thumb */
|
||||||
|
.thumb {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
background: var(--color-theme-800);
|
||||||
|
border-radius: 999px;
|
||||||
|
cursor: grab;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
background: var(--color-theme-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb:hover {
|
||||||
|
background: var(--color-theme-700);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+2
-1
@@ -32,7 +32,8 @@ export const serverConnectionFails = writable(0);
|
|||||||
export const onlineFriends = writable<number | undefined>(undefined);
|
export const onlineFriends = writable<number | undefined>(undefined);
|
||||||
|
|
||||||
export const beatmapSets = writable<number | undefined>(undefined);
|
export const beatmapSets = writable<number | undefined>(undefined);
|
||||||
export const skins = writable<number | undefined>(undefined);
|
export const skinsCount = writable<number | undefined>(undefined);
|
||||||
|
export const skins = writable<{ name: string; author: string | undefined; modified: number }[]>([]);
|
||||||
|
|
||||||
export const osuStream = writable<string | undefined>(undefined);
|
export const osuStream = writable<string | undefined>(undefined);
|
||||||
export const osuBuild = writable<string | undefined>(undefined);
|
export const osuBuild = writable<string | undefined>(undefined);
|
||||||
|
|||||||
+17
-7
@@ -5,6 +5,18 @@ import { betterFetch } from '@better-fetch/fetch';
|
|||||||
|
|
||||||
const updateUrl = 'https://ez-pp.farm/api/ezpplauncher';
|
const updateUrl = 'https://ez-pp.farm/api/ezpplauncher';
|
||||||
|
|
||||||
|
export function normalizeOsuName(name: string): string {
|
||||||
|
return (
|
||||||
|
name
|
||||||
|
.trim()
|
||||||
|
// Remove leading symbols/spaces
|
||||||
|
.replace(/^[^a-zA-Z0-9]+/, '')
|
||||||
|
// Normalize spacing
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.toLowerCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const getHWID = async () => {
|
export const getHWID = async () => {
|
||||||
const hwid = await invoke('get_hwid');
|
const hwid = await invoke('get_hwid');
|
||||||
return typeof hwid === 'string' ? hwid : undefined;
|
return typeof hwid === 'string' ? hwid : undefined;
|
||||||
@@ -55,13 +67,6 @@ export const getBeatmapSetsCount = async (folder: string) => {
|
|||||||
return typeof result === 'number' ? result : 0;
|
return typeof result === 'number' ? result : 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSkinsCount = async (folder: string) => {
|
|
||||||
const result = await invoke('get_skins_count', {
|
|
||||||
folder,
|
|
||||||
});
|
|
||||||
return typeof result === 'number' ? result : 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSkin = async (folder: string) => {
|
export const getSkin = async (folder: string) => {
|
||||||
const result = await invoke('get_osu_skin', {
|
const result = await invoke('get_osu_skin', {
|
||||||
folder,
|
folder,
|
||||||
@@ -151,3 +156,8 @@ export const downloadUpdate = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
export const installUpdate = async () => await invoke('install_ezpp_launcher_update');
|
export const installUpdate = async () => await invoke('install_ezpp_launcher_update');
|
||||||
|
export const getSkins = async (folder: string) =>
|
||||||
|
await invoke<{ name: string; author: string | undefined; modified: number }[] | undefined>(
|
||||||
|
'get_skins',
|
||||||
|
{ folder }
|
||||||
|
);
|
||||||
|
|||||||
+363
-248
@@ -21,6 +21,7 @@
|
|||||||
serverConnectionFails,
|
serverConnectionFails,
|
||||||
serverPing,
|
serverPing,
|
||||||
skins,
|
skins,
|
||||||
|
skinsCount,
|
||||||
trackingEnabled,
|
trackingEnabled,
|
||||||
} from '@/global';
|
} from '@/global';
|
||||||
import {
|
import {
|
||||||
@@ -38,6 +39,8 @@
|
|||||||
ArrowRight,
|
ArrowRight,
|
||||||
Settings,
|
Settings,
|
||||||
House,
|
House,
|
||||||
|
Paintbrush,
|
||||||
|
FileQuestionMark,
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import NumberFlow from '@number-flow/svelte';
|
import NumberFlow from '@number-flow/svelte';
|
||||||
import * as AlertDialog from '@/components/ui/alert-dialog';
|
import * as AlertDialog from '@/components/ui/alert-dialog';
|
||||||
@@ -50,7 +53,7 @@
|
|||||||
releaseStreamToReadable,
|
releaseStreamToReadable,
|
||||||
urlIsValidImage,
|
urlIsValidImage,
|
||||||
} from '@/utils';
|
} from '@/utils';
|
||||||
import { fade, fly, scale } from 'svelte/transition';
|
import { fade, fly } from 'svelte/transition';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import Label from '@/components/ui/label/label.svelte';
|
import Label from '@/components/ui/label/label.svelte';
|
||||||
import {
|
import {
|
||||||
@@ -85,7 +88,7 @@
|
|||||||
getEZPPLauncherUpdateFiles,
|
getEZPPLauncherUpdateFiles,
|
||||||
getReleaseStream,
|
getReleaseStream,
|
||||||
getSkin,
|
getSkin,
|
||||||
getSkinsCount,
|
getSkins,
|
||||||
getVersion,
|
getVersion,
|
||||||
hasNet8,
|
hasNet8,
|
||||||
hasOsuWinello,
|
hasOsuWinello,
|
||||||
@@ -108,8 +111,9 @@
|
|||||||
import * as DropdownMenu from '@/components/ui/dropdown-menu';
|
import * as DropdownMenu from '@/components/ui/dropdown-menu';
|
||||||
import DownloadButton from '@/components/ui/download-button/DownloadButton.svelte';
|
import DownloadButton from '@/components/ui/download-button/DownloadButton.svelte';
|
||||||
import { animate } from 'animejs';
|
import { animate } from 'animejs';
|
||||||
import AnimatedBg from '@/components/ui/animated-bg/AnimatedBg.svelte';
|
|
||||||
import { sileo } from 'sileo';
|
import { sileo } from 'sileo';
|
||||||
|
import Check from '@lucide/svelte/icons/check';
|
||||||
|
import ScrollContainer from '@/components/ui/scroll-container/ScrollContainer.svelte';
|
||||||
|
|
||||||
let selectedView = $state('home');
|
let selectedView = $state('home');
|
||||||
let progress = $state(-1);
|
let progress = $state(-1);
|
||||||
@@ -173,8 +177,11 @@
|
|||||||
const beatmapSetCount: number | null = await getBeatmapSetsCount(selectedPath);
|
const beatmapSetCount: number | null = await getBeatmapSetsCount(selectedPath);
|
||||||
if (beatmapSetCount) beatmapSets.set(beatmapSetCount);
|
if (beatmapSetCount) beatmapSets.set(beatmapSetCount);
|
||||||
|
|
||||||
const skinsCount: number | null = await getSkinsCount(selectedPath);
|
const skins_list = await getSkins(selectedPath);
|
||||||
if (skinsCount !== null) skins.set(skinsCount);
|
if (skins_list) {
|
||||||
|
skins.set(skins_list);
|
||||||
|
skinsCount.set(skins_list.length);
|
||||||
|
}
|
||||||
|
|
||||||
const skin: string = await getSkin(selectedPath);
|
const skin: string = await getSkin(selectedPath);
|
||||||
currentSkin.set(skin);
|
currentSkin.set(skin);
|
||||||
@@ -188,6 +195,8 @@
|
|||||||
|
|
||||||
const launch = async () => {
|
const launch = async () => {
|
||||||
if ($launching) return;
|
if ($launching) return;
|
||||||
|
launchInfo = 'Checking if osu! is already running...';
|
||||||
|
launching.set(true);
|
||||||
if ($trackingEnabled) umami.track('app_launch_osu');
|
if ($trackingEnabled) umami.track('app_launch_osu');
|
||||||
const osuRunning = await isOsuRunning();
|
const osuRunning = await isOsuRunning();
|
||||||
if (osuRunning) {
|
if (osuRunning) {
|
||||||
@@ -200,8 +209,10 @@
|
|||||||
description: 'text-center!',
|
description: 'text-center!',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
launching.set(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
launchInfo = 'Checking osu! installation...';
|
||||||
if (!$osuBuild) {
|
if (!$osuBuild) {
|
||||||
sileo.error({
|
sileo.error({
|
||||||
title: 'Hmmm...',
|
title: 'Hmmm...',
|
||||||
@@ -211,12 +222,12 @@
|
|||||||
description: 'text-center!',
|
description: 'text-center!',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
launching.set(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const osuPath = $osuInstallationPath;
|
const osuPath = $osuInstallationPath;
|
||||||
|
|
||||||
launchInfo = 'Validating osu! installation...';
|
launchInfo = 'Validating osu! installation...';
|
||||||
launching.set(true);
|
|
||||||
|
|
||||||
const validFolder = await isValidOsuFolder(osuPath);
|
const validFolder = await isValidOsuFolder(osuPath);
|
||||||
if (!validFolder) {
|
if (!validFolder) {
|
||||||
@@ -603,8 +614,11 @@
|
|||||||
const beatmapSetCount = await getBeatmapSetsCount(osuPath);
|
const beatmapSetCount = await getBeatmapSetsCount(osuPath);
|
||||||
if (beatmapSetCount) beatmapSets.set(beatmapSetCount);
|
if (beatmapSetCount) beatmapSets.set(beatmapSetCount);
|
||||||
|
|
||||||
const skinCount = await getSkinsCount(osuPath);
|
const skinsList = await getSkins(osuPath);
|
||||||
if (skinCount !== null) skins.set(skinCount);
|
if (skinsList) {
|
||||||
|
skins.set(skinsList);
|
||||||
|
skinsCount.set(skinsList.length);
|
||||||
|
}
|
||||||
|
|
||||||
const skin = await getSkin(osuPath);
|
const skin = await getSkin(osuPath);
|
||||||
currentSkin.set(skin);
|
currentSkin.set(skin);
|
||||||
@@ -914,14 +928,15 @@
|
|||||||
<div
|
<div
|
||||||
class="p-3 border-r border-r-theme-900 flex flex-col items-center gap-2 z-10 bg-black/40 backdrop-blur-sm"
|
class="p-3 border-r border-r-theme-900 flex flex-col items-center gap-2 z-10 bg-black/40 backdrop-blur-sm"
|
||||||
>
|
>
|
||||||
<div class="bg-primary-800/80 border border-primary-700/50 rounded-[1.1rem] p-1">
|
<div class="bg-primary-800/80 ring-1 ring-inset ring-white/15 rounded-[1.1rem] p-1.5">
|
||||||
<img class="pointer-events-none" src={Logo} alt="logo" bind:this={ezppLogo} />
|
<img class="pointer-events-none" src={Logo} alt="logo" bind:this={ezppLogo} />
|
||||||
</div>
|
</div>
|
||||||
<Badge class="text-[0.5rem] py-0 px-2">{$launcherVersion || 'dev'}</Badge>
|
<Badge class="text-[0.5rem] py-0 px-2">{$launcherVersion || 'dev'}</Badge>
|
||||||
<Button
|
<Button
|
||||||
class="flex size-12 items-center gap-2 border {selectedView === 'home'
|
class="flex size-12 items-center gap-2 ring-1 ring-inset ring-white/15 {selectedView ===
|
||||||
? 'bg-primary-300/50 border-primary-300/50'
|
'home'
|
||||||
: 'bg-black/20 border-black/20'} hover:bg-primary-300/50 hover:border-primary-300/50 rounded-[0.85rem] p-3 mt-3"
|
? 'bg-primary-300/50'
|
||||||
|
: 'bg-black/20 border-black/20'} hover:bg-primary-300/50 rounded-[0.85rem] p-3 mt-3"
|
||||||
disabled={$launching}
|
disabled={$launching}
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
if (!$launching) selectedView = 'home';
|
if (!$launching) selectedView = 'home';
|
||||||
@@ -930,9 +945,22 @@
|
|||||||
<House class="text-theme-200 !size-5" />
|
<House class="text-theme-200 !size-5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
class="flex size-12 items-center gap-2 border {selectedView === 'settings'
|
class="flex size-12 items-center gap-2 ring-1 ring-inset ring-white/15 {selectedView ===
|
||||||
? 'bg-primary-300/50 border-primary-300/50'
|
'skins'
|
||||||
: 'bg-black/20 border-black/20'} hover:bg-primary-300/50 hover:border-primary-300/50 rounded-[0.85rem] p-3 mt-3"
|
? 'bg-primary-300/50'
|
||||||
|
: 'bg-black/20 border-black/20'} hover:bg-primary-300/50 rounded-[0.85rem] p-3 mt-3"
|
||||||
|
disabled={$launching}
|
||||||
|
onclick={() => {
|
||||||
|
if (!$launching) selectedView = 'skins';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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'
|
||||||
|
? 'bg-primary-300/50'
|
||||||
|
: 'bg-black/20 border-black/20'} hover:bg-primary-300/50 rounded-[0.85rem] p-3 mt-3"
|
||||||
disabled={$launching}
|
disabled={$launching}
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
if (!$launching) selectedView = 'settings';
|
if (!$launching) selectedView = 'settings';
|
||||||
@@ -1012,10 +1040,10 @@
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Brush class="size-3.5 text-amber-400" />
|
<Brush class="size-3.5 text-amber-400" />
|
||||||
<span class="text-[11px] text-white/70 font-medium">
|
<span class="text-[11px] text-white/70 font-medium">
|
||||||
{#if !$skins && $skins !== 0}
|
{#if !$skinsCount && $skinsCount !== 0}
|
||||||
<LoaderCircle class="animate-spin" size={12} />
|
<LoaderCircle class="animate-spin" size={12} />
|
||||||
{:else}
|
{:else}
|
||||||
{numberHumanReadable($skins ?? 0)}
|
{numberHumanReadable($skinsCount ?? 0)}
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1114,7 +1142,7 @@
|
|||||||
|
|
||||||
{#if $currentUserInfo}
|
{#if $currentUserInfo}
|
||||||
<div
|
<div
|
||||||
class="m-1 w-72 h-48 bg-black/40 backdrop-blur-sm rounded-md border border-black/40 flex flex-col items-center p-3"
|
class="m-1 w-72 h-48 bg-black/40 backdrop-blur-sm rounded-md ring-1 ring-inset ring-white/5 flex flex-col items-center p-3"
|
||||||
in:fly={{ duration: $reduceAnimations ? 0 : 400, y: 5, opacity: 0 }}
|
in:fly={{ duration: $reduceAnimations ? 0 : 400, y: 5, opacity: 0 }}
|
||||||
out:fly={{ duration: $reduceAnimations ? 0 : 400, y: -5, opacity: 0 }}
|
out:fly={{ duration: $reduceAnimations ? 0 : 400, y: -5, opacity: 0 }}
|
||||||
>
|
>
|
||||||
@@ -1267,7 +1295,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else if selectedView === 'settings'}
|
{:else if selectedView === 'settings'}
|
||||||
<div
|
<div
|
||||||
class="h-[100vh] w-full flex flex-col items-center justify-center"
|
class="h-[100vh] w-full flex flex-col items-center justify-center bg-black/20 backdrop-blur-sm"
|
||||||
in:fly={{
|
in:fly={{
|
||||||
duration: $reduceAnimations ? 0 : 400,
|
duration: $reduceAnimations ? 0 : 400,
|
||||||
delay: $reduceAnimations ? 0 : 400,
|
delay: $reduceAnimations ? 0 : 400,
|
||||||
@@ -1276,205 +1304,198 @@
|
|||||||
}}
|
}}
|
||||||
out:fly={{ duration: $reduceAnimations ? 0 : 400, y: -5, opacity: 0 }}
|
out:fly={{ duration: $reduceAnimations ? 0 : 400, y: -5, opacity: 0 }}
|
||||||
>
|
>
|
||||||
<div class="p-8 w-full">
|
<div class="grid grid-cols-[1fr_auto] w-full gap-y-5 items-center px-6">
|
||||||
<div class="bg-black/40 backdrop-blur-sm py-8 px-6 rounded-lg">
|
<div class="flex flex-col">
|
||||||
<div class="grid grid-cols-[1fr_auto] gap-y-5 items-center px-6">
|
<Label class="text-sm" for="setting-patch">Patching</Label>
|
||||||
<div class="flex flex-col">
|
<div class="text-muted-foreground text-xs">
|
||||||
<Label class="text-sm" for="setting-patch">Patching</Label>
|
Shows misses in Relax and Autopilot {#if $platform !== 'windows'}<span
|
||||||
<div class="text-muted-foreground text-xs">
|
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]"
|
||||||
Shows misses in Relax and Autopilot {#if $platform !== 'windows'}<span
|
>currently only on 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}
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Checkbox
|
|
||||||
id="setting-patch"
|
|
||||||
checked={$platform === 'windows' ? $patch : false}
|
|
||||||
disabled={$platform !== 'windows'}
|
|
||||||
onCheckedChange={async (e) => {
|
|
||||||
patch.set(e);
|
|
||||||
$userSettings.save();
|
|
||||||
}}
|
|
||||||
class="flex items-center justify-center w-5 h-5"
|
|
||||||
></Checkbox>
|
|
||||||
|
|
||||||
<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);
|
|
||||||
|
|
||||||
$userSettings.save();
|
|
||||||
}}
|
|
||||||
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);
|
|
||||||
$userSettings.save();
|
|
||||||
}}
|
|
||||||
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);
|
|
||||||
$userSettings.save();
|
|
||||||
}}
|
|
||||||
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>
|
|
||||||
{/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="flex flex-col">
|
|
||||||
<Label class="text-sm" for="setting-tracking">App Tracking</Label>
|
|
||||||
<div class="text-muted-foreground text-xs">
|
|
||||||
Allow anonymous usage data to be collected to help improve the application.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Checkbox
|
|
||||||
id="setting-tracking"
|
|
||||||
checked={$trackingEnabled}
|
|
||||||
onCheckedChange={async (e) => {
|
|
||||||
trackingEnabled.set(e);
|
|
||||||
$userSettings.value('tracking_consent').set(e);
|
|
||||||
await $userSettings.save();
|
|
||||||
}}
|
|
||||||
class="flex items-center justify-center w-5 h-5"
|
|
||||||
></Checkbox>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
</div>
|
||||||
class="grid grid-cols-[0.7fr_auto] gap-y-1 items-center border-theme-800 pl-6 pr-5 pb-4"
|
<Checkbox
|
||||||
|
id="setting-patch"
|
||||||
|
checked={$platform === 'windows' ? $patch : false}
|
||||||
|
disabled={$platform !== 'windows'}
|
||||||
|
onCheckedChange={async (e) => {
|
||||||
|
patch.set(e);
|
||||||
|
$userSettings.save();
|
||||||
|
}}
|
||||||
|
class="flex items-center justify-center w-5 h-5"
|
||||||
|
></Checkbox>
|
||||||
|
|
||||||
|
<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);
|
||||||
|
|
||||||
|
$userSettings.save();
|
||||||
|
}}
|
||||||
|
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);
|
||||||
|
$userSettings.save();
|
||||||
|
}}
|
||||||
|
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);
|
||||||
|
$userSettings.save();
|
||||||
|
}}
|
||||||
|
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>
|
||||||
|
{/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="flex flex-col">
|
||||||
|
<Label class="text-sm" for="setting-tracking">App Tracking</Label>
|
||||||
|
<div class="text-muted-foreground text-xs">
|
||||||
|
Allow anonymous usage data to be collected to help improve the application.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
id="setting-tracking"
|
||||||
|
checked={$trackingEnabled}
|
||||||
|
onCheckedChange={async (e) => {
|
||||||
|
trackingEnabled.set(e);
|
||||||
|
$userSettings.value('tracking_consent').set(e);
|
||||||
|
await $userSettings.save();
|
||||||
|
}}
|
||||||
|
class="flex items-center justify-center w-5 h-5"
|
||||||
|
></Checkbox>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-[0.7fr_auto] gap-y-1 mt-3 w-full items-center border-theme-800 pl-6 pr-5 pb-4"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<Label class="text-sm" for="setting-custom-cursor">osu! installation path</Label>
|
||||||
|
<div class="text-muted-foreground text-xs">The path to your osu! installation.</div>
|
||||||
|
</div>
|
||||||
|
<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"
|
||||||
|
value={$osuInstallationPath}
|
||||||
|
placeholder="Path to osu! installation"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
class="mt-4 bg-theme-950 border-theme-800 rounded-l-none"
|
||||||
|
variant="outline"
|
||||||
|
onclick={browse_osu_installation}>Browse</Button
|
||||||
>
|
>
|
||||||
<div class="flex flex-col">
|
</div>
|
||||||
<Label class="text-sm" for="setting-custom-cursor">osu! installation path</Label>
|
|
||||||
<div class="text-muted-foreground text-xs">The path to your osu! installation.</div>
|
|
||||||
</div>
|
|
||||||
<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"
|
|
||||||
value={$osuInstallationPath}
|
|
||||||
placeholder="Path to osu! installation"
|
|
||||||
readonly
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
class="mt-4 bg-theme-950 border-theme-800 rounded-l-none"
|
|
||||||
variant="outline"
|
|
||||||
onclick={browse_osu_installation}>Browse</Button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<Label class="text-sm" for="setting-custom-cursor">patcher release stream</Label>
|
<Label class="text-sm" for="setting-custom-cursor">patcher release stream</Label>
|
||||||
<div class="text-muted-foreground text-xs">
|
<div class="text-muted-foreground text-xs">test different versions of the patcher</div>
|
||||||
test different versions of the patcher
|
</div>
|
||||||
|
<div class="flex flex-row w-full">
|
||||||
|
<Select.Root
|
||||||
|
type="single"
|
||||||
|
bind:value={$launcherStream}
|
||||||
|
onValueChange={async (newStream) => {
|
||||||
|
const isNet8Installed = await hasNet8();
|
||||||
|
if (!isNet8Installed) {
|
||||||
|
launcherStream.set('stable');
|
||||||
|
sileo.error({
|
||||||
|
title: 'Hmm...',
|
||||||
|
description: '.NET 8.0 Desktop Runtime not found!',
|
||||||
|
fill: '#181825',
|
||||||
|
styles: {
|
||||||
|
description: 'text-center!',
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
title: 'Download .NET 8.0',
|
||||||
|
onClick: async () =>
|
||||||
|
await openURL(
|
||||||
|
'https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-8.0.22-windows-x64-installer'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$userSettings.value('patcherStream').set(newStream);
|
||||||
|
launcherStream.set(newStream);
|
||||||
|
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">
|
||||||
|
{$launcherStream}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Select.Trigger>
|
||||||
<div class="flex flex-row w-full">
|
<Select.Content class="bg-theme-950 border border-theme-950 rounded-lg">
|
||||||
<Select.Root
|
{#each $launcherStreams as stream (stream)}
|
||||||
type="single"
|
<Select.Item value={stream}>
|
||||||
bind:value={$launcherStream}
|
<div class="flex flex-row gap-2 items-center">
|
||||||
onValueChange={async (newStream) => {
|
{stream}
|
||||||
const isNet8Installed = await hasNet8();
|
|
||||||
if (!isNet8Installed) {
|
|
||||||
launcherStream.set('stable');
|
|
||||||
sileo.error({
|
|
||||||
title: 'Hmm...',
|
|
||||||
description: '.NET 8.0 Desktop Runtime not found!',
|
|
||||||
fill: '#181825',
|
|
||||||
styles: {
|
|
||||||
description: 'text-center!',
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
title: 'Download .NET 8.0',
|
|
||||||
onClick: async () =>
|
|
||||||
await openURL(
|
|
||||||
'https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-8.0.22-windows-x64-installer'
|
|
||||||
),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$userSettings.value('patcherStream').set(newStream);
|
|
||||||
launcherStream.set(newStream);
|
|
||||||
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">
|
|
||||||
{$launcherStream}
|
|
||||||
</div>
|
</div>
|
||||||
</Select.Trigger>
|
</Select.Item>
|
||||||
<Select.Content class="bg-theme-950 border border-theme-950 rounded-lg">
|
{/each}
|
||||||
{#each $launcherStreams as stream (stream)}
|
</Select.Content>
|
||||||
<Select.Item value={stream}>
|
</Select.Root>
|
||||||
<div class="flex flex-row gap-2 items-center">
|
|
||||||
{stream}
|
|
||||||
</div>
|
|
||||||
</Select.Item>
|
|
||||||
{/each}
|
|
||||||
</Select.Content>
|
|
||||||
</Select.Root>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if selectedView === 'login'}
|
{:else if selectedView === 'login'}
|
||||||
<div
|
<div
|
||||||
class="h-[100vh] w-full flex flex-col items-center justify-center"
|
class="h-[100vh] w-full flex flex-col items-center justify-center bg-black/20 backdrop-blur-sm"
|
||||||
in:fly={{
|
in:fly={{
|
||||||
duration: $reduceAnimations ? 0 : 400,
|
duration: $reduceAnimations ? 0 : 400,
|
||||||
delay: $reduceAnimations ? 0 : 400,
|
delay: $reduceAnimations ? 0 : 400,
|
||||||
@@ -1483,48 +1504,142 @@
|
|||||||
}}
|
}}
|
||||||
out:fly={{ duration: $reduceAnimations ? 0 : 400, y: -5, opacity: 0 }}
|
out:fly={{ duration: $reduceAnimations ? 0 : 400, y: -5, opacity: 0 }}
|
||||||
>
|
>
|
||||||
<div class="p-8 px-52 w-full">
|
<form onsubmit={performLogin} class="w-96">
|
||||||
<form onsubmit={performLogin} class="bg-black/40 backdrop-blur-sm rounded-lg p-8">
|
<div class="flex flex-col items-center justify-center mb-8">
|
||||||
<div class="flex flex-col items-center justify-center mb-4">
|
<span class="text-xl font-semibold">Login to EZPPFarm</span>
|
||||||
<span class="text-xl font-semibold">Login to EZPPFarm</span>
|
<span class="text-xs text-muted-foreground"
|
||||||
<span class="text-xs text-muted-foreground"
|
>Use your EZPPFarm account to login to EZPPLauncher</span
|
||||||
>Use your EZPPFarm account to login to EZPPLauncher</span
|
>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<Label for="username" class="block text-sm font-medium">Username</Label>
|
||||||
|
<Input
|
||||||
|
class="mt-4 w-full bg-theme-900 border-theme-800"
|
||||||
|
type="text"
|
||||||
|
id="username"
|
||||||
|
bind:value={username}
|
||||||
|
disabled={loginIsLoading}
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<Label for="password" class="block text-sm font-medium">Password</Label>
|
||||||
|
<Input
|
||||||
|
class="mt-4 w-full bg-theme-900 border-theme-800"
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
bind:value={password}
|
||||||
|
disabled={loginIsLoading}
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button class="w-full" type="submit" disabled={loginIsLoading}>
|
||||||
|
{#if loginIsLoading}
|
||||||
|
<LoaderCircle class="animate-spin" />
|
||||||
|
{:else}
|
||||||
|
Login
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{:else if selectedView === 'skins'}
|
||||||
|
<div
|
||||||
|
class="h-[100vh] w-full flex flex-col items-center justify-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 }}
|
||||||
|
>
|
||||||
|
{#if $skins.length > 0}
|
||||||
|
{@const skinsSorted = $skins.sort((a, b) =>
|
||||||
|
a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' })
|
||||||
|
)}
|
||||||
|
<ScrollContainer
|
||||||
|
class="flex flex-col max-h-[100vh] overflow-y-auto items-center gap-4 p-16 w-full"
|
||||||
|
topOffset={35}
|
||||||
|
>
|
||||||
|
{#each skinsSorted as skin (skin.name)}
|
||||||
|
<div
|
||||||
|
class={`group w-full flex items-center border rounded-lg transition-all duration-150 ${
|
||||||
|
skin.name === $currentSkin
|
||||||
|
? 'bg-primary/20 border-primary/40'
|
||||||
|
: 'bg-black/40 border-black/60 hover:bg-black/70 hover:border-border'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
</div>
|
<div class="flex-1 min-w-0 px-5 py-3.5">
|
||||||
<div class="mb-4">
|
<p class="text-sm text-foreground/90 truncate font-medium line-clamp-1">
|
||||||
<Label for="username" class="block text-sm font-medium">Username</Label>
|
{skin.name}
|
||||||
<Input
|
</p>
|
||||||
class="mt-4 w-full bg-theme-900 border-theme-800"
|
<div class="flex items-center gap-4 mt-1">
|
||||||
type="text"
|
<span class="text-[11px] text-muted-foreground">
|
||||||
id="username"
|
{skin.author || 'Unknown'}
|
||||||
bind:value={username}
|
</span>
|
||||||
disabled={loginIsLoading}
|
<span class="text-[11px] text-muted-foreground/50">
|
||||||
autocomplete="off"
|
{new Date(skin.modified * 1000).toLocaleDateString(undefined, {
|
||||||
autocorrect="off"
|
year: '2-digit',
|
||||||
/>
|
month: 'short',
|
||||||
</div>
|
day: 'numeric',
|
||||||
<div class="mb-4">
|
})}
|
||||||
<Label for="password" class="block text-sm font-medium">Password</Label>
|
</span>
|
||||||
<Input
|
</div>
|
||||||
class="mt-4 w-full bg-theme-900 border-theme-800"
|
</div>
|
||||||
type="password"
|
|
||||||
id="password"
|
<div
|
||||||
bind:value={password}
|
class="flex items-center gap-1.5 pr-3 opacity-0 group-hover:opacity-100 transition-opacity duration-150"
|
||||||
disabled={loginIsLoading}
|
>
|
||||||
autocomplete="off"
|
{#if $currentSkin !== skin.name}
|
||||||
autocorrect="off"
|
<button
|
||||||
/>
|
transition:fade={{
|
||||||
</div>
|
duration: 150,
|
||||||
<Button class="w-full" type="submit" disabled={loginIsLoading}>
|
}}
|
||||||
{#if loginIsLoading}
|
class="p-2 rounded-md text-muted-foreground hover:text-primary hover:bg-primary/10 transition-colors"
|
||||||
<LoaderCircle class="animate-spin" />
|
onclick={async () => {
|
||||||
{:else}
|
if (skin.name === $currentSkin) return;
|
||||||
Login
|
await setUserConfigValues($osuInstallationPath, [
|
||||||
{/if}
|
{ key: 'Skin', value: skin.name },
|
||||||
</Button>
|
]);
|
||||||
</form>
|
currentSkin.set(skin.name);
|
||||||
</div>
|
sileo.success({
|
||||||
|
title: 'Skin applied!',
|
||||||
|
description: `${skin.name} is now your active skin!`,
|
||||||
|
fill: '#181825',
|
||||||
|
styles: {
|
||||||
|
description: 'text-center!',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
<!-- <button
|
||||||
|
class="p-2 rounded-md transition-colors text-muted-foreground hover:text-destructive hover:bg-destructive/10"
|
||||||
|
>
|
||||||
|
<Trash2 class="w-4 h-4" />
|
||||||
|
</button> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</ScrollContainer>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col items-center justify-center gap-4">
|
||||||
|
<FileQuestionMark class="size-12 text-muted-foreground" strokeWidth={1.5} />
|
||||||
|
<span class="text-sm text-muted-foreground">No skins found!</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.custom-scrollbox {
|
||||||
|
scrollbar-color: #999 transparent;
|
||||||
|
scrollbar-width: 0px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
osuBuild,
|
osuBuild,
|
||||||
osuStream,
|
osuStream,
|
||||||
skins,
|
skins,
|
||||||
|
skinsCount,
|
||||||
} from '@/global';
|
} from '@/global';
|
||||||
import {
|
import {
|
||||||
cursorSmoothness,
|
cursorSmoothness,
|
||||||
@@ -34,7 +35,7 @@
|
|||||||
getEZPPLauncherStreams,
|
getEZPPLauncherStreams,
|
||||||
getReleaseStream,
|
getReleaseStream,
|
||||||
getSkin,
|
getSkin,
|
||||||
getSkinsCount,
|
getSkins,
|
||||||
getVersion,
|
getVersion,
|
||||||
isValidOsuFolder,
|
isValidOsuFolder,
|
||||||
} from '@/osuUtil';
|
} from '@/osuUtil';
|
||||||
@@ -151,8 +152,15 @@
|
|||||||
if (beatmapSetCount !== null) beatmapSets.set(beatmapSetCount);
|
if (beatmapSetCount !== null) beatmapSets.set(beatmapSetCount);
|
||||||
|
|
||||||
currentLoadingInfo.set('Counting skins...');
|
currentLoadingInfo.set('Counting skins...');
|
||||||
const skinCount = await getSkinsCount($osuInstallationPath);
|
const skins_list = await getSkins($osuInstallationPath);
|
||||||
if (skinCount !== null) skins.set(skinCount);
|
console.log(skins_list);
|
||||||
|
if (skins_list) {
|
||||||
|
skins.set(skins_list);
|
||||||
|
skinsCount.set(skins_list.length);
|
||||||
|
} else {
|
||||||
|
skinsCount.set(0);
|
||||||
|
}
|
||||||
|
|
||||||
const skin: string = await getSkin($osuInstallationPath);
|
const skin: string = await getSkin($osuInstallationPath);
|
||||||
currentSkin.set(skin);
|
currentSkin.set(skin);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
platform,
|
platform,
|
||||||
presenceLoading,
|
presenceLoading,
|
||||||
skins,
|
skins,
|
||||||
|
skinsCount,
|
||||||
} from '@/global';
|
} from '@/global';
|
||||||
import Launch from './Launch.svelte';
|
import Launch from './Launch.svelte';
|
||||||
import Confetti from 'svelte-confetti';
|
import Confetti from 'svelte-confetti';
|
||||||
@@ -35,7 +36,7 @@
|
|||||||
getBeatmapSetsCount,
|
getBeatmapSetsCount,
|
||||||
getReleaseStream,
|
getReleaseStream,
|
||||||
getSkin,
|
getSkin,
|
||||||
getSkinsCount,
|
getSkins,
|
||||||
getVersion,
|
getVersion,
|
||||||
isValidOsuFolder,
|
isValidOsuFolder,
|
||||||
} from '@/osuUtil';
|
} from '@/osuUtil';
|
||||||
@@ -102,9 +103,12 @@
|
|||||||
beatmapSets.set(beatmapSetCount);
|
beatmapSets.set(beatmapSetCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
const skinsCount: number | null = await getSkinsCount(osuInstallPath);
|
const skins_list = await getSkins(osuInstallPath);
|
||||||
if (skinsCount) {
|
if (skins_list) {
|
||||||
skins.set(skinsCount);
|
skins.set(skins_list);
|
||||||
|
skinsCount.set(skins_list.length);
|
||||||
|
} else {
|
||||||
|
skinsCount.set(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const skin: string = await getSkin(osuInstallPath);
|
const skin: string = await getSkin(osuInstallPath);
|
||||||
|
|||||||
Reference in New Issue
Block a user