Skip to content

Commit 4e56f7b

Browse files
authored
Merge pull request #337 from SkyCryptWebsite/refactor/item-renderinng
refactor: item rendering
2 parents cd7cb00 + 7060655 commit 4e56f7b

5 files changed

Lines changed: 91 additions & 5 deletions

File tree

src/lib/components/item/ContainedItem.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
import { cn } from "$lib/shared/utils";
44
import Image from "@lucide/svelte/icons/image";
55
import { Avatar } from "bits-ui";
6+
import ResolvedItemImage from "./ResolvedItemImage.svelte";
67
78
let { piece, isInventory }: { piece: ModelsStrippedItem; isInventory?: boolean } = $props();
89
</script>
910

1011
<div class="relative flex aspect-square items-center justify-center overflow-clip p-0">
1112
<div class="absolute inset-0 rounded-xl"></div>
1213
<Avatar.Root>
13-
<Avatar.Image loading="lazy" src={piece.texture_path} alt={piece.display_name} class={cn("h-auto w-14 select-none [image-rendering:pixelated] data-[enchanted=true]:enchanted", isInventory ? "w-8" : "w-14")} />
14+
<ResolvedItemImage loading="lazy" src={piece.texture_path} alt={piece.display_name} class={cn("h-auto w-14 select-none [image-rendering:pixelated] data-[enchanted=true]:enchanted", isInventory ? "w-8" : "w-14")} />
1415
<Avatar.Fallback>
1516
<Image class={cn(isInventory ? "size-8" : "size-14")} />
1617
</Avatar.Fallback>

