diff --git a/src/lib/components/item/ContainedItem.svelte b/src/lib/components/item/ContainedItem.svelte
index 04f1c8b1e..ce11cef71 100644
--- a/src/lib/components/item/ContainedItem.svelte
+++ b/src/lib/components/item/ContainedItem.svelte
@@ -3,6 +3,7 @@
import { cn } from "$lib/shared/utils";
import Image from "@lucide/svelte/icons/image";
import { Avatar } from "bits-ui";
+ import ResolvedItemImage from "./ResolvedItemImage.svelte";
let { piece, isInventory }: { piece: ModelsStrippedItem; isInventory?: boolean } = $props();
@@ -10,7 +11,7 @@
-
+
diff --git a/src/lib/components/item/Item.svelte b/src/lib/components/item/Item.svelte
index 60388a0ba..d83a40bd9 100644
--- a/src/lib/components/item/Item.svelte
+++ b/src/lib/components/item/Item.svelte
@@ -7,6 +7,7 @@
import ImageOff from "@lucide/svelte/icons/image-off";
import { Avatar, Tooltip, type AvatarImageLoadingStatus } from "bits-ui";
import { IsInViewport } from "runed";
+ import ResolvedItemImage from "./ResolvedItemImage.svelte";
type Props = {
piece: ModelsStrippedItem;
@@ -19,11 +20,12 @@
let targetNode = $state(null);
let hasBeenInViewport = $state(false);
let loadingStatus = $state(null!);
+ let resolvedTexturePack = $state();
const internalState = getInternalState();
const inViewport = new IsInViewport(() => targetNode, { rootMargin: "200px 0px", threshold: 0 });
- const skyblockItem = $derived(piece);
+ const skyblockItem = $derived({ ...piece, texture_pack: resolvedTexturePack ?? piece.texture_pack });
const bgColor = $derived(getRarityClass(piece.rarity ?? ("common".toLowerCase() as string), "bg"));
const recombobulated = $derived(showRecombobulated && (skyblockItem.recombobulated ?? false));
const enchanted = $derived(skyblockItem?.texture_path?.includes("/api/leather/") ? false : skyblockItem.shiny);
@@ -53,7 +55,7 @@
{#if hasBeenInViewport}
-
+ (resolvedTexturePack = resolution.texture_pack)} />
{#if loadingStatus === "loading"}
{@render loadingState()}
{:else}
diff --git a/src/lib/components/item/ResolvedItemImage.svelte b/src/lib/components/item/ResolvedItemImage.svelte
new file mode 100644
index 000000000..e3d8521e3
--- /dev/null
+++ b/src/lib/components/item/ResolvedItemImage.svelte
@@ -0,0 +1,32 @@
+
+
+
diff --git a/src/lib/components/item/item-content.svelte b/src/lib/components/item/item-content.svelte
index f0a3a8f01..4789128f4 100644
--- a/src/lib/components/item/item-content.svelte
+++ b/src/lib/components/item/item-content.svelte
@@ -9,6 +9,7 @@
import TriangleAlert from "@lucide/svelte/icons/triangle-alert";
import { Avatar, Button } from "bits-ui";
import ContainedItem from "./ContainedItem.svelte";
+ import ResolvedItemImage from "./ResolvedItemImage.svelte";
type Props = {
piece: ModelsStrippedItem;
@@ -16,9 +17,10 @@
};
let { piece, isDrawer }: Props = $props();
+ let resolvedTexturePack = $state();
const preferences = getPreferences();
- const skyblockItem = $derived(piece);
+ const skyblockItem = $derived({ ...piece, texture_pack: resolvedTexturePack ?? piece.texture_pack });
const itemName = $derived(piece?.display_name);
const itemNameHtml = $derived(itemName ? renderLore(itemName) : "");
const isMulticolor = $derived((itemNameHtml?.match(/<\/span>/g) || [])?.length > 1);
@@ -34,7 +36,7 @@
-
+ (resolvedTexturePack = resolution.texture_pack)} />
diff --git a/src/lib/shared/items/resolve-item-texture.ts b/src/lib/shared/items/resolve-item-texture.ts
new file mode 100644
index 000000000..eb85c47ba
--- /dev/null
+++ b/src/lib/shared/items/resolve-item-texture.ts
@@ -0,0 +1,49 @@
+export type ResolvedItemTexture = {
+ texture: string;
+ texture_pack?: string;
+};
+
+const resolutionCache = new Map>();
+
+function enabledPacksCookie(): string {
+ if (typeof document === "undefined") return "";
+ const cookie = document.cookie
+ .split(";")
+ .map((part) => part.trim())
+ .find((part) => part.startsWith("enabledPacks="));
+ const value = cookie?.slice("enabledPacks=".length) ?? "";
+ try {
+ return decodeURIComponent(value);
+ } catch {
+ return value;
+ }
+}
+
+function resolverUrl(textureUrl: string, enabledPacks: string): string | null {
+ const url = new URL(textureUrl, window.location.origin);
+ const match = url.pathname.match(/^(.*\/api\/item\/)([^/]+)$/);
+ if (!match) return null;
+ url.pathname = `${match[1]}${match[2]}/resolve`;
+ if (enabledPacks) url.searchParams.set("enabledPacks", enabledPacks);
+ return url.toString();
+}
+
+export function resolveItemTexture(textureUrl: string): Promise {
+ const enabledPacks = enabledPacksCookie();
+ const cacheKey = `${textureUrl}|${enabledPacks}`;
+ const cached = resolutionCache.get(cacheKey);
+ if (cached) return cached;
+
+ const url = resolverUrl(textureUrl, enabledPacks);
+ if (!url) return Promise.resolve({ texture: textureUrl });
+
+ const resolution = fetch(url)
+ .then(async (response) => {
+ if (!response.ok) throw new Error(`Failed to resolve item texture: ${response.status}`);
+ return (await response.json()) as ResolvedItemTexture;
+ })
+ .catch(() => ({ texture: textureUrl }));
+
+ resolutionCache.set(cacheKey, resolution);
+ return resolution;
+}