Compare commits
9 Commits
3.1.2
...
2e0ef81bcb
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e0ef81bcb | |||
| 96680ff8b5 | |||
| 0b9d6373bf | |||
| 295c1424c9 | |||
| 0e12a801d2 | |||
| cf86dba5c4 | |||
| 540c653399 | |||
| c9b8d9f599 | |||
| 49665d94bc |
Generated
+1
@@ -1011,6 +1011,7 @@ version = "3.1.2"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"discord-rich-presence",
|
||||
"futures-util",
|
||||
"hardware-id",
|
||||
"md5",
|
||||
"once_cell",
|
||||
|
||||
@@ -32,15 +32,16 @@ reqwest = { version = "0.13.1", features = ["json", "stream", "query"] }
|
||||
md5 = "0.8.0"
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
open = "5.3.3"
|
||||
windows-sys = "0.61.2"
|
||||
discord-rich-presence = "1.0.0"
|
||||
once_cell = "1.21.3"
|
||||
futures-util = "0.3"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = "0.55.0"
|
||||
winapi = { version = "0.3", features = ["winuser", "wincrypt", "memoryapi", "winbase", "dpapi"] }
|
||||
base64 = "0.22.1"
|
||||
widestring = "1.2.1"
|
||||
windows-sys = "0.61.2"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-single-instance = "2.3.6"
|
||||
|
||||
@@ -185,6 +185,75 @@ pub async fn get_beatmapsets_count(folder: String) -> Option<u64> {
|
||||
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]
|
||||
pub async fn get_skins_count(folder: String) -> Option<u64> {
|
||||
let path = PathBuf::from(folder);
|
||||
|
||||
@@ -8,8 +8,8 @@ use crate::commands::{
|
||||
check_for_corruption, download_ezpp_launcher_update, download_ezpp_launcher_update_files,
|
||||
encrypt_string, exit, find_osu_installation, get_beatmapsets_count,
|
||||
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,
|
||||
has_wmctrl, install_ezpp_launcher_update, is_osu_running, open_url_in_browser,
|
||||
get_osu_skin, get_osu_version, get_platform, get_skins, get_skins_count, has_net8,
|
||||
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_update_user, replace_ui_files, run_osu, run_osu_updater, set_osu_config_values,
|
||||
set_osu_user_config_values, valid_osu_folder,
|
||||
@@ -37,6 +37,7 @@ pub fn run() {
|
||||
find_osu_installation,
|
||||
valid_osu_folder,
|
||||
get_beatmapsets_count,
|
||||
get_skins,
|
||||
get_skins_count,
|
||||
get_osu_version,
|
||||
get_osu_release_stream,
|
||||
|
||||
+29
-11
@@ -2,6 +2,7 @@ use discord_rich_presence::{
|
||||
DiscordIpc, DiscordIpcClient,
|
||||
activity::{Activity, Assets, Button, Timestamps},
|
||||
};
|
||||
use futures_util::future::BoxFuture;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
@@ -24,6 +25,7 @@ enum PresenceCommand {
|
||||
Disconnect(oneshot::Sender<()>),
|
||||
UpdateData(PresenceData),
|
||||
IsConnected(oneshot::Sender<bool>),
|
||||
Tick,
|
||||
}
|
||||
|
||||
struct PresenceActor {
|
||||
@@ -58,7 +60,7 @@ impl PresenceActor {
|
||||
}
|
||||
|
||||
async fn run(&mut self) {
|
||||
let mut update_interval = interval(Duration::from_millis(2500));
|
||||
let mut update_interval = interval(Duration::from_secs(5));
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
@@ -74,18 +76,22 @@ impl PresenceActor {
|
||||
PresenceCommand::IsConnected(responder) => {
|
||||
let _ = responder.send(self.client.is_some());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = update_interval.tick() => {
|
||||
PresenceCommand::Tick => {
|
||||
if self.client.is_some() {
|
||||
self.handle_update().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = update_interval.tick() => {
|
||||
let _ = PRESENCE_TX.send(PresenceCommand::Tick).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_connect(&mut self, responder: oneshot::Sender<bool>) {
|
||||
fn handle_connect(&mut self, responder: oneshot::Sender<bool>) -> BoxFuture<'_, ()> {
|
||||
Box::pin(async move {
|
||||
if self.client.is_some() {
|
||||
let _ = responder.send(true);
|
||||
return;
|
||||
@@ -101,11 +107,21 @@ impl PresenceActor {
|
||||
let _ = responder.send(true);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to create Discord client: {:?}", e);
|
||||
eprintln!("Failed to connect to Discord: {:?}. Retrying in 5s...", e);
|
||||
let tx = PRESENCE_TX.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
let (resp_tx, _) = oneshot::channel();
|
||||
let _ = tx.send(PresenceCommand::Connect(resp_tx)).await;
|
||||
});
|
||||
|
||||
if !responder.is_closed() {
|
||||
let _ = responder.send(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_disconnect(&mut self, responder: oneshot::Sender<()>) {
|
||||
if let Some(mut client) = self.client.take() {
|
||||
@@ -143,12 +159,14 @@ impl PresenceActor {
|
||||
Button::new("Join EZPPFarm", "https://ez-pp.farm/discord"),
|
||||
]);
|
||||
|
||||
if let Err(e) = client.set_activity(activity).map_err(|e| e.to_string()) {
|
||||
if let Err(e) = client.set_activity(activity) {
|
||||
eprintln!("Failed to set activity, disconnecting: {:?}", e);
|
||||
if let Some(mut client) = self.client.take() {
|
||||
let _ = client.clear_activity();
|
||||
let _ = client.close();
|
||||
}
|
||||
self.client = None;
|
||||
|
||||
let (tx, _) = oneshot::channel();
|
||||
self.handle_connect(tx).await;
|
||||
} else {
|
||||
println!("Actor: Presence updated successfully.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
Binary file not shown.
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Logo from '$assets/logo.png';
|
||||
import { reduceAnimations } from '@/userSettings';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
//TODO: maybe dynamic background images fetched from ezpp?
|
||||
|
||||
@@ -37,15 +39,15 @@
|
||||
<div
|
||||
class="relative h-full w-full from-0% to-90% bg-gradient-to-b from-transparent to-black z-50"
|
||||
></div>
|
||||
{#each streaks as s}
|
||||
{#each streaks as s, index (index)}
|
||||
<div
|
||||
class="streak drop-shadow-lg"
|
||||
class="streak drop-shadow-lg transition-opacity duration-1000"
|
||||
style="
|
||||
width:{s.width}px;
|
||||
height:{s.height}px;
|
||||
left:{s.x}%;
|
||||
top:{s.y}%;
|
||||
opacity:{s.opacity};
|
||||
opacity:{$reduceAnimations ? 0 : s.opacity};
|
||||
animation-duration:{s.duration}s;
|
||||
animation-delay:-{s.delay}s;
|
||||
"
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
import NyanCat from '$assets/nyan-cat.gif';
|
||||
import { reduceAnimations } from '@/userSettings';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
//TODO: maybe dynamic background images fetched from ezpp?
|
||||
|
||||
const COUNT = 18;
|
||||
|
||||
function rand(min: number, max: number) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
const streaks = Array.from({ length: COUNT })
|
||||
.map((_, i) => {
|
||||
const depth = rand(0, 2);
|
||||
|
||||
const scale = 0.6 + depth * 0.8;
|
||||
const speed = 0.4 + depth * 1.35;
|
||||
|
||||
const distance = 150;
|
||||
const duration = distance / speed;
|
||||
|
||||
return {
|
||||
width: 290 * scale,
|
||||
height: 140 * scale,
|
||||
x: (i / COUNT) * 200 - 50,
|
||||
y: ((i * 137.5) % 100) - 20,
|
||||
opacity: 0.25 + depth * 0.65,
|
||||
duration,
|
||||
delay: rand(0, duration),
|
||||
scale,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.scale - b.scale);
|
||||
</script>
|
||||
|
||||
<div class="absolute bg blur-[2px]">
|
||||
<div
|
||||
class="relative h-full w-full from-0% to-90% bg-gradient-to-b from-transparent to-black z-50"
|
||||
></div>
|
||||
{#each streaks as s, index (index)}
|
||||
<div
|
||||
class="streak drop-shadow-lg transition-opacity duration-1000"
|
||||
style="
|
||||
width:{s.width}px;
|
||||
height:{s.height}px;
|
||||
left:{s.x}%;
|
||||
top:{s.y}%;
|
||||
opacity:{$reduceAnimations ? 0 : s.opacity};
|
||||
animation-duration:{s.duration}s;
|
||||
animation-delay:-{s.delay}s;
|
||||
"
|
||||
>
|
||||
<img
|
||||
src={NyanCat}
|
||||
alt="nyan cat"
|
||||
class="w-full h-full"
|
||||
style="opacity:{$reduceAnimations ? 0 : s.opacity};"
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.bg {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
var(--color-blue-800) 0%,
|
||||
var(--color-blue-600) 40%,
|
||||
var(--color-blue-900) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.streak {
|
||||
position: absolute;
|
||||
animation: move linear infinite;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
@keyframes move {
|
||||
from {
|
||||
transform: translate(-250vw, 0);
|
||||
}
|
||||
to {
|
||||
transform: translate(250vw, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -8,6 +8,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { launcherVersion } from '@/global';
|
||||
import Badge from '../badge/badge.svelte';
|
||||
import { reduceAnimations } from '@/userSettings';
|
||||
|
||||
onMount(() => {
|
||||
const appWindow = getCurrentWindow();
|
||||
@@ -20,22 +21,39 @@
|
||||
</script>
|
||||
|
||||
<div data-tauri-drag-region class="titlebar z-[100]">
|
||||
<div class="titlebar-button rounded transition-colors duration-75" id="titlebar-minimize">
|
||||
<Minimize size={14} />
|
||||
<div class="relative flex items-center justify-center p-1 group" id="titlebar-minimize">
|
||||
<div
|
||||
class="absolute inset-0 bg-background rounded
|
||||
scale-50 opacity-0
|
||||
group-hover:opacity-100
|
||||
group-hover:scale-100
|
||||
{$reduceAnimations ? '' : 'transition-all duration-200 ease-out'}
|
||||
origin-center"
|
||||
></div>
|
||||
|
||||
<Minimize class="z-10 pointer-events-none" size={14} />
|
||||
</div>
|
||||
<div class="titlebar-button close rounded transition-colors duration-75" id="titlebar-close">
|
||||
<Close size={14} />
|
||||
<div class="relative flex items-center justify-center p-1 group" id="titlebar-close">
|
||||
<div
|
||||
class="absolute inset-0 bg-red-500 rounded
|
||||
scale-50 opacity-0
|
||||
group-hover:opacity-100
|
||||
group-hover:scale-100
|
||||
{$reduceAnimations ? '' : 'transition-all duration-200 ease-out'}
|
||||
origin-center"
|
||||
></div>
|
||||
|
||||
<Close class="z-10 pointer-events-none" size={14} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.titlebar {
|
||||
height: 35px;
|
||||
/* background: #040612; */
|
||||
user-select: none;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -45,19 +63,4 @@
|
||||
padding-right: 5px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.titlebar-button {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
.titlebar-button:hover {
|
||||
&.close {
|
||||
background: #c22e2e;
|
||||
}
|
||||
background: #2d3049;
|
||||
}
|
||||
</style>
|
||||
|
||||
+3
-1
@@ -24,6 +24,7 @@ export const currentLoadingInfo = writable<string>('Initializing...');
|
||||
export const firstStartup = writable<boolean>(false);
|
||||
|
||||
export const launching = writable<boolean>(false);
|
||||
export const nyanCatSong = writable<HTMLAudioElement | undefined>(undefined);
|
||||
|
||||
export const serverPing = writable<number | undefined>(undefined);
|
||||
export const serverConnectionFails = writable(0);
|
||||
@@ -31,7 +32,8 @@ export const serverConnectionFails = writable(0);
|
||||
export const onlineFriends = 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 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';
|
||||
|
||||
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 () => {
|
||||
const hwid = await invoke('get_hwid');
|
||||
return typeof hwid === 'string' ? hwid : undefined;
|
||||
@@ -55,13 +67,6 @@ export const getBeatmapSetsCount = async (folder: string) => {
|
||||
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) => {
|
||||
const result = await invoke('get_osu_skin', {
|
||||
folder,
|
||||
@@ -151,3 +156,8 @@ export const downloadUpdate = async (
|
||||
}
|
||||
};
|
||||
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 }
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Logo from '$assets/logo.png';
|
||||
import NyanCatSong from '$assets/nyan-cat.mp3';
|
||||
import '../app.css';
|
||||
|
||||
import Titlebar from '@/components/ui/titlebar/titlebar.svelte';
|
||||
@@ -9,6 +10,7 @@
|
||||
discordPresence,
|
||||
firstStartup,
|
||||
launcherVersion,
|
||||
nyanCatSong,
|
||||
platform,
|
||||
presenceLoading,
|
||||
setupValues,
|
||||
@@ -30,14 +32,18 @@
|
||||
import { encryptString, exit, getLauncherVersion, getPlatform } from '@/osuUtil';
|
||||
import Button from '@/components/ui/button/button.svelte';
|
||||
import * as presence from '@/presence';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import '@fontsource/sora';
|
||||
import '@fontsource/space-mono';
|
||||
import AnimatedBg from '@/components/ui/animated-bg/AnimatedBg.svelte';
|
||||
import NyanCatBg from '@/components/ui/animated-bg/NyanCatBg.svelte';
|
||||
import { sileo } from 'sileo';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
let unsupported_platform = $state<boolean>(false);
|
||||
let secret_enabled = $state<boolean>(false);
|
||||
|
||||
function disableReload() {
|
||||
if (window.location.hostname !== 'tauri.localhost') {
|
||||
@@ -87,6 +93,19 @@
|
||||
);
|
||||
}
|
||||
|
||||
let konami_code = [
|
||||
'ArrowUp',
|
||||
'ArrowUp',
|
||||
'ArrowDown',
|
||||
'ArrowDown',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'b',
|
||||
'a',
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
window.Buffer = Buffer;
|
||||
|
||||
@@ -98,6 +117,107 @@
|
||||
const isFirstStartup = await $userSettings.init();
|
||||
$userAuth.init();
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key.toLowerCase() === konami_code[0].toLowerCase()) {
|
||||
konami_code.shift();
|
||||
if (konami_code.length === 0) {
|
||||
if (secret_enabled) {
|
||||
sileo.success({
|
||||
title: 'Cheatcode deactivated!',
|
||||
duration: 3000,
|
||||
fill: '#181825',
|
||||
styles: {
|
||||
description: 'text-center!',
|
||||
},
|
||||
});
|
||||
const audio = $nyanCatSong;
|
||||
if (audio) {
|
||||
const fadeOutDuration = 2000; // 2 seconds
|
||||
const fadeOutSteps = 20;
|
||||
const fadeOutStepTime = fadeOutDuration / fadeOutSteps;
|
||||
const volumeStep = audio.volume / fadeOutSteps;
|
||||
|
||||
const fadeOutInterval = setInterval(() => {
|
||||
if (audio.volume > 0) {
|
||||
audio.volume = Math.max(0, audio.volume - volumeStep);
|
||||
} else {
|
||||
audio.pause();
|
||||
clearInterval(fadeOutInterval);
|
||||
}
|
||||
}, fadeOutStepTime);
|
||||
}
|
||||
nyanCatSong.set(undefined);
|
||||
secret_enabled = false;
|
||||
konami_code = [
|
||||
'ArrowUp',
|
||||
'ArrowUp',
|
||||
'ArrowDown',
|
||||
'ArrowDown',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'b',
|
||||
'a',
|
||||
];
|
||||
return;
|
||||
}
|
||||
sileo.success({
|
||||
title: 'Cheatcode activated!',
|
||||
duration: 3000,
|
||||
fill: '#181825',
|
||||
styles: {
|
||||
description: 'text-center!',
|
||||
},
|
||||
});
|
||||
const audio = new Audio(NyanCatSong);
|
||||
audio.loop = true;
|
||||
audio.volume = 0;
|
||||
audio.play();
|
||||
const fadeInDuration = 2000; // 2 seconds
|
||||
const fadeInSteps = 20;
|
||||
const fadeInStepTime = fadeInDuration / fadeInSteps;
|
||||
const volumeStep = (1 - audio.volume) / fadeInSteps;
|
||||
|
||||
const fadeInInterval = setInterval(() => {
|
||||
if (audio.volume < 0.3) {
|
||||
audio.volume = Math.min(0.3, audio.volume + volumeStep);
|
||||
} else {
|
||||
clearInterval(fadeInInterval);
|
||||
}
|
||||
}, fadeInStepTime);
|
||||
nyanCatSong.set(audio);
|
||||
|
||||
secret_enabled = true;
|
||||
konami_code = [
|
||||
'ArrowUp',
|
||||
'ArrowUp',
|
||||
'ArrowDown',
|
||||
'ArrowDown',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'b',
|
||||
'a',
|
||||
];
|
||||
}
|
||||
} else {
|
||||
konami_code = [
|
||||
'ArrowUp',
|
||||
'ArrowUp',
|
||||
'ArrowDown',
|
||||
'ArrowDown',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'b',
|
||||
'a',
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
currentLoadingInfo.set('Loading config...');
|
||||
const config_patching = $userSettings.value('patching');
|
||||
const config_custom_cursor = $userSettings.value('custom_cursor');
|
||||
@@ -180,7 +300,15 @@
|
||||
|
||||
<main>
|
||||
<div class="opacity-30">
|
||||
{#if secret_enabled}
|
||||
<div transition:fade={{ duration: $reduceAnimations ? 0 : 500 }}>
|
||||
<NyanCatBg />
|
||||
</div>
|
||||
{:else}
|
||||
<div transition:fade={{ duration: $reduceAnimations ? 0 : 500 }}>
|
||||
<AnimatedBg />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
+212
-42
@@ -13,6 +13,7 @@
|
||||
launcherVersion,
|
||||
launching,
|
||||
newVersion,
|
||||
nyanCatSong,
|
||||
osuBuild,
|
||||
osuStream,
|
||||
platform,
|
||||
@@ -20,6 +21,7 @@
|
||||
serverConnectionFails,
|
||||
serverPing,
|
||||
skins,
|
||||
skinsCount,
|
||||
trackingEnabled,
|
||||
} from '@/global';
|
||||
import {
|
||||
@@ -37,6 +39,8 @@
|
||||
ArrowRight,
|
||||
Settings,
|
||||
House,
|
||||
Paintbrush,
|
||||
FileQuestionMark,
|
||||
} from 'lucide-svelte';
|
||||
import NumberFlow from '@number-flow/svelte';
|
||||
import * as AlertDialog from '@/components/ui/alert-dialog';
|
||||
@@ -49,7 +53,7 @@
|
||||
releaseStreamToReadable,
|
||||
urlIsValidImage,
|
||||
} from '@/utils';
|
||||
import { fade, fly, scale } from 'svelte/transition';
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import Label from '@/components/ui/label/label.svelte';
|
||||
import {
|
||||
@@ -84,7 +88,7 @@
|
||||
getEZPPLauncherUpdateFiles,
|
||||
getReleaseStream,
|
||||
getSkin,
|
||||
getSkinsCount,
|
||||
getSkins,
|
||||
getVersion,
|
||||
hasNet8,
|
||||
hasOsuWinello,
|
||||
@@ -107,8 +111,9 @@
|
||||
import * as DropdownMenu from '@/components/ui/dropdown-menu';
|
||||
import DownloadButton from '@/components/ui/download-button/DownloadButton.svelte';
|
||||
import { animate } from 'animejs';
|
||||
import AnimatedBg from '@/components/ui/animated-bg/AnimatedBg.svelte';
|
||||
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 progress = $state(-1);
|
||||
@@ -172,8 +177,11 @@
|
||||
const beatmapSetCount: number | null = await getBeatmapSetsCount(selectedPath);
|
||||
if (beatmapSetCount) beatmapSets.set(beatmapSetCount);
|
||||
|
||||
const skinsCount: number | null = await getSkinsCount(selectedPath);
|
||||
if (skinsCount !== null) skins.set(skinsCount);
|
||||
const skins_list = await getSkins(selectedPath);
|
||||
if (skins_list) {
|
||||
skins.set(skins_list);
|
||||
skinsCount.set(skins_list.length);
|
||||
}
|
||||
|
||||
const skin: string = await getSkin(selectedPath);
|
||||
currentSkin.set(skin);
|
||||
@@ -187,6 +195,8 @@
|
||||
|
||||
const launch = async () => {
|
||||
if ($launching) return;
|
||||
launchInfo = 'Checking if osu! is already running...';
|
||||
launching.set(true);
|
||||
if ($trackingEnabled) umami.track('app_launch_osu');
|
||||
const osuRunning = await isOsuRunning();
|
||||
if (osuRunning) {
|
||||
@@ -199,8 +209,10 @@
|
||||
description: 'text-center!',
|
||||
},
|
||||
});
|
||||
launching.set(false);
|
||||
return;
|
||||
}
|
||||
launchInfo = 'Checking osu! installation...';
|
||||
if (!$osuBuild) {
|
||||
sileo.error({
|
||||
title: 'Hmmm...',
|
||||
@@ -210,12 +222,12 @@
|
||||
description: 'text-center!',
|
||||
},
|
||||
});
|
||||
launching.set(false);
|
||||
return;
|
||||
}
|
||||
const osuPath = $osuInstallationPath;
|
||||
|
||||
launchInfo = 'Validating osu! installation...';
|
||||
launching.set(true);
|
||||
|
||||
const validFolder = await isValidOsuFolder(osuPath);
|
||||
if (!validFolder) {
|
||||
@@ -426,6 +438,25 @@
|
||||
}
|
||||
await new Promise((res) => setTimeout(res, 1500));
|
||||
launchInfo = 'Launching osu!...';
|
||||
|
||||
if ($nyanCatSong) {
|
||||
const audio = $nyanCatSong;
|
||||
const fadeOutDuration = 2000; // 2 seconds
|
||||
const fadeOutSteps = 20;
|
||||
const fadeOutStepTime = fadeOutDuration / fadeOutSteps;
|
||||
const volumeStep = audio.volume / fadeOutSteps;
|
||||
|
||||
const fadeOutInterval = setInterval(() => {
|
||||
if (audio.volume > 0) {
|
||||
audio.volume = Math.max(0, audio.volume - volumeStep);
|
||||
} else {
|
||||
clearInterval(fadeOutInterval);
|
||||
audio.pause();
|
||||
audio.currentTime = 0;
|
||||
}
|
||||
}, fadeOutStepTime);
|
||||
}
|
||||
|
||||
await replaceUIFiles(osuPath, false);
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
await getCurrentWindow().hide();
|
||||
@@ -535,6 +566,26 @@
|
||||
if ($trackingEnabled) umami.track('app_exit_osu');
|
||||
cleanup = true;
|
||||
launchInfo = 'Cleaning up...';
|
||||
|
||||
if ($nyanCatSong) {
|
||||
const audio = $nyanCatSong;
|
||||
audio.play();
|
||||
audio.volume = 0;
|
||||
|
||||
const fadeInDuration = 2000; // 2 seconds
|
||||
const fadeInSteps = 20;
|
||||
const fadeInStepTime = fadeInDuration / fadeInSteps;
|
||||
const volumeStep = (1 - audio.volume) / fadeInSteps;
|
||||
|
||||
const fadeInInterval = setInterval(() => {
|
||||
if (audio.volume < 0.3) {
|
||||
audio.volume = Math.min(0.3, audio.volume + volumeStep);
|
||||
} else {
|
||||
clearInterval(fadeInInterval);
|
||||
}
|
||||
}, fadeInStepTime);
|
||||
}
|
||||
|
||||
await getCurrentWindow().show();
|
||||
if (presenceUpdater) {
|
||||
window.clearInterval(presenceUpdater);
|
||||
@@ -563,8 +614,11 @@
|
||||
const beatmapSetCount = await getBeatmapSetsCount(osuPath);
|
||||
if (beatmapSetCount) beatmapSets.set(beatmapSetCount);
|
||||
|
||||
const skinCount = await getSkinsCount(osuPath);
|
||||
if (skinCount !== null) skins.set(skinCount);
|
||||
const skinsList = await getSkins(osuPath);
|
||||
if (skinsList) {
|
||||
skins.set(skinsList);
|
||||
skinsCount.set(skinsList.length);
|
||||
}
|
||||
|
||||
const skin = await getSkin(osuPath);
|
||||
currentSkin.set(skin);
|
||||
@@ -874,14 +928,15 @@
|
||||
<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"
|
||||
>
|
||||
<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} />
|
||||
</div>
|
||||
<Badge class="text-[0.5rem] py-0 px-2">{$launcherVersion || 'dev'}</Badge>
|
||||
<Button
|
||||
class="flex size-12 items-center gap-2 border {selectedView === 'home'
|
||||
? 'bg-primary-300/50 border-primary-300/50'
|
||||
: 'bg-black/20 border-black/20'} hover:bg-primary-300/50 hover:border-primary-300/50 rounded-[0.85rem] p-3 mt-3"
|
||||
class="flex size-12 items-center gap-2 ring-1 ring-inset ring-white/15 {selectedView ===
|
||||
'home'
|
||||
? '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 = 'home';
|
||||
@@ -890,9 +945,22 @@
|
||||
<House class="text-theme-200 !size-5" />
|
||||
</Button>
|
||||
<Button
|
||||
class="flex size-12 items-center gap-2 border {selectedView === 'settings'
|
||||
? 'bg-primary-300/50 border-primary-300/50'
|
||||
: 'bg-black/20 border-black/20'} hover:bg-primary-300/50 hover:border-primary-300/50 rounded-[0.85rem] p-3 mt-3"
|
||||
class="flex size-12 items-center gap-2 ring-1 ring-inset ring-white/15 {selectedView ===
|
||||
'skins'
|
||||
? '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}
|
||||
onclick={() => {
|
||||
if (!$launching) selectedView = 'settings';
|
||||
@@ -948,8 +1016,13 @@
|
||||
{#if selectedView === 'home'}
|
||||
<div
|
||||
class="flex flex-col-reverse h-full"
|
||||
in:fly={{ duration: 400, delay: 400, y: 5, opacity: 0 }}
|
||||
out:fly={{ duration: 400, y: -5, opacity: 0 }}
|
||||
in:fly={{
|
||||
duration: $reduceAnimations ? 0 : 400,
|
||||
delay: $reduceAnimations ? 0 : 400,
|
||||
y: 5,
|
||||
opacity: 0,
|
||||
}}
|
||||
out:fly={{ duration: $reduceAnimations ? 0 : 400, y: -5, opacity: 0 }}
|
||||
>
|
||||
<div class="relative z-10 px-8 py-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
@@ -967,10 +1040,10 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<Brush class="size-3.5 text-amber-400" />
|
||||
<span class="text-[11px] text-white/70 font-medium">
|
||||
{#if !$skins && $skins !== 0}
|
||||
{#if !$skinsCount && $skinsCount !== 0}
|
||||
<LoaderCircle class="animate-spin" size={12} />
|
||||
{:else}
|
||||
{numberHumanReadable($skins ?? 0)}
|
||||
{numberHumanReadable($skinsCount ?? 0)}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
@@ -1069,9 +1142,9 @@
|
||||
|
||||
{#if $currentUserInfo}
|
||||
<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"
|
||||
in:fly={{ duration: 400, y: 5, opacity: 0 }}
|
||||
out:fly={{ duration: 400, y: -5, opacity: 0 }}
|
||||
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 }}
|
||||
out:fly={{ duration: $reduceAnimations ? 0 : 400, y: -5, opacity: 0 }}
|
||||
>
|
||||
<Select.Root
|
||||
type="single"
|
||||
@@ -1222,13 +1295,16 @@
|
||||
</div>
|
||||
{:else if selectedView === 'settings'}
|
||||
<div
|
||||
class="h-[100vh] w-full flex flex-col items-center justify-center"
|
||||
in:fly={{ duration: 400, delay: 400, y: 5, opacity: 0 }}
|
||||
out:fly={{ duration: 400, y: -5, opacity: 0 }}
|
||||
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 }}
|
||||
>
|
||||
<div class="p-8 w-full">
|
||||
<div class="bg-black/40 backdrop-blur-sm py-8 px-6 rounded-lg">
|
||||
<div class="grid grid-cols-[1fr_auto] gap-y-5 items-center px-6">
|
||||
<div class="grid grid-cols-[1fr_auto] w-full 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">
|
||||
@@ -1291,8 +1367,7 @@
|
||||
<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.
|
||||
Disables some animations in the Launcher to improve performance on low-end devices.
|
||||
</div>
|
||||
</div>
|
||||
<Checkbox
|
||||
@@ -1343,7 +1418,7 @@
|
||||
></Checkbox>
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-cols-[0.7fr_auto] gap-y-1 items-center border-theme-800 pl-6 pr-5 pb-4"
|
||||
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>
|
||||
@@ -1366,9 +1441,7 @@
|
||||
|
||||
<div class="flex flex-col">
|
||||
<Label class="text-sm" for="setting-custom-cursor">patcher release stream</Label>
|
||||
<div class="text-muted-foreground text-xs">
|
||||
test different versions of the patcher
|
||||
</div>
|
||||
<div class="text-muted-foreground text-xs">test different versions of the patcher</div>
|
||||
</div>
|
||||
<div class="flex flex-row w-full">
|
||||
<Select.Root
|
||||
@@ -1420,17 +1493,19 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if selectedView === 'login'}
|
||||
<div
|
||||
class="h-[100vh] w-full flex flex-col items-center justify-center"
|
||||
in:fly={{ duration: 400, delay: 400, y: 5, opacity: 0 }}
|
||||
out:fly={{ duration: 400, y: -5, opacity: 0 }}
|
||||
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 }}
|
||||
>
|
||||
<div class="p-8 px-52 w-full">
|
||||
<form onsubmit={performLogin} class="bg-black/40 backdrop-blur-sm rounded-lg p-8">
|
||||
<div class="flex flex-col items-center justify-center mb-4">
|
||||
<form onsubmit={performLogin} class="w-96">
|
||||
<div class="flex flex-col items-center justify-center mb-8">
|
||||
<span class="text-xl font-semibold">Login to EZPPFarm</span>
|
||||
<span class="text-xs text-muted-foreground"
|
||||
>Use your EZPPFarm account to login to EZPPLauncher</span
|
||||
@@ -1469,7 +1544,102 @@
|
||||
</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 class="flex-1 min-w-0 px-5 py-3.5">
|
||||
<p class="text-sm text-foreground/90 truncate font-medium line-clamp-1">
|
||||
{skin.name}
|
||||
</p>
|
||||
<div class="flex items-center gap-4 mt-1">
|
||||
<span class="text-[11px] text-muted-foreground">
|
||||
{skin.author || 'Unknown'}
|
||||
</span>
|
||||
<span class="text-[11px] text-muted-foreground/50">
|
||||
{new Date(skin.modified * 1000).toLocaleDateString(undefined, {
|
||||
year: '2-digit',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center gap-1.5 pr-3 opacity-0 group-hover:opacity-100 transition-opacity duration-150"
|
||||
>
|
||||
{#if $currentSkin !== skin.name}
|
||||
<button
|
||||
transition:fade={{
|
||||
duration: 150,
|
||||
}}
|
||||
class="p-2 rounded-md text-muted-foreground hover:text-primary hover:bg-primary/10 transition-colors"
|
||||
onclick={async () => {
|
||||
if (skin.name === $currentSkin) return;
|
||||
await setUserConfigValues($osuInstallationPath, [
|
||||
{ key: 'Skin', value: skin.name },
|
||||
]);
|
||||
currentSkin.set(skin.name);
|
||||
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>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.custom-scrollbox {
|
||||
scrollbar-color: #999 transparent;
|
||||
scrollbar-width: 0px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
osuBuild,
|
||||
osuStream,
|
||||
skins,
|
||||
skinsCount,
|
||||
} from '@/global';
|
||||
import {
|
||||
cursorSmoothness,
|
||||
@@ -34,7 +35,7 @@
|
||||
getEZPPLauncherStreams,
|
||||
getReleaseStream,
|
||||
getSkin,
|
||||
getSkinsCount,
|
||||
getSkins,
|
||||
getVersion,
|
||||
isValidOsuFolder,
|
||||
} from '@/osuUtil';
|
||||
@@ -151,8 +152,15 @@
|
||||
if (beatmapSetCount !== null) beatmapSets.set(beatmapSetCount);
|
||||
|
||||
currentLoadingInfo.set('Counting skins...');
|
||||
const skinCount = await getSkinsCount($osuInstallationPath);
|
||||
if (skinCount !== null) skins.set(skinCount);
|
||||
const skins_list = await getSkins($osuInstallationPath);
|
||||
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);
|
||||
currentSkin.set(skin);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { animate } from 'animejs';
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { Check, CheckCircle, CircleOff, Settings2 } from 'lucide-svelte';
|
||||
import { Check, CircleCheckBig, CircleOff, LoaderCircle } from 'lucide-svelte';
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import Checkbox from '@/components/ui/checkbox/checkbox.svelte';
|
||||
import {
|
||||
@@ -17,7 +17,18 @@
|
||||
userSettings,
|
||||
} from '@/userSettings';
|
||||
import Label from '@/components/ui/label/label.svelte';
|
||||
import { beatmapSets, currentSkin, currentView, osuBuild, osuStream, skins } from '@/global';
|
||||
import {
|
||||
beatmapSets,
|
||||
currentSkin,
|
||||
currentView,
|
||||
discordPresence,
|
||||
osuBuild,
|
||||
osuStream,
|
||||
platform,
|
||||
presenceLoading,
|
||||
skins,
|
||||
skinsCount,
|
||||
} from '@/global';
|
||||
import Launch from './Launch.svelte';
|
||||
import Confetti from 'svelte-confetti';
|
||||
import {
|
||||
@@ -25,7 +36,7 @@
|
||||
getBeatmapSetsCount,
|
||||
getReleaseStream,
|
||||
getSkin,
|
||||
getSkinsCount,
|
||||
getSkins,
|
||||
getVersion,
|
||||
isValidOsuFolder,
|
||||
} from '@/osuUtil';
|
||||
@@ -92,9 +103,12 @@
|
||||
beatmapSets.set(beatmapSetCount);
|
||||
}
|
||||
|
||||
const skinsCount: number | null = await getSkinsCount(osuInstallPath);
|
||||
if (skinsCount) {
|
||||
skins.set(skinsCount);
|
||||
const skins_list = await getSkins(osuInstallPath);
|
||||
if (skins_list) {
|
||||
skins.set(skins_list);
|
||||
skinsCount.set(skins_list.length);
|
||||
} else {
|
||||
skinsCount.set(0);
|
||||
}
|
||||
|
||||
const skin: string = await getSkin(osuInstallPath);
|
||||
@@ -130,14 +144,16 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid grid-cols-[0.41fr_1fr] h-[100vh] z-50">
|
||||
<div class="w-full h-full border-r border-theme-800 bg-black/20 backdrop-blur-md flex flex-col gap-6 p-3 z-50">
|
||||
<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"
|
||||
>
|
||||
{#each steps as step, i (step)}
|
||||
<div
|
||||
class="flex flex-row items-center gap-2 border {selectedStep === i + 1
|
||||
? 'border-primary-800 bg-primary-900'
|
||||
? 'border-primary-600/30 bg-primary-700/30'
|
||||
: selectedStep > i
|
||||
? 'border-green-800 bg-green-900'
|
||||
: 'border-theme-800 bg-theme-900'} rounded-lg p-2 transition-all"
|
||||
? 'border-green-800/30 bg-green-900/30'
|
||||
: 'border-black/30 bg-black/30'} rounded-lg p-2 transition-all"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col items-center justify-center h-8 w-8 border-[2px] {selectedStep >
|
||||
@@ -164,7 +180,7 @@
|
||||
<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/20 rounded-lg border border-theme-900/20 p-6 mt-5 flex flex-col items-center justify-center"
|
||||
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 }}
|
||||
>
|
||||
<img
|
||||
@@ -179,13 +195,13 @@
|
||||
<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-500 p-4 rounded-lg mt-4">
|
||||
<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-theme-800 rounded-lg border border-theme-900 p-6 mt-5 flex flex-col items-center justify-center"
|
||||
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>
|
||||
@@ -210,7 +226,7 @@
|
||||
<div
|
||||
class="flex flex-row gap-3 bg-green-800 border border-green-900 text-green-500 p-4 rounded-lg mt-4"
|
||||
>
|
||||
<CheckCircle />
|
||||
<CircleCheckBig />
|
||||
<span>Auto-detected osu! installation path! Please check if its correct!</span>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -226,7 +242,7 @@
|
||||
<div
|
||||
class="flex flex-row gap-3 bg-green-800 border border-green-900 text-green-500 p-4 rounded-lg mt-4"
|
||||
>
|
||||
<CheckCircle />
|
||||
<CircleCheckBig />
|
||||
<span>Selected osu! installation path is valid!</span>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -242,23 +258,26 @@
|
||||
{/if}
|
||||
</div>
|
||||
{:else if selectedStep === 3}
|
||||
<div class="h-full w-full flex items-center justify-center">
|
||||
<div
|
||||
class="bg-theme-900 flex flex-col justify-center gap-3 mt-5 border border-theme-800 rounded-lg"
|
||||
class="bg-black/40 backdrop-blur-sm py-8 px-8 rounded-lg"
|
||||
in:fade={{ duration: $reduceAnimations ? 0 : 200 }}
|
||||
>
|
||||
<div class="flex flex-row items-center gap-3 font-semibold text-xl px-3 pt-3">
|
||||
<Settings2 /> EZPPLauncher Settings
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-cols-[1fr_auto] gap-y-5 items-center border-t border-theme-800 py-3 px-6"
|
||||
>
|
||||
<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-custom-cursor">Patching</Label>
|
||||
<div class="text-muted-foreground text-xs">Shows misses in Relax and Autopilot</div>
|
||||
<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-custom-cursor"
|
||||
checked={$patch}
|
||||
id="setting-patch"
|
||||
checked={$platform === 'windows' ? $patch : false}
|
||||
disabled={$platform !== 'windows'}
|
||||
onCheckedChange={async (e) => {
|
||||
patch.set(e);
|
||||
$userSettings.save();
|
||||
@@ -280,6 +299,8 @@
|
||||
cursorSmoothening.set(false);
|
||||
}
|
||||
customCursor.set(e);
|
||||
|
||||
$userSettings.save();
|
||||
}}
|
||||
class="flex items-center justify-center w-5 h-5"
|
||||
></Checkbox>
|
||||
@@ -296,26 +317,49 @@
|
||||
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-cursor-smoothening">Reduce Animations</Label>
|
||||
<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.
|
||||
Disables some animations in the Launcher to improve performance on low-end
|
||||
devices.
|
||||
</div>
|
||||
</div>
|
||||
<Checkbox
|
||||
id="setting-cursor-smoothening"
|
||||
id="setting-reduce-animations"
|
||||
checked={$reduceAnimations}
|
||||
onCheckedChange={async (e) => {
|
||||
reduceAnimations.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-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>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user