This commit is contained in:
lukas
2026-03-24 16:21:37 +01:00
commit a221a18436
27 changed files with 4343 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};
+11
View File
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 871 KiB

+1
View File
@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.
+21
View File
@@ -0,0 +1,21 @@
import PQueue from 'p-queue';
export const normalApiQueue = new PQueue({
interval: 1000,
intervalCap: 1,
carryoverIntervalCount: true
});
export const skinApiQueue = new PQueue({
interval: 1000,
intervalCap: 40,
carryoverIntervalCount: true
});
normalApiQueue.on('add', () => {
console.log(`Task added. Queue size: ${normalApiQueue.size} Pending: ${normalApiQueue.pending}`);
});
skinApiQueue.on('add', () => {
console.log(`Task added. Queue size: ${skinApiQueue.size} Pending: ${skinApiQueue.pending}`);
});
+89
View File
@@ -0,0 +1,89 @@
<script lang="ts">
import './layout.css';
import { ModeWatcher, toggleMode } from 'mode-watcher';
let { children } = $props();
</script>
<ModeWatcher />
<div
class="relative flex min-h-screen flex-col bg-gray-100 text-gray-900 transition-colors duration-200 dark:bg-gray-900 dark:text-gray-100"
>
<div class="absolute top-4 right-4 z-50">
<button
onclick={toggleMode}
class="flex h-10 w-10 items-center justify-center rounded-lg border border-gray-300 bg-white text-gray-700 shadow-sm transition-colors hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
aria-label="Toggle Dark Mode"
>
<svg
class="block dark:hidden"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2" />
<path d="M12 20v2" />
<path d="m4.93 4.93 1.41 1.41" />
<path d="m17.66 17.66 1.41 1.41" />
<path d="M2 12h2" />
<path d="M20 12h2" />
<path d="m6.34 17.66-1.41 1.41" />
<path d="m19.07 4.93-1.41 1.41" />
</svg>
<svg
class="hidden dark:block"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
</svg>
</button>
</div>
<main class="flex-1 pt-16">
{@render children()}
</main>
<footer
class="border-t border-gray-300 bg-white p-6 text-center transition-colors duration-200 dark:border-gray-800 dark:bg-gray-900"
>
<div
class="flex items-center justify-center gap-4 text-sm font-medium text-gray-600 dark:text-gray-400"
>
<a
href="https://github.com/your-username"
target="_blank"
class="hover:text-blue-600 dark:hover:text-blue-400"
>
GitHub
</a>
<span>&bull;</span>
<a
href="https://yourwebsite.com"
target="_blank"
class="hover:text-blue-600 dark:hover:text-blue-400"
>
My Website
</a>
</div>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-500">
&copy; {new Date().getFullYear()} Lukasabbe. All rights reserved.
</p>
</footer>
</div>
+306
View File
@@ -0,0 +1,306 @@
<script lang="ts">
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import JSZip from 'jszip';
import skins from '$lib/assets/skins.png';
let isLoading = $state(false);
let names = $state<string[]>([]);
let currentInput = $state('');
let errorMessage = $state<string | null>(null);
type MCData = {
success: boolean;
data: {
MinecraftUUID: string;
MinecraftUsername: string;
MinecraftSkinData: {
timestamp: number;
UUID: string;
username: string;
skinUrl: string;
model: string;
};
}[];
};
function handleKeydown(event: KeyboardEvent) {
if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault();
const trimmedName = currentInput.trim();
if (trimmedName && !names.includes(trimmedName)) {
names = [...names, trimmedName];
currentInput = '';
}
} else if (event.key === 'Backspace' && currentInput === '') {
names = names.slice(0, -1);
}
}
function removeName(indexToRemove: number) {
names = names.filter((_, index) => index !== indexToRemove);
}
async function handleSubmit(event: Event) {
event.preventDefault();
errorMessage = null;
const pendingName = currentInput.trim();
let finalNames = [...names];
if (pendingName && !finalNames.includes(pendingName)) {
finalNames.push(pendingName);
names = finalNames;
currentInput = '';
}
if (finalNames.length === 0) {
errorMessage = 'Please enter at least one username.';
return;
}
isLoading = true;
try {
const response = await fetch('/api/get-skins', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ usernames: finalNames })
});
const result = (await response.json()) as MCData;
if (result.success) {
const zipBlob = await generateZip(result);
if (zipBlob) {
saveAs(zipBlob, 'skinpack.zip');
}
names = [];
} else {
errorMessage = 'Something went wrong.';
}
} catch (error) {
console.error(error);
errorMessage = 'Failed to connect to the server.';
} finally {
isLoading = false;
}
}
async function getImageDimensions(blob: Blob) {
return new Promise<{ width: number; height: number }>((resolve, reject) => {
const img = new Image();
const objectUrl = URL.createObjectURL(blob);
img.onload = () => {
URL.revokeObjectURL(objectUrl);
resolve({ width: img.width, height: img.height });
};
img.onerror = () => {
URL.revokeObjectURL(objectUrl);
reject(new Error('Failed to load image to get dimensions'));
};
img.src = objectUrl;
});
}
async function generateZip(data: MCData) {
const zip = new JSZip();
const packMcmeta = {
pack: {
pack_format: 75,
min_format: 75,
max_format: 75,
description: 'Skin pack\nMade by Lukasabbe'
}
};
zip.file('pack.mcmeta', JSON.stringify(packMcmeta, null, 2));
const itemsFolder = zip.folder('assets/minecraft/items');
const modelsFolder = zip.folder('assets/trusted_skin_pack/models/item');
const texturesFolder = zip.folder('assets/trusted_skin_pack/textures/item');
if (!texturesFolder || !modelsFolder || !itemsFolder) return;
let carved_pumpkin_item_obj = {
model: {
type: 'minecraft:select',
property: 'minecraft:component',
component: 'minecraft:custom_name',
cases: {},
fallback: {
type: 'minecraft:model',
model: 'minecraft:block/carved_pumpkin'
}
}
};
let cases: { when: string; model: { type: string; model: string } }[] = [];
for (const profile of data.data) {
const image = await fetch(profile.MinecraftSkinData.skinUrl);
const imageBlob = await image.blob();
texturesFolder.file(`${profile.MinecraftUsername}.png`, imageBlob);
const { height } = await getImageDimensions(imageBlob);
let modelRes;
if (height == 32) {
modelRes = await fetch('/old.json');
} else if (profile.MinecraftSkinData.model == 'SLIM') {
modelRes = await fetch('/slim.json');
} else if (profile.MinecraftSkinData.model == 'CLASSIC') {
modelRes = await fetch('/normal.json');
} else return;
const model = await modelRes.json();
model.textures['0'] = `trusted_skin_pack:item/${profile.MinecraftUsername}`;
model.textures.particle = `trusted_skin_pack:item/${profile.MinecraftUsername}`;
modelsFolder.file(`${profile.MinecraftUsername}.json`, JSON.stringify(model, null, 2));
cases.push({
when: profile.MinecraftUsername.toLowerCase(),
model: {
type: 'minecraft:model',
model: `trusted_skin_pack:item/${profile.MinecraftUsername}`
}
});
}
carved_pumpkin_item_obj.model.cases = cases;
itemsFolder.file('carved_pumpkin.json', JSON.stringify(carved_pumpkin_item_obj, null, 2));
return new Promise<Blob>((resolve, reject) => {
zip
.generateAsync({ type: 'blob' })
.then((content) => {
resolve(content);
})
.catch(reject);
});
}
</script>
<div class="flex flex-col items-center px-4 py-8">
<div
class="mb-8 w-full max-w-2xl rounded-xl border border-gray-200 bg-white p-8 shadow-lg dark:border-gray-700 dark:bg-gray-800"
>
<h2 class="mb-4 text-xl font-bold text-gray-800 dark:text-white">
Skin Pack Generator - 1.21.11
</h2>
<div class="flex flex-col gap-6">
<div class="space-y-3 text-sm text-gray-600 dark:text-gray-300">
<p>
Welcome to the Minecraft Skin Pack Generator! This tool allows you generate a resource
pack with dynamic skins.
</p>
<p>
Simply type in the usernames of the players below, press Space or Enter to add them, and
click "Download Pack".
</p>
<h2 class="text-m font-bold text-gray-800 dark:text-white">How to use</h2>
<ul>
<li>1. Put the zip file in your Minecraft resource packs folder.</li>
<li>2. Rename a carved pumkin to a skin you inputed when you generated the pack.</li>
<li>3. The pumkin will get the model of the skin you inputed.</li>
</ul>
</div>
<div
class="flex aspect-video w-full items-center justify-center overflow-hidden rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 dark:border-gray-600 dark:bg-gray-700/50"
>
<img src={skins} alt="Skins" class="h-full w-full object-cover" />
</div>
</div>
</div>
<div
class="w-full max-w-2xl rounded-xl border border-gray-200 bg-white p-8 shadow-lg dark:border-gray-700 dark:bg-gray-800"
>
<h1 class="mb-6 text-2xl font-bold text-gray-800 dark:text-white">Minecraft Skin Generator</h1>
<form onsubmit={handleSubmit} class="space-y-4">
<div>
<label
for="nameInput"
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Enter Usernames (Press Space or Enter to add)
</label>
<div
class="flex min-h-15 w-full flex-wrap items-start gap-2 rounded-lg border border-gray-300 bg-white p-2 transition-colors focus-within:border-blue-500 focus-within:ring-2 focus-within:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 {isLoading
? 'cursor-not-allowed bg-gray-100 dark:bg-gray-600'
: ''}"
>
{#each names as name, index (name)}
<span
class="flex items-center gap-1 rounded-md bg-blue-100 px-3 py-1 text-sm font-medium text-blue-800 dark:bg-blue-900/50 dark:text-blue-200"
>
{name}
<button
type="button"
class="flex h-5 w-5 items-center justify-center rounded-full hover:bg-blue-200 hover:text-blue-900 focus:outline-none dark:hover:bg-blue-800 dark:hover:text-blue-100"
onclick={() => removeName(index)}
disabled={isLoading}
aria-label="Remove {name}"
>
&times;
</button>
</span>
{/each}
<input
id="nameInput"
type="text"
bind:value={currentInput}
onkeydown={handleKeydown}
disabled={isLoading}
placeholder={names.length === 0 ? 'e.g. Notch, Jeb_...' : ''}
class="mt-1 min-w-30 flex-1 bg-transparent py-1 text-sm text-gray-900 outline-none placeholder:text-gray-400 disabled:cursor-not-allowed dark:text-white dark:placeholder:text-gray-400"
/>
</div>
</div>
<button
type="submit"
disabled={isLoading}
class="flex w-full items-center justify-center rounded-lg bg-blue-600 px-4 py-2 font-semibold text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-blue-400 dark:hover:bg-blue-500 dark:disabled:bg-blue-800"
>
{#if isLoading}
<svg
class="mr-3 h-5 w-5 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Fetching Skins...
{:else}
Download Pack
{/if}
</button>
</form>
{#if errorMessage}
<div
class="mt-6 rounded-lg border border-red-200 bg-red-50 p-4 text-red-800 dark:border-red-900/50 dark:bg-red-900/20 dark:text-red-400"
>
<p>{errorMessage}</p>
</div>
{/if}
</div>
</div>
+50
View File
@@ -0,0 +1,50 @@
import { json } from '@sveltejs/kit';
import { normalApiQueue, skinApiQueue } from '$lib/server/apiQueue';
import { getProfilesFromUsernames, MinecraftProfile } from 'minecraft-api-wrapper';
export async function POST({ request }) {
try {
const payload = await request.json();
const usernames = chunkArray<string>(payload.usernames);
const promises: Promise<MinecraftProfile[] | null>[] = [];
for (const chunk of usernames) {
promises.push(
normalApiQueue.add(async () => {
const profiles = await getProfilesFromUsernames(chunk);
if (!profiles) return null;
for (const profile of profiles) {
await skinApiQueue.add(async () => {
await profile.getSkinUrl();
});
}
return profiles;
})
);
}
const results = await Promise.all(promises);
const filteredResults = results.filter(
(result): result is MinecraftProfile[] => result !== null
);
const allProfiles = filteredResults.flat();
return json({ success: true, data: allProfiles });
} catch (error) {
console.error('Queue/API Error:', error);
return json({ success: false, message: 'Failed to fetch data' }, { status: 500 });
}
}
function chunkArray<T>(list: T[]): T[][] {
const chunkSize = 10;
const chunkedList: T[][] = [];
for (let i = 0; i < list.length; i += chunkSize) {
// .slice() automatically handles cases where there are fewer than 10 elements left
chunkedList.push(list.slice(i, i + chunkSize));
}
return chunkedList;
}
+2
View File
@@ -0,0 +1,2 @@
@import 'tailwindcss';
@custom-variant dark (&:where(.dark, .dark *));