Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 44 additions & 43 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,45 +1,46 @@
{
"name": "mcutils",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-cloudflare": "^4.1.0",
"@sveltejs/kit": "^2.5.2",
"@types/figlet": "^1.5.6",
"@types/javascript-color-gradient": "^2.4.0",
"@types/node": "^18.15.10",
"@zerodevx/svelte-toast": "^0.9.3",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.21",
"svelte": "^4.2.12",
"svelte-awesome-color-picker": "^2.4.5",
"svelte-awesome-slider": "^1.1.0",
"svelte-check": "^3.0.1",
"svelte-multiselect": "^10.1.0",
"tailwindcss": "^3.2.7",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^5.1.5"
},
"type": "module",
"dependencies": {
"carbon-components-svelte": "^0.80.0",
"figlet": "^1.6.0",
"javascript-color-gradient": "^2.4.4",
"minecraft-server-status": "^1.0.1",
"minecraft-status": "^1.1.0",
"minecraft-text-js": "^1.1.3",
"mongoose": "^8.2.3",
"pixel-draw": "^0.0.1273",
"skinview3d": "^3.0.0-alpha.1",
"svelte-canvas": "^0.9.3",
"svelte-easy-crop": "^2.0.1"
"name": "mcutils",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-cloudflare": "^4.1.0",
"@sveltejs/kit": "^2.5.2",
"@types/figlet": "^1.5.6",
"@types/javascript-color-gradient": "^2.4.0",
"@types/node": "^18.15.10",
"@zerodevx/svelte-toast": "^0.9.3",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.21",
"svelte": "^4.2.12",
"svelte-awesome-color-picker": "^2.4.5",
"svelte-awesome-slider": "^1.1.0",
"svelte-check": "^3.0.1",
"svelte-multiselect": "^10.1.0",
"tailwindcss": "^3.2.7",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^5.1.5"
},
"type": "module",
"dependencies": {
"carbon-components-svelte": "^0.80.0",
"figlet": "^1.6.0",
"javascript-color-gradient": "^2.4.4",
"minecraft-server-status": "^1.0.1",
"minecraft-status": "^1.1.0",
"minecraft-text-js": "^1.1.3",
"mongoose": "^8.2.3",
"pixel-draw": "^0.0.1273",
"skinview3d": "^3.0.0-alpha.1",
"svelte-canvas": "^0.9.3",
"svelte-easy-crop": "^2.0.1",
"pako": "^2.1.0"
}
}
}
216 changes: 216 additions & 0 deletions src/lib/component/util/luck-perms.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
<script lang="ts">
import { gzip } from 'pako';

type Node = {
type: 'prefix' | 'weight' | 'permission';
key: string;
value: boolean;
};

type Group = {
name: string;
prefix: string; // full key: e.g. "prefix.1.&7&lMEMBER&r "
weight?: number; // required before export
permissions: string[]; // e.g. ['essentials.fly']
};

// Required field
let author = '';

// Editable groups
let groups: Group[] = [
{ name: 'default', prefix: 'prefix.1.&7&lMEMBER&r ', weight: undefined, permissions: [] }
];

// Validation helpers
$: authorError = author.trim().length === 0 ? 'Author is required' : '';
function groupWeightError(g: Group): string | '' {
return typeof g.weight === 'number' && Number.isFinite(g.weight) ? '' : 'Weight is required';
}
$: anyGroupMissingWeight = groups.some((g) => !groupWeightError(g) ? false : (g.name.trim().length > 0 || g.prefix.trim().length > 0 || g.permissions.length > 0));
$: hasDefault = groups.some((g) => g.name.trim() === 'default');
$: defaultGroup = groups.find((g) => g.name.trim() === 'default');
$: defaultWeightError = defaultGroup ? groupWeightError(defaultGroup) : 'Default group is required';

// Build payload
function nowPretty(): string {
try {
return new Date().toLocaleString(undefined, { hour12: false, timeZoneName: 'short' });
} catch {
return new Date().toISOString();
}
}