src/lib/components/item/Item.svelte

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import ImageOff from "@lucide/svelte/icons/image-off";
88
import { Avatar, Tooltip, type AvatarImageLoadingStatus } from "bits-ui";
99
import { IsInViewport } from "runed";
10+
import ResolvedItemImage from "./ResolvedItemImage.svelte";
1011
1112
type Props = {
1213
piece: ModelsStrippedItem;
@@ -19,11 +20,12 @@
1920
let targetNode = $state<HTMLButtonElement | null>(null);
2021
let hasBeenInViewport = $state(false);
2122
let loadingStatus = $state<AvatarImageLoadingStatus>(null!);
23+
let resolvedTexturePack = $state<string>();
2224
2325
const internalState = getInternalState();
2426
2527
const inViewport = new IsInViewport(() => targetNode, { rootMargin: "200px 0px", threshold: 0 });
26-
const skyblockItem = $derived(piece);
28+
const skyblockItem = $derived({ ...piece, texture_pack: resolvedTexturePack ?? piece.texture_pack });
2729
const bgColor = $derived(getRarityClass(piece.rarity ?? ("common".toLowerCase() as string), "bg"));
2830
const recombobulated = $derived(showRecombobulated && (skyblockItem.recombobulated ?? false));
2931
const enchanted = $derived(skyblockItem?.texture_path?.includes("/api/leather/") ? false : skyblockItem.shiny);
@@ -53,7 +55,7 @@
5355
<div {...props}>
5456
{#if hasBeenInViewport}
5557
<Avatar.Root bind:loadingStatus class={cn("after:border-none", isInventory ? "size-6 sm:size-14" : "size-14")}>
56-
<Avatar.Image loading="lazy" src={piece.texture_path} alt={piece.display_name} class={cn("pointer-events-none aspect-square select-none [image-rendering:pixelated] data-[enchanted=true]:enchanted", isInventory ? "size-6 sm:size-14" : "size-14")} data-enchanted={enchanted} />
58+
<ResolvedItemImage loading="lazy" src={piece.texture_path} alt={piece.display_name} class={cn("pointer-events-none aspect-square select-none [image-rendering:pixelated] data-[enchanted=true]:enchanted", isInventory ? "size-6 sm:size-14" : "size-14")} {enchanted} onresolved={(resolution) => (resolvedTexturePack = resolution.texture_pack)} />
5759
{#if loadingStatus === "loading"}
5860
{@render loadingState()}
5961
{:else}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<script lang="ts">
2+
import { resolveItemTexture, type ResolvedItemTexture } from "$lib/shared/items/resolve-item-texture";
3+
import { Avatar } from "bits-ui";
4+
import { getAbortSignal } from "svelte";
5+
6+
type Props = {
7+
src?: string;
8+
alt?: string;
9+
class?: string;
10+
loading?: "eager" | "lazy";
11+
enchanted?: boolean;
12+
onresolved?: (resolution: ResolvedItemTexture) => void;
13+
};
14+
15+
let { src = "", alt = "", class: className, loading = "lazy", enchanted, onresolved }: Props = $props();
16+
let resolvedSrc = $state("");
17+
18+
$effect(() => {
19+
const textureUrl = src;
20+
resolvedSrc = textureUrl;
21+
if (!textureUrl) return;
22+
23+
const signal = getAbortSignal();
24+
resolveItemTexture(textureUrl).then((resolution) => {
25+
if (signal.aborted) return;
26+
resolvedSrc = resolution.texture;
27+
onresolved?.(resolution);
28+
});
29+
});
30+
</script>
31+
32+
<Avatar.Image {loading} src={resolvedSrc} {alt} class={className} data-enchanted={enchanted} />

src/lib/components/item/item-content.svelte

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,18 @@
99
import TriangleAlert from "@lucide/svelte/icons/triangle-alert";
1010
import { Avatar, Button } from "bits-ui";
1111
import ContainedItem from "./ContainedItem.svelte";
12+
import ResolvedItemImage from "./ResolvedItemImage.svelte";
1213
1314
type Props = {
1415
piece: ModelsStrippedItem;
1516
isDrawer?: boolean;
1617
};
1718
1819
let { piece, isDrawer }: Props = $props();
20+
let resolvedTexturePack = $state<string>();
1921
const preferences = getPreferences();
2022
21-
const skyblockItem = $derived(piece);
23+
const skyblockItem = $derived({ ...piece, texture_pack: resolvedTexturePack ?? piece.texture_pack });
2224
const itemName = $derived(piece?.display_name);
2325
const itemNameHtml = $derived(itemName ? renderLore(itemName) : "");
2426
const isMulticolor = $derived((itemNameHtml?.match(/<\/span>/g) || [])?.length > 1);
@@ -34,7 +36,7 @@
3436
<div class="group-data-[mctooltip=false]/itemtooltip:contents group-data-[mctooltip=true]/itemtooltip:minecraft-tooltip group-data-[mctooltip=true]/itemtooltip:max-h-[calc(100dvh-8rem)] group-data-[mctooltip=true]/itemtooltip:overflow-auto">
3537
<div class={cn("flex-nowrap items-center justify-center gap-4 nice-colors-dark group-data-[mctooltip=false]/itemtooltip:flex group-data-[mctooltip=false]/itemtooltip:p-5", { "group-data-[mctooltip=false]/itemtooltip:rounded-t-[10px]": isDrawer }, preferences.mctooltip ? undefined : bgColor)}>
3638
<Avatar.Root class="shrink-0 px-2 group-data-[mctooltip=true]/itemtooltip:hidden">
37-
<Avatar.Image loading="lazy" src={piece?.texture_path} alt={piece?.display_name} class="h-auto w-8 flex-none shrink-0 overflow-hidden [image-rendering:pixelated] data-[enchanted=true]:enchanted" data-enchanted={enchanted} />
39+
<ResolvedItemImage loading="lazy" src={piece?.texture_path} alt={piece?.display_name} class="h-auto w-8 flex-none shrink-0 overflow-hidden [image-rendering:pixelated] data-[enchanted=true]:enchanted" {enchanted} onresolved={(resolution) => (resolvedTexturePack = resolution.texture_pack)} />
3840
<Avatar.Fallback>
3941
<Image class="size-8" />
4042
</Avatar.Fallback>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
export type ResolvedItemTexture = {
2+
texture: string;
3+
texture_pack?: string;
4+
};
5+
6+
const resolutionCache = new Map<string, Promise<ResolvedItemTexture>>();
7+
8+
function enabledPacksCookie(): string {
9+
if (typeof document === "undefined") return "";
10+
const cookie = document.cookie
11+
.split(";")
12+
.map((part) => part.trim())
13+
.find((part) => part.startsWith("enabledPacks="));
14+
const value = cookie?.slice("enabledPacks=".length) ?? "";
15+
try {
16+
return decodeURIComponent(value);
17+
} catch {
18+
return value;
19+
}
20+
}
21+
22+
function resolverUrl(textureUrl: string, enabledPacks: string): string | null {
23+
const url = new URL(textureUrl, window.location.origin);
24+
const match = url.pathname.match(/^(.*\/api\/item\/)([^/]+)$/);
25+
if (!match) return null;
26+
url.pathname = `${match[1]}${match[2]}/resolve`;
27+
if (enabledPacks) url.searchParams.set("enabledPacks", enabledPacks);
28+
return url.toString();
29+
}
30+
31+
export function resolveItemTexture(textureUrl: string): Promise<ResolvedItemTexture> {
32+
const enabledPacks = enabledPacksCookie();
33+
const cacheKey = `${textureUrl}|${enabledPacks}`;
34+
const cached = resolutionCache.get(cacheKey);
35+
if (cached) return cached;
36+
37+
const url = resolverUrl(textureUrl, enabledPacks);
38+
if (!url) return Promise.resolve({ texture: textureUrl });
39+
40+
const resolution = fetch(url)
41+
.then(async (response) => {
42+
if (!response.ok) throw new Error(`Failed to resolve item texture: ${response.status}`);
43+
return (await response.json()) as ResolvedItemTexture;
44+
})
45+
.catch(() => ({ texture: textureUrl }));
46+
47+
resolutionCache.set(cacheKey, resolution);
48+
return resolution;
49+
}

0 commit comments

Comments
 (0)