"use client"; import { useTranslations } from "next-intl"; import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { adminFetch } from "@/lib/admin-fetch"; import type { CompressionReport, NitroQuality, ScaleReport, } from "@/lib/furni/nitro-quality"; function kb(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / 1024 / 1024).toFixed(2)} MB`; } function CompressionPanel({ report }: { report: CompressionReport }) { const t = useTranslations("admin.studio.quality"); const { texture, json } = report, // Differences that a renderer can actually see. visibleDiffs = texture.mismatchedPixels - texture.invisibleDifferences, // `webpEstimate.savingBytes` measures a PNG texture against a WebP // re-encode, so it reads ~0 for a bundle that already *is* WebP. The // honest WebP-vs-PNG comparison is the stored bytes against the PNG // encoding of the same artwork. pngSaving = texture.rawBytes - texture.pngEstimate.bytes; return (

{t("compressionTitle", { format: texture.format.toUpperCase() })} {texture.format === "webp" ? texture.lossless ? ` ${t("losslessVp8l")}` : ` ${t("lossyVp8")}` : ""}

{t("bundle")}
{kb(report.bundleBytes)}
{t("texture")}
{kb(texture.compressedBytes)} {" "} ({Math.round(report.textureShare * 100)}%)
{t("metadata")}
{kb(json.compressedBytes)} · ×{json.ratio}
{t("container")}
{kb(report.overheadBytes)}

{t("textureDetail", { raw: kb(texture.rawBytes), ratio: texture.ratio, width: texture.width, height: texture.height, frames: texture.spriteCount, })}

{texture.format === "png" ? (

{texture.webpEstimate.savingBytes > 0 ? t.rich("legacyPngSaving", { size: kb(texture.webpEstimate.bytes), saving: kb(texture.webpEstimate.savingBytes), percent: texture.webpEstimate.bytes > 0 ? Math.round( (1 - texture.webpEstimate.bytes / texture.rawBytes) * 100, ) : 0, strong: (chunks) => {chunks}, }) : t.rich("legacyPngNoGain", { size: kb(texture.webpEstimate.bytes), strong: (chunks) => {chunks}, })}

) : (

{pngSaving > 0 ? t.rich("webpSaving", { raw: kb(texture.rawBytes), png: kb(texture.pngEstimate.bytes), saving: kb(pngSaving), percent: texture.pngEstimate.bytes > 0 ? Math.round((pngSaving / texture.pngEstimate.bytes) * 100) : 0, strong: (chunks) => {chunks}, }) : t("webpNoGain", { raw: kb(texture.rawBytes), png: kb(texture.pngEstimate.bytes), })}

)}

{visibleDiffs === 0 ? ( <> {t.rich("pixelExact", { strong: (chunks) => ( {chunks} ), })} {texture.invisibleDifferences > 0 && ( {" "} {t("invisibleDiffs", { count: texture.invisibleDifferences, })} )} ) : ( {t("texelsDiffer", { count: visibleDiffs })} )}

); } function ScalePanel({ report, classname, revision, }: { report: ScaleReport; classname: string; revision: number; }) { const [asset, setAsset] = useState(report.assets[0] ?? ""), [failed, setFailed] = useState(false); const t = useTranslations("admin.studio.quality"); const stateLabel = report.state === "missing" ? t("stateMissing") : report.state === "issues" ? t("stateIssues") : report.derived ? t("stateDerived") : t("statePresent"); return (

{t("scaleTitle", { size: report.size, state: stateLabel })}

{t("spritesLine", { sprites: report.assets.length, animations: report.animations.length, directions: report.directions.join(", ") || "—", })}

{report.state === "missing" && (

{t("missingScale")}

)} {report.assets.length > 0 && ( <>
{failed ? (

{t("renderFailed")}

) : ( {asset} setFailed(true)} /> )}
)} {report.issues.length > 0 && ( )}
); } export function NitroQualityPanel({ classname }: { classname: string }) { const t = useTranslations("admin.studio.quality"); const [report, setReport] = useState(null), [error, setError] = useState(""), [revision, setRevision] = useState(0); useEffect(() => { const controller = new AbortController(); setReport(null); setError(""); void (async () => { try { const response = await adminFetch( `/api/admin/studio/nitro-quality?classname=${encodeURIComponent(classname)}&v=${revision}`, { signal: controller.signal }, ); const data = await response.json(); if (!response.ok) throw Error(data.error || t("inspectionFailed")); if (!controller.signal.aborted) setReport(data.report); } catch (error) { if (!controller.signal.aborted) setError( error instanceof Error ? error.message : t("inspectionFailed"), ); } })(); return () => controller.abort(); }, [classname, revision, t]); return (

{t("intro")}

{error ? (

{error}

) : !report ? (

{t("checking")}

) : (
{report.compression && ( )}
{report.scales.map((scale) => ( ))}
)}
); }