function buildJsonPayload(emptyBase = false) {
const metadata = {
generatedBy: author.trim(),
generatedAt: nowPretty()
};

if (emptyBase) {
return {
metadata,
groups: {},
tracks: {},
users: {}
};
}

const groupsObj: Record<string, { nodes: Node[] }> = {};

for (const g of groups) {
const name = g.name.trim();
if (!name) continue;

// enforce weight
if (!(typeof g.weight === 'number' && Number.isFinite(g.weight))) {
continue; // skip invalid groups
}

const nodes: Node[] = [];
if (g.prefix && g.prefix.trim()) {
nodes.push({ type: 'prefix', key: g.prefix.trim(), value: true });
}
nodes.push({ type: 'weight', key: `weight.${g.weight}`, value: true });
for (const perm of g.permissions) {
const p = (perm || '').trim();
if (!p) continue;
nodes.push({ type: 'permission', key: p, value: true });
}

groupsObj[name] = { nodes };
}

return {
metadata,
groups: groupsObj,
tracks: {},
users: {}
};
}

function downloadGz(filename: string, jsonObj: unknown) {
const json = JSON.stringify(jsonObj, null, 2);
const compressed = gzip(json);
// Copy into a fresh Uint8Array to avoid SharedArrayBuffer typing issues
const u8 = new Uint8Array(compressed.byteLength);
u8.set(compressed);
const blob = new Blob([u8], { type: 'application/gzip' });
const url = URL.createObjectURL(blob);

const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}

const canExportFilled = () => authorError === '' && !anyGroupMissingWeight && hasDefault && defaultWeightError === '';
const canExportEmpty = () => authorError === '';

function addGroup() {
groups = [
...groups,
{ name: '', prefix: 'prefix.1.&7&lRANK&r ', weight: undefined, permissions: [] }
];
}
function removeGroup(idx: number) {
const g = groups[idx];
if (g && g.name.trim() === 'default') return; // cannot remove default
groups = groups.filter((_, i) => i !== idx);
}
function addPermission(idx: number) { groups[idx].permissions = [...groups[idx].permissions, 'example.permission']; }
function removePermission(gidx: number, pidx: number) { groups[gidx].permissions = groups[gidx].permissions.filter((_, i) => i !== pidx); }

function exportFilled() { if (canExportFilled()) downloadGz('luckperms.json.gz', buildJsonPayload(false)); }
function exportEmptyBase() { if (canExportEmpty()) downloadGz('luckperms-base.json.gz', buildJsonPayload(true)); }
</script>

