Skip to content
Merged
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
3 changes: 2 additions & 1 deletion src/lib/components/item/ContainedItem.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
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();
</script>

<div class="relative flex aspect-square items-center justify-center overflow-clip p-0">
<div class="absolute inset-0 rounded-xl"></div>
<Avatar.Root>
<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")} />
<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")} />
<Avatar.Fallback>
<Image class={cn(isInventory ? "size-8" : "size-14")} />
</Avatar.Fallback>
Expand Down
6 changes: 4 additions & 2 deletions src/lib/components/item/Item.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,11 +20,12 @@
let targetNode = $state<HTMLButtonElement | null>(null);
let hasBeenInViewport = $state(false);
let loadingStatus = $state<AvatarImageLoadingStatus>(null!);
let resolvedTexturePack = $state<string>();

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);
Expand Down Expand Up @@ -53,7 +55,7 @@
<div {...props}>
{#if hasBeenInViewport}
<Avatar.Root bind:loadingStatus class={cn("after:border-none", isInventory ? "size-6 sm:size-14" : "size-14")}>
<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} />
<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)} />
{#if loadingStatus === "loading"}
{@render loadingState()}
{:else}
Expand Down
32 changes: 32 additions & 0 deletions src/lib/components/item/ResolvedItemImage.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script lang="ts">
import { resolveItemTexture, type ResolvedItemTexture } from "$lib/shared/items/resolve-item-texture";
import { Avatar } from "bits-ui";
import { getAbortSignal } from "svelte";

type Props = {
src?: string;
alt?: string;
class?: string;
loading?: "eager" | "lazy";
enchanted?: boolean;
onresolved?: (resolution: ResolvedItemTexture) => void;
};

let { src = "", alt = "", class: className, loading = "lazy", enchanted, onresolved }: Props = $props();
let resolvedSrc = $state("");

$effect(() => {
const textureUrl = src;
resolvedSrc = textureUrl;
if (!textureUrl) return;

const signal = getAbortSignal();
resolveItemTexture(textureUrl).then((resolution) => {
if (signal.aborted) return;
resolvedSrc = resolution.texture;
onresolved?.(resolution);
});
});
</script>

<Avatar.Image {loading} src={resolvedSrc} {alt} class={className} data-enchanted={enchanted} />
6 changes: 4 additions & 2 deletions src/lib/components/item/item-content.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@
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;
isDrawer?: boolean;
};

let { piece, isDrawer }: Props = $props();
let resolvedTexturePack = $state<string>();
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);
Expand All @@ -34,7 +36,7 @@
<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">
<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)}>
<Avatar.Root class="shrink-0 px-2 group-data-[mctooltip=true]/itemtooltip:hidden">
<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} />
<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)} />
<Avatar.Fallback>
<Image class="size-8" />
</Avatar.Fallback>
Expand Down
49 changes: 49 additions & 0 deletions src/lib/shared/items/resolve-item-texture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export type ResolvedItemTexture = {
texture: string;
texture_pack?: string;
};

const resolutionCache = new Map<string, Promise<ResolvedItemTexture>>();

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<ResolvedItemTexture> {
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;
}