chore: add custom themes

Co-authored-by: Copilot <[email protected]>
This commit is contained in:
HorizonCodeandCopilot committed 2026-05-02 00:38:56 +02:00
1 parent b2e7d22d28
commit 8fd56ae749
15 files changed
+510 -164

No files matched your search

@@ -74,12 +74,18 @@
<div class="wrapper" bind:this={wrapper}>
<!-- Scrollable content -->
<div class={`content ${className}`} bind:this={container} onscroll={updateScrollbar}>
<div
class={`content ${className}`}
bind:this={container}
onscroll={updateScrollbar}
id="scrollable-content"
>
{@render children?.()}
</div>
<!-- Custom scrollbar -->
<div class="scrollbar" style="top: {topOffset}px;">
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="thumb"
bind:this={thumb}
@@ -87,6 +93,7 @@
onpointermove={drag}
onpointerup={endDrag}
onpointercancel={endDrag}
aria-controls="scrollable-content"
style="height:{thumbHeight}px; transform:translateY({thumbTop}px);"
></div>
</div>
@@ -117,8 +124,8 @@
position: absolute;
right: 2px;
bottom: 0;
width: 12px;
background: var(--color-theme-950);
width: 8px;
background: transparent;
border-radius: 999px;
}
@@ -126,7 +133,7 @@
.thumb {
position: absolute;
width: 100%;
background: var(--color-theme-800);
background: var(--color-theme-700);
border-radius: 999px;
cursor: grab;
transition: background 0.15s;
+4 -4
View File
@@ -3,8 +3,7 @@ import { ezppfarm } from './api/ezpp';
import type { Component } from 'svelte';
import Loading from '../screens/Loading.svelte';
import type { Release } from './types';
import { THEMES } from './themes';
import type { Theme } from './themes';
export const currentView = writable<Component>(Loading);
export const platform = writable<string>('');
@@ -25,8 +24,9 @@ export const currentLoadingInfo = writable<string>('Initializing...');
export const firstStartup = writable<boolean>(false);
export const launching = writable<boolean>(false);
export const theme = writable<(typeof THEMES)[number]>(THEMES[0]);
export const theme_video = writable<HTMLVideoElement | undefined>(undefined);
export const custom_themes = writable<Theme[]>([]);
export const custom_theme_container = writable<HTMLElement | undefined>(undefined);
export const active_custom_theme = writable<Theme | undefined>(undefined);
export const serverPing = writable<number | undefined>(undefined);
export const serverConnectionFails = writable(0);
+257 -40
View File
@@ -1,43 +1,260 @@
import executionclap_video from '$assets/themes/execution_clap/video.webm';
import lostumbrella_video from '$assets/themes/lost_umbrella/video.webm';
import nyancat_image from '$assets/themes/nyan_cat/image.gif';
import nyancat_audio from '$assets/themes/nyan_cat/audio.mp3';
import * as path from '@tauri-apps/api/path';
import * as fs from '@tauri-apps/plugin-fs';
import { convertFileSrc } from '@tauri-apps/api/core';
import { active_custom_theme, custom_themes } from './global';
import { setGlobalVolume } from './utils';
import { betterFetch } from '@better-fetch/fetch';
import zip from 'jszip';
export const THEMES = [
{
name: 'default',
display_name: 'Default',
assets: {
background_video: undefined,
background_image: undefined,
audio: undefined,
export type Theme = {
name: string;
version: string;
author: string;
scriptUrl: string;
assets: string;
preview: string;
status: 'installed' | 'downloading' | 'extracting' | 'not-installed';
progress: number;
};
export const getDownloadableThemes = async () => {
const themes: Theme[] = [];
const downloadableThemes = await betterFetch<
{
name: string;
version: string;
author: string;
}[]
>('https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/themes.json', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
},
},
{
name: 'execution_clap',
display_name: 'Execution Clap',
assets: {
background_video: executionclap_video,
background_image: undefined,
audio: undefined,
});
if (downloadableThemes.data) {
for (const theme of downloadableThemes.data) {
themes.push({
name: theme.name,
version: theme.version,
author: theme.author,
scriptUrl: '',
assets: '',
preview: `https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/previews/${toSafeName(theme.name)}.png`,
status: 'not-installed',
progress: 0,
});
}
} else {
console.error('Failed to fetch downloadable themes');
console.error(downloadableThemes.error);
}
return themes;
};
export const getThemes = async (): Promise<Theme[]> => {
const themes: Theme[] = [];
themes.push({
author: 'EZPPFarm',
name: 'Default',
version: '1.0.0',
scriptUrl: '',
assets: '',
preview: '',
status: 'installed',
progress: 0,
});
const folderPath = await path.join(await path.homeDir(), '.ezpplauncher', 'themes');
if (await fs.exists(folderPath)) {
const themeFolders = await fs.readDir(folderPath);
for (const themeFolder of themeFolders) {
const themeFolderPath = await path.join(folderPath, themeFolder.name);
const themeConfigFilePath = await path.join(folderPath, themeFolder.name, 'theme.json');
if (await fs.exists(themeConfigFilePath)) {
const themeConfig = await fs.readTextFile(themeConfigFilePath);
const theme = JSON.parse(themeConfig) as {
name: string;
version: string;
apiVersion: string;
author: string;
entry: string;
style: string;
preview: string;
};
const scriptFile = await path.join(folderPath, themeFolder.name, theme.entry);
if (await fs.exists(scriptFile)) {
themes.push({
name: theme.name,
version: theme.version,
author: theme.author,
scriptUrl: convertFileSrc(await path.normalize(`${themeFolderPath}/${theme.entry}`)),
assets: convertFileSrc(await path.normalize(`${themeFolderPath}/assets`)),
preview: convertFileSrc(
await path.normalize(`${themeFolderPath}/assets/${theme.preview}`)
),
status: 'installed',
progress: 0,
});
}
}
}
}
return themes;
};
function combineChunks(chunks: Uint8Array[], totalLength: number): Uint8Array {
const result = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return result;
}
export const downloadTheme = async (theme: Theme): Promise<boolean> => {
if (theme.status !== 'not-installed') return false;
custom_themes.update((themes) => {
const matchedTheme = themes.find((t) => t.name === theme.name);
if (matchedTheme) {
matchedTheme.status = 'downloading';
}
return themes;
});
const downloadUrl = `https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/themes/${toSafeName(
theme.name
)}.ezpplauncher-theme`;
const downloadReq = await fetch(downloadUrl, {
method: 'GET',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
},
},
{
name: 'lost_umbrella',
display_name: 'Lost Umbrella',
assets: {
background_video: lostumbrella_video,
background_image: undefined,
audio: undefined,
},
},
{
name: 'nyan_cat',
display_name: 'Nyan Cat',
assets: {
background_video: undefined,
background_image: nyancat_image,
audio: nyancat_audio,
},
},
];
});
if (!downloadReq.ok || downloadReq.body === null) {
console.log(downloadUrl);
custom_themes.update((themes) => {
const matchedTheme = themes.find((t) => t.name === theme.name);
if (matchedTheme) {
matchedTheme.status = 'not-installed';
}
return themes;
});
return false;
}
const total = Number(downloadReq.headers.get('content-length')) || 0;
const reader = downloadReq.body.getReader();
let received = 0;
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
received += value.length;
const progress = received / total;
custom_themes.update((themes) => {
const matchedTheme = themes.find((t) => t.name === theme.name);
if (matchedTheme) {
matchedTheme.progress = progress;
}
return themes;
});
}
custom_themes.update((themes) => {
const matchedTheme = themes.find((t) => t.name === theme.name);
if (matchedTheme) {
matchedTheme.status = 'extracting';
}
return themes;
});
const baseThemeFolder = await path.join(await path.homeDir(), '.ezpplauncher', 'themes');
const themeFolder = await path.join(baseThemeFolder, toSafeName(theme.name));
if (!(await fs.exists(baseThemeFolder))) await fs.mkdir(baseThemeFolder, { recursive: true });
if (!(await fs.exists(themeFolder))) await fs.mkdir(themeFolder, { recursive: true });
const zipFile = await zip.loadAsync(combineChunks(chunks, total));
const totalFiles = Object.keys(zipFile.files).length;
let extractedFiles = 0;
for (const zipEntry of Object.keys(zipFile.files)) {
const file = zipFile.file(zipEntry);
if (file) {
try {
const fileData = await file.async('uint8array');
const filePath = await path.join(themeFolder, zipEntry);
const dirPath = await path.dirname(filePath);
if (!(await fs.exists(dirPath))) await fs.mkdir(dirPath, { recursive: true });
await new Promise((res) => setTimeout(res, 250));
await fs.writeFile(filePath, fileData);
extractedFiles++;
custom_themes.update((themes) => {
const matchedTheme = themes.find((t) => t.name === theme.name);
if (matchedTheme) {
matchedTheme.progress = extractedFiles / totalFiles;
}
return themes;
});
} catch (err) {
console.log(err);
}
}
}
const themeConfig = await fs.readTextFile(await path.join(themeFolder, 'theme.json'));
const themeConfigObj = JSON.parse(themeConfig) as {
name: string;
version: string;
apiVersion: string;
author: string;
entry: string;
style: string;
preview: string;
};
const themeScript = convertFileSrc(
await path.normalize(`${themeFolder}/${themeConfigObj.entry}`)
);
const themeAssets = convertFileSrc(await path.normalize(`${themeFolder}/assets`));
const themePreview = convertFileSrc(
await path.normalize(`${themeFolder}/assets/${themeConfigObj.preview}`)
);
custom_themes.update((themes) => {
const matchedTheme = themes.find((t) => t.name === theme.name);
if (matchedTheme) {
matchedTheme.scriptUrl = themeScript;
matchedTheme.assets = themeAssets;
matchedTheme.preview = themePreview;
matchedTheme.name = themeConfigObj.name;
matchedTheme.version = themeConfigObj.version;
matchedTheme.author = themeConfigObj.author;
matchedTheme.status = 'installed';
}
return themes;
});
return true;
};
export const loadTheme = async (theme: Theme, themeContainer: HTMLElement) => {
active_custom_theme.set(theme);
themeContainer.innerHTML = '';
if (theme.scriptUrl.length > 0) {
const themeScript = await import(theme.scriptUrl);
/* @vite-ignore */
themeScript.mountTheme(themeContainer, { assets: theme.assets });
}
setGlobalVolume(0.15);
};
export const toSafeName = (name: string) => {
return name
.toLowerCase()
.replace(/[^a-z0-9]/g, '_')
.replace(/-+/g, '_');
};
+45
View File
@@ -99,3 +99,48 @@ export const urlIsValidImage = async (url: string) => {
return false;
}
};
export function toURL(data: Buffer, type: string) {
return URL.createObjectURL(new Blob([new Uint8Array(data)], { type }));
}
export function setGlobalVolume(volume: number) {
const audioElements = document.querySelectorAll('audio');
audioElements.forEach((audio) => {
audio.volume = volume;
});
const videoElements = document.querySelectorAll('video');
videoElements.forEach((video) => {
video.volume = volume;
});
}
export function fadeGlobalVolume(volumeFrom: number, volumeTo: number, duration: number) {
const audioElements = document.querySelectorAll('audio');
const videoElements = document.querySelectorAll('video');
const startTime = performance.now();
function animate() {
const currentTime = performance.now();
const elapsedTime = currentTime - startTime;
const progress = Math.min(elapsedTime / duration, 1);
const interpolatedVolume = volumeFrom + (volumeTo - volumeFrom) * progress;
audioElements.forEach((audio) => {
audio.volume = interpolatedVolume;
});
videoElements.forEach((video) => {
video.volume = interpolatedVolume;
});
if (progress < 1) {
requestAnimationFrame(animate);
}
}
animate();
}