Compare commits

...

4 Commits

8 changed files with 127 additions and 21 deletions
+1
View File
@@ -1,5 +1,6 @@
{
"svelte.enable-ts-plugin": true,
"git.enableCommitSigning": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
+7
View File
@@ -22,6 +22,7 @@ It enhances the osu! experience with quality-of-life features and integration sp
- [x] Displays ping to the EZPPFarm server
- [x] Discord Rich Presence
- [x] Performance Display Overlay (currently in experimental patcher release stream)
- [x] Launcher Theming
---
@@ -73,3 +74,9 @@ src-tauri/target/release/ezpplauncher.exe
├── package.json # Bun project config
└── README.md
```
---
## 🎨 Launcher Themes
By default, the launcher includes several downloadable themes. If you want to create your own custom theme, please use the [create-ezpplauncher-theme](https://git.ez-pp.farm/EZPPFarm/create-ezpplauncher-theme) repository for a template and instructions.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 157 KiB

+76 -12
View File
@@ -2,7 +2,7 @@ import * as path from '@tauri-apps/api/path';
import * as fs from '@tauri-apps/plugin-fs';
import { convertFileSrc } from '@tauri-apps/api/core';
import { active_custom_theme, custom_themes } from './global';
import { setGlobalVolume } from './utils';
import { calculateGitBlobSha, setGlobalVolume } from './utils';
import { betterFetch } from '@better-fetch/fetch';
import zip from 'jszip';
import { SemVer } from 'semver';
@@ -15,7 +15,7 @@ export type Theme = {
scriptUrl: string;
assets: string;
preview: string;
status: 'installed' | 'downloading' | 'extracting' | 'not-installed';
status: 'installed' | 'downloading' | 'extracting' | 'deleting' | 'not-installed';
progress: number;
updateAvailable: boolean;
};
@@ -126,8 +126,11 @@ function combineChunks(chunks: Uint8Array[], totalLength: number): Uint8Array {
return result;
}
export const downloadTheme = async (theme: Theme, force = false): Promise<boolean> => {
if (theme.status !== 'not-installed' && !force) return false;
export const downloadTheme = async (
theme: Theme,
force = false
): Promise<{ success: boolean; error?: string }> => {
if (theme.status !== 'not-installed' && !force) return { success: false };
custom_themes.update((themes) => {
const matchedTheme = themes.find((t) => t.name === theme.name);
@@ -136,9 +139,29 @@ export const downloadTheme = async (theme: Theme, force = false): Promise<boolea
}
return themes;
});
const downloadUrl = `https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/themes/${toSafeName(
theme.name
)}.ezpplauncher-theme`;
const themeFileName = `${toSafeName(theme.name)}.ezpplauncher-theme`;
const themeContents = await betterFetch<{
file_contents: {
sha: string;
download_url: string;
};
}>(
`https://git.ez-pp.farm/api/v1/repos/EZPPFarm/EZPPLauncher-Themes/contents-ext/themes/${themeFileName}?ref=main`
);
if (themeContents.error) {
custom_themes.update((themes) => {
const matchedTheme = themes.find((t) => t.name === theme.name);
if (matchedTheme) {
matchedTheme.status = 'not-installed';
}
return themes;
});
return {
success: false,
error: themeContents.error.message ?? 'An unknown error occurred',
};
}
const downloadUrl = `https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/themes/${themeFileName}`;
const downloadReq = await fetch(downloadUrl, {
method: 'GET',
headers: {
@@ -148,7 +171,6 @@ export const downloadTheme = async (theme: Theme, force = false): Promise<boolea
});
if (!downloadReq.ok || downloadReq.body === null) {
console.log(downloadUrl);
custom_themes.update((themes) => {
const matchedTheme = themes.find((t) => t.name === theme.name);
if (matchedTheme) {
@@ -156,7 +178,10 @@ export const downloadTheme = async (theme: Theme, force = false): Promise<boolea
}
return themes;
});
return false;
return {
success: false,
error: 'Failed to download theme file.',
};
}
const total = Number(downloadReq.headers.get('content-length')) || 0;
const reader = downloadReq.body.getReader();
@@ -178,6 +203,25 @@ export const downloadTheme = async (theme: Theme, force = false): Promise<boolea
});
}
const fileBuffer = combineChunks(chunks, total);
const sha = calculateGitBlobSha(Buffer.from(fileBuffer));
if (sha !== themeContents.data.file_contents.sha) {
custom_themes.update((themes) => {
const matchedTheme = themes.find((t) => t.name === theme.name);
if (matchedTheme) {
matchedTheme.status = 'not-installed';
}
return themes;
});
return {
success: false,
error: 'Failed to verify downloaded theme file hash.',
};
}
//TODO: check file hash
custom_themes.update((themes) => {
const matchedTheme = themes.find((t) => t.name === theme.name);
if (matchedTheme) {
@@ -190,7 +234,7 @@ export const downloadTheme = async (theme: Theme, force = false): Promise<boolea
if (!(await fs.exists(baseThemeFolder))) await fs.mkdir(baseThemeFolder, { recursive: true });
if (!(await fs.exists(themeFolder))) await fs.mkdir(themeFolder, { recursive: true });
const zipFile = await zip.loadAsync(combineChunks(chunks, total));
const zipFile = await zip.loadAsync(fileBuffer);
const totalFiles = Object.keys(zipFile.files).length;
let extractedFiles = 0;
for (const zipEntry of Object.keys(zipFile.files)) {
@@ -213,6 +257,17 @@ export const downloadTheme = async (theme: Theme, force = false): Promise<boolea
});
} catch (err) {
console.log(err);
custom_themes.update((themes) => {
const matchedTheme = themes.find((t) => t.name === theme.name);
if (matchedTheme) {
matchedTheme.status = 'not-installed';
}
return themes;
});
return {
success: false,
error: 'Failed to extract theme file.',
};
}
}
}
@@ -270,15 +325,17 @@ export const downloadTheme = async (theme: Theme, force = false): Promise<boolea
return a.name.localeCompare(b.name);
});
});
return true;
return {
success: true,
};
};
export const loadTheme = async (theme: Theme, themeContainer: HTMLElement, volume = 0.15) => {
active_custom_theme.set(theme);
themeContainer.innerHTML = '';
if (theme.scriptUrl.length > 0) {
const themeScript = await import(theme.scriptUrl);
/* @vite-ignore */
const themeScript = await import(theme.scriptUrl);
themeScript.mountTheme(themeContainer, { assets: theme.assets });
}
setGlobalVolume(volume);
@@ -290,6 +347,13 @@ export const deleteTheme = async (themeToUninstall: Theme) => {
if (!(await fs.exists(baseThemeFolder))) return false;
const themeFolder = await path.join(baseThemeFolder, themeToUninstall.folder_name);
if (!(await fs.exists(themeFolder))) return false;
custom_themes.update((themes) => {
const matchedTheme = themes.find((t) => t.name === themeToUninstall.name);
if (matchedTheme) {
matchedTheme.status = 'deleting';
}
return themes;
});
await fs.remove(themeFolder, { recursive: true });
const downloadableThemes = await getDownloadableThemes();
custom_themes.update((themes) => {
+8
View File
@@ -2,6 +2,7 @@ import { invoke } from '@tauri-apps/api/core';
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { custom_theme_audio_playing } from './global';
import * as crypto from 'crypto-js';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
@@ -201,3 +202,10 @@ export function trackMediaContainer(node: HTMLElement) {
},
};
}
export function calculateGitBlobSha(fileBuffer: Buffer): string {
const header = Buffer.from(`blob ${fileBuffer.length}\0`);
const store = Buffer.concat([header, fileBuffer]);
return crypto.SHA1(crypto.lib.WordArray.create(store)).toString(crypto.enc.Hex);
}
+4 -1
View File
@@ -110,7 +110,6 @@
try {
const themeVolume = $userSettings.value('volume').get(0.15);
console.log(themeVolume);
custom_theme_volume.set(themeVolume);
currentLoadingInfo.set('Loading themes...');
const downloadableThemes = await getDownloadableThemes();
@@ -144,6 +143,10 @@
sileo.error({
title: 'An error occured!',
description: 'Failed to load theme',
fill: '#181825',
styles: {
description: 'text-center!',
},
});
}
+31 -7
View File
@@ -1566,7 +1566,8 @@
disabled={($active_custom_theme &&
$active_custom_theme.folder_name === theme.folder_name) ||
theme.status === 'downloading' ||
theme.status === 'extracting'}
theme.status === 'extracting' ||
theme.status === 'deleting'}
onclick={async () => {
if (theme.status === 'installed') {
if ($custom_theme_container) {
@@ -1580,10 +1581,26 @@
});
}
} else {
if (!(await downloadTheme(theme))) {
sileo.error({
title: 'Uhhm..',
description: 'Failed to download theme.',
const downloadThemeResult = await downloadTheme(theme);
if (!downloadThemeResult.success) {
if (downloadThemeResult.error && downloadThemeResult.error.length > 0) {
sileo.error({
title: 'Uhhm..',
description: downloadThemeResult.error,
fill: '#181825',
styles: {
description: 'text-center!',
},
});
}
} else {
sileo.success({
title: 'Yaay!',
description: 'Theme installed successfully!',
fill: '#181825',
styles: {
description: 'text-center!',
},
});
}
}
@@ -1598,6 +1615,8 @@
)}%)
{:else if theme.status === 'extracting'}
Extracting Theme... ({Math.round(theme.progress * 100)}%)
{:else if theme.status === 'deleting'}
Uninstalling Theme...
{:else}
Download Theme
{/if}
@@ -1629,14 +1648,19 @@
<CloudDownload />
</Button>
{/if}
{#if theme.status === 'installed' && $active_custom_theme && $active_custom_theme.folder_name !== theme.folder_name && theme.name !== 'Default'}
{#if (theme.status === 'installed' || theme.status === 'deleting') && $active_custom_theme && $active_custom_theme.folder_name !== theme.folder_name && theme.name !== 'Default'}
<Button
class="min-w-[40px]"
size="icon"
variant="destructive"
disabled={theme.status !== 'installed'}
onclick={() => deleteTheme(theme)}
>
<Trash />
{#if theme.status === 'deleting'}
<LoaderCircle class="animate-spin" />
{:else}
<Trash />
{/if}
</Button>
{/if}
</div>
-1
View File
@@ -124,7 +124,6 @@
onMount(async () => {
const osuPath = await autoDetectOsuInstallFolder();
console.log(osuPath);
if (osuPath) {
osuInstallPath = osuPath;
autoDetectedOsuPath = true;