export const STATIC_STICKER_CATALOG_SCHEMA_VERSION = "StaticStickerCatalog/v1" as const; export const STATIC_STICKER_CATALOG_ORIGIN = "bundled_read_only" as const; export interface StaticStickerThumbnailReference { media: "thumbnail"; resource_id: string; resource_version: string; url: string; } export interface StaticStickerCatalogItem { stable_id: string; part: number; order: number; original_filename: string; relative_path: string; width: number; height: number; mime_type: "image/png" | "image/webp"; mime: "image/png" | "image/webp"; sha256: string; original_reference: string; thumbnail_reference: StaticStickerThumbnailReference; resource_version: string; enabled: boolean; origin: "bundled_read_only" | "admin_uploaded"; } export interface StaticStickerCatalog { schema_version: typeof STATIC_STICKER_CATALOG_SCHEMA_VERSION; release_version: string; manifest_sha256: string; count: number; part_counts: Record; duplicate_sha256_groups: Array<{ sha256: string; stable_ids: string[] }>; items: StaticStickerCatalogItem[]; } export interface VirtualStickerWindowOptions { itemHeight: number; viewportHeight: number; columns: number; overscanScreens: number; scrollTop: number; } export interface VirtualStickerWindow { start: number; end: number; top_spacer_px: number; bottom_spacer_px: number; items: T[]; } export function stableStickerId(index: number): string { if (!Number.isInteger(index) || index < 1) throw new Error("sticker_index_invalid"); return `STK${String(index).padStart(index < 1_000 ? 3 : 4, "0")}`; } export function getVirtualStickerWindow(items: readonly T[], options: VirtualStickerWindowOptions): VirtualStickerWindow { const itemHeight = Math.max(1, options.itemHeight); const viewportHeight = Math.max(1, options.viewportHeight); const columns = Math.max(1, Math.floor(options.columns)); const overscanScreens = Math.max(0, options.overscanScreens); const scrollTop = Math.max(0, options.scrollTop); const totalRows = Math.ceil(items.length / columns); const visibleRows = Math.max(1, Math.ceil(viewportHeight / itemHeight)); const overscanRows = Math.ceil((visibleRows * overscanScreens) / 2); const firstVisibleRow = Math.min(totalRows, Math.floor(scrollTop / itemHeight)); const startRow = Math.max(0, firstVisibleRow - overscanRows); const endRow = Math.min(totalRows, firstVisibleRow + visibleRows + overscanRows); const start = startRow * columns; const end = Math.min(items.length, endRow * columns); return { bottom_spacer_px: Math.max(0, (totalRows - endRow) * itemHeight), end, items: items.slice(start, end) as T[], start, top_spacer_px: startRow * itemHeight, }; } export function staticStickerThumbnailUrl(item: Pick): string { return `/api/v1/assets/public/${encodeURIComponent(item.resource_version)}/${encodeURIComponent(item.stable_id)}?variant=thumbnail`; } export function staticStickerOriginalUrl(item: Pick): string { return `/api/v1/assets/public/${encodeURIComponent(item.resource_version)}/${encodeURIComponent(item.stable_id)}`; }