<style>
.card { border: 1px solid #333; border-radius: 8px; padding: 12px; margin: 8px 0; background: #111; }
.row { display: flex; gap: 8px; flex-wrap: wrap; }
.col { display: flex; flex-direction: column; gap: 6px; }
input { background: #1b1b1b; color: #e0e0e0; border: 1px solid #333; border-radius: 6px; padding: 8px; }
label { font-size: 0.9rem; color: #bbb; }
button { background: #2d6cdf; color: white; border: none; border-radius: 6px; padding: 8px 12px; cursor: pointer; }
button.secondary { background: #444; }
button.danger { background: #d94a4a; }
.muted { color: #8a8a8a; font-size: 0.9rem; }
.error { color: #ff6b6b; font-size: 0.85rem; }
.disabled { opacity: 0.6; cursor: not-allowed; }
</style>

<svelte:head>
<title>LuckPerms Prefix/Groups Builder</title>
<meta name="description" content="Create LuckPerms groups with prefix, weight, and permissions. Export as .json.gz" />
</svelte:head>

<div class="col" style="gap:12px">
<div class="row" style="align-items:center; gap:12px; margin-top:8px">
<img src="/component/icon/luck-perms.svg" alt="LuckPerms" style="height:32px" />
<h1>LuckPerms Prefix/Groups Builder</h1>
</div>

<div class="row">
<div class="col" style="flex:1; min-width:260px">
<label for="author">Author (required)</label>
<input id="author" bind:value={author} placeholder="Your name or team" />
{#if authorError}<span class="error">{authorError}</span>{/if}
<span class="muted">Will be set to <code>metadata.generatedBy</code>.</span>
</div>
</div>

<div class="row" style="align-items:center; gap:12px; margin-top:8px">
<button on:click={addGroup}>+ Add group</button>
<button class="secondary {canExportEmpty() ? '' : 'disabled'}" on:click={exportEmptyBase} disabled={!canExportEmpty()}>Export empty base (.json.gz)</button>
<button class="{canExportFilled() ? '' : 'disabled'}" on:click={exportFilled} disabled={!canExportFilled()}>Export with groups (.json.gz)</button>
</div>

{#each groups as g, i}
<div class="card">
<div class="row">
<div class="col" style="flex:1; min-width:200px">
<label for={`group-name-${i}`}>Group name</label>
<input id={`group-name-${i}`} bind:value={g.name} placeholder="owner, admin, default, ..." disabled={g.name.trim()==='default'} readonly={g.name.trim()==='default'} />
</div>
<div class="col" style="flex:2; min-width:300px">
<label for={`group-prefix-${i}`}>Prefix (full key)</label>
<input id={`group-prefix-${i}`} bind:value={g.prefix} placeholder="prefix.1.&7&lMEMBER&r " />
<span class="muted">Full syntax required: <code>prefix.&lt;priority&gt;.&lt;formatted text&gt;</code></span>
</div>
<div class="col" style="width:160px">
<label for={`group-weight-${i}`}>Weight (required)</label>
<input id={`group-weight-${i}`} type="number" bind:value={g.weight} placeholder="100" />
{#if groupWeightError(g)}<span class="error">{groupWeightError(g)}</span>{/if}
</div>
</div>

<div class="col" style="margin-top:8px">
<h3 class="muted">Permissions</h3>
{#each g.permissions as p, pidx}
<div class="row" style="align-items:center">
<input style="flex:1" aria-label="Permission" bind:value={g.permissions[pidx]} placeholder="e.g. essentials.fly" />
<button class="danger" on:click={() => removePermission(i, pidx)}>Remove</button>
</div>
{/each}
<div>
<button class="secondary" on:click={() => addPermission(i)}>+ Add permission</button>
</div>
</div>

{#if g.name.trim() !== 'default'}
<div style="margin-top:8px">
<button class="danger" on:click={() => removeGroup(i)}>Remove group</button>
</div>
{/if}
</div>
{/each}

<div class="card">
<h3>Export format (empty base preview)</h3>
<pre class="muted" style="overflow:auto; max-height:240px">{JSON.stringify(buildJsonPayload(true), null, 2)}</pre>
<span class="muted">The exported base has <code>groups</code> and <code>users</code> empty so you can fill it in the LuckPerms site.</span>
</div>
</div>
27 changes: 17 additions & 10 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const utils: Util[] = [
name: "Item IDs",
description: "Search all in-game items and IDs.",
shortDescription: "Modern & legacy item IDs",
seoDescription: "Comprehensive Minecraft item ID database. Search and explore all current and legacy item IDs (data values). Essential tool for modders, plugin developers, and command block enthusiasts.",
seoDescription: "Comprehensive Minecraft item ID database. Search and explore all current and legacy item IDs (data values). Essential tool for modders, plugin developers, and command block enthusiasts."
},
{
path: "sound-explorer",
Expand All @@ -39,7 +39,7 @@ const utils: Util[] = [
name: "RAM Calculator",
description: "Calculate the recommended RAM for your server.",
shortDescription: "Calculate recommended RAM",
seoDescription: "Optimize your Minecraft server performance with a RAM calculator. Get precise memory recommendations based on player count and installed plugins. Ensure smooth gameplay and maximize server efficiency with accurate RAM allocation.",
seoDescription: "Optimize your Minecraft server performance with a RAM calculator. Get precise memory recommendations based on player count and installed plugins. Ensure smooth gameplay and maximize server efficiency with accurate RAM allocation."
},
{
path: "server-info",
Expand All @@ -53,42 +53,42 @@ const utils: Util[] = [
name: "Skin Stealer",
description: "Steal a player's skin from their IGN.",
shortDescription: "Steal another player's skin",
seoDescription: "Discover and download Minecraft player skins instantly. Search by in-game name to view, save, and use any player's unique skin. Perfect for inspiration, cosplay, or trying new looks in Minecraft.",
seoDescription: "Discover and download Minecraft player skins instantly. Search by in-game name to view, save, and use any player's unique skin. Perfect for inspiration, cosplay, or trying new looks in Minecraft."
},
{
path: "cape-stealer",
name: "Cape Stealer",
description: "Steal a player's cape from their IGN.",
shortDescription: "Steal another player's cape",
seoDescription: "Explore and download rare Minecraft capes. Find Mojang, Minecon, and Optifine capes by player name. View, save, and admire exclusive Minecraft capes from any player. Ideal for cape enthusiasts and collectors.",
seoDescription: "Explore and download rare Minecraft capes. Find Mojang, Minecon, and Optifine capes by player name. View, save, and admire exclusive Minecraft capes from any player. Ideal for cape enthusiasts and collectors."
},
{
path: "color-codes",
name: "Color Codes",
description: "List of built in color and format codes.",
shortDescription: "All default color codes",
seoDescription: "Complete Minecraft color code reference guide. Access chat codes (&), MiniMessage tags, MOTD codes, and hex values. Enhance your server's visual appeal with our comprehensive color code table for text formatting and customization.",
seoDescription: "Complete Minecraft color code reference guide. Access chat codes (&), MiniMessage tags, MOTD codes, and hex values. Enhance your server's visual appeal with our comprehensive color code table for text formatting and customization."
},
{
path: "color-text-generator",
name: "Color Text Generator",
description: "Generate and preview text with color codes.",
shortDescription: "Create text with color codes",
seoDescription: "Create vibrant Minecraft text with our color code generator. Preview custom colored messages in chat, signs, books, MOTD, item name, item lore and kick message. Supports both standard color codes and custom hex values for unlimited creativity.",
seoDescription: "Create vibrant Minecraft text with our color code generator. Preview custom colored messages in chat, signs, books, MOTD, item name, item lore and kick message. Supports both standard color codes and custom hex values for unlimited creativity."
},
{
path: "gradient-generator",
name: "Gradient Generator",
description: "Create a gradient between two colors for in-game text.",
shortDescription: "Create chat color gradient",
seoDescription: "Design color gradients for Minecraft text, creating smooth transitions between any two colors. Get instant Spigot ChatColor and MiniMessage outputs for easy implementation in plugins.",
seoDescription: "Design color gradients for Minecraft text, creating smooth transitions between any two colors. Get instant Spigot ChatColor and MiniMessage outputs for easy implementation in plugins."
},
{
path: "small-text-converter",
name: "Small Text Converter",
description: "Convert into sᴍᴀʟʟ ᴛᴇxᴛ seen on new era servers.",
shortDescription: "Convert into sᴍᴀʟʟ ᴛᴇxᴛ",
seoDescription: "Transform your Minecraft text into stylish small caps. Convert regular text to the trendy, compact font used on popular servers like MCCI, Mineclub, TubNet. Stand out in chat, signs, and names with the easy-to-use small text converter.",
seoDescription: "Transform your Minecraft text into stylish small caps. Convert regular text to the trendy, compact font used on popular servers like MCCI, Mineclub, TubNet. Stand out in chat, signs, and names with the easy-to-use small text converter."
},
{
path: "server-icon-converter",
Expand All @@ -102,7 +102,7 @@ const utils: Util[] = [
name: "Unicode Symbols",
description: "Collection of allowed in-game unicode characters.",
shortDescription: "All allowed unicode symbols",
seoDescription: "Explore hundreds of Minecraft-compatible Unicode symbols. Enhance your chat, GUIs, and MOTDs with unique characters. The curated collection ensures all symbols display correctly in-game.",
seoDescription: "Explore hundreds of Minecraft-compatible Unicode symbols. Enhance your chat, GUIs, and MOTDs with unique characters. The curated collection ensures all symbols display correctly in-game."
},
{
path: "note-block-songs",
Expand Down Expand Up @@ -139,8 +139,15 @@ const utils: Util[] = [
"description": "Convert coordinates between the Overworld and the Nether, or vice versa.",
"shortDescription": "Convert coordinates",
"seoDescription": "Convert Minecraft coordinates between the Overworld and the Nether instantly. Calculate precise coordinates for fast travel, portal linking, and efficient navigation. Get accurate 8:1 ratio conversions for both directions, making your Nether travel planning quick and easy."
},
{
path: "luck-perms",
name: "LuckPerms Generator",
description: "Build LuckPerms groups with prefix, weight and permissions. Export a .json.gz file ready for the editor.",
shortDescription: "Generate LuckPerms groups",
seoDescription: "Create LuckPerms group configurations with colored prefixes, weights, and permissions. Export a compressed .json.gz compatible with the LuckPerms web editor."
}
];
];

type Util = {
path: string;
Expand Down
Loading