@kaikybrofc/omnizap-system 2.3.7 → 2.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example CHANGED
@@ -167,9 +167,14 @@ STICKER_WEB_GOOGLE_SESSION_DB_PRUNE_INTERVAL_MS=3600000
167
167
  STICKER_WEB_GOOGLE_SESSION_DB_TOUCH_INTERVAL_MS=60000
168
168
  STICKER_WEB_GOOGLE_SESSION_TTL_MS=604800000
169
169
  STICKER_WEB_IMMUTABLE_ASSET_CACHE_SECONDS=31536000
170
- STICKER_WEB_LIST_LIMIT=24
170
+ STICKER_WEB_LIST_LIMIT=16
171
171
  STICKER_WEB_LIST_MAX_LIMIT=60
172
172
  STICKER_WEB_MY_PROFILE_INCLUDE_AUTO_PACKS=false
173
+ STICKER_PREVIEW_SIDE_PX=112
174
+ STICKER_PREVIEW_QUALITY=20
175
+ STICKER_PREVIEW_TIMEOUT_MS=2500
176
+ STICKER_PREVIEW_CACHE_TTL_MS=21600000
177
+ STICKER_PREVIEW_CACHE_MAX_ITEMS=2000
173
178
  STICKER_WEB_STATIC_TEXT_CACHE_SECONDS=3600
174
179
  STICKER_WEB_UPLOAD_CONCURRENCY=3
175
180
  STICKER_WEB_UPLOAD_SOURCE_MAX_BYTES=20971520
package/README.md CHANGED
@@ -78,7 +78,7 @@ Este bloco pode ser atualizado automaticamente pela API (`/api/sticker-packs/rea
78
78
  <!-- README_SNAPSHOT:START -->
79
79
  ### Snapshot do Sistema
80
80
 
81
- > Atualizado em `2026-03-02T04:56:50.656Z` | cache `1800s`
81
+ > Atualizado em `2026-03-02T04:59:06.731Z` | cache `1800s`
82
82
 
83
83
  | Métrica | Valor |
84
84
  | --- | ---: |
@@ -86,15 +86,15 @@ Este bloco pode ser atualizado automaticamente pela API (`/api/sticker-packs/rea
86
86
  | Grupos | 118 |
87
87
  | Packs | 319 |
88
88
  | Stickers | 8.026 |
89
- | Mensagens registradas | 453.904 |
89
+ | Mensagens registradas | 453.907 |
90
90
 
91
91
  #### Tipos de mensagem mais usados (amostra: 25.000)
92
92
  | Tipo | Total |
93
93
  | --- | ---: |
94
- | `texto` | 15.337 |
95
- | `figurinha` | 4.863 |
94
+ | `texto` | 15.340 |
95
+ | `figurinha` | 4.861 |
96
96
  | `reacao` | 1.818 |
97
- | `imagem` | 1.513 |
97
+ | `imagem` | 1.512 |
98
98
  | `outros` | 1.081 |
99
99
  | `video` | 205 |
100
100
  | `audio` | 178 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaikybrofc/omnizap-system",
3
- "version": "2.3.7",
3
+ "version": "2.3.8",
4
4
  "description": "Sistema profissional de automação WhatsApp com tecnologia Baileys",
5
5
  "main": "index.js",
6
6
  "publishConfig": {
@@ -11,6 +11,7 @@ const PROFILE_ROUTE_SEGMENTS = new Set(['perfil', 'profile']);
11
11
  const CREATORS_ROUTE_SEGMENTS = new Set(['creators', 'criadores']);
12
12
  const DEFAULT_STICKER_PLACEHOLDER_URL = 'https://iili.io/fSNGag2.png';
13
13
  const NSFW_STICKER_PLACEHOLDER_URL = 'https://iili.io/qfhwS6u.jpg';
14
+ const LAZY_IMAGE_PLACEHOLDER_DATA_URL = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==';
14
15
 
15
16
  const CATALOG_SORT_OPTIONS = [
16
17
  { value: 'recent', label: 'Mais recentes', icon: '🆕' },
@@ -23,6 +24,16 @@ const CATALOG_SORT_OPTIONS = [
23
24
  const DEFAULT_CATALOG_SORT = 'trending';
24
25
  const DEFAULT_CREATORS_SORT = 'popular';
25
26
  const FIRST_CATALOG_PAGE = 1;
27
+ const MOBILE_MAX_CATALOG_LIMIT = 12;
28
+ const MOBILE_DISCOVER_CAROUSEL_LIMIT = 5;
29
+ const DESKTOP_DISCOVER_GROWING_LIMIT = 4;
30
+ const DESKTOP_DISCOVER_TOP_LIMIT = 4;
31
+ const PACK_STICKERS_INITIAL_LIMIT_MOBILE = 8;
32
+ const PACK_STICKERS_INITIAL_LIMIT_DESKTOP = 12;
33
+ const PACK_STICKERS_LOAD_STEP = 8;
34
+ const OMNIZAP_LOGO_DATA_URL = "data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 0 48 48'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%230ea5e9'/%3E%3Cstop offset='1' stop-color='%2310b981'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='48' height='48' rx='24' fill='url(%23g)'/%3E%3Ctext x='24' y='30' text-anchor='middle' font-family='Segoe UI, Roboto, Arial, sans-serif' font-size='16' font-weight='700' fill='white'%3EOZ%3C/text%3E%3C/svg%3E";
35
+ const AVATAR_BG_PALETTE = ['#0f172a', '#1e293b', '#334155', '#0f766e', '#115e59', '#1d4ed8', '#3b0764', '#7c2d12', '#7f1d1d', '#14532d'];
36
+ const AVATAR_URL_CACHE = new Map();
26
37
 
27
38
  const safeNumber = (value, fallback = 0) => {
28
39
  const numeric = Number(value);
@@ -103,6 +114,20 @@ const parseIntSafe = (value, fallback) => {
103
114
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
104
115
  };
105
116
 
117
+ const resolveCatalogPageLimit = (defaultLimit) => {
118
+ const parsedLimit = parseIntSafe(defaultLimit, 24);
119
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return parsedLimit;
120
+ const isMobileViewport = window.matchMedia('(max-width: 767px)').matches;
121
+ if (!isMobileViewport) return parsedLimit;
122
+ return Math.max(8, Math.min(parsedLimit, MOBILE_MAX_CATALOG_LIMIT));
123
+ };
124
+
125
+ const resolveInitialPackStickerLimit = () => {
126
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return PACK_STICKERS_INITIAL_LIMIT_DESKTOP;
127
+ const isMobileViewport = window.matchMedia('(max-width: 767px)').matches;
128
+ return isMobileViewport ? PACK_STICKERS_INITIAL_LIMIT_MOBILE : PACK_STICKERS_INITIAL_LIMIT_DESKTOP;
129
+ };
130
+
106
131
  const normalizeGoogleAuthState = (value) => {
107
132
  const user = value?.user && typeof value.user === 'object' ? value.user : null;
108
133
  const sub = String(user?.sub || '').trim();
@@ -266,7 +291,51 @@ const getPackEngagement = (pack) => {
266
291
  };
267
292
  };
268
293
 
269
- const getAvatarUrl = (name) => `https://api.dicebear.com/8.x/thumbs/svg?seed=${encodeURIComponent(String(name || 'omnizap'))}`;
294
+ const buildAvatarInitials = (value) => {
295
+ const base = String(value || '')
296
+ .trim()
297
+ .replace(/\s+/g, ' ');
298
+ if (!base) return 'OZ';
299
+ const parts = base.split(' ').filter(Boolean);
300
+ if (!parts.length) return 'OZ';
301
+ if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
302
+ return `${parts[0][0] || ''}${parts[parts.length - 1][0] || ''}`.toUpperCase();
303
+ };
304
+
305
+ const hashText = (value) => {
306
+ let hash = 0;
307
+ const input = String(value || '');
308
+ for (let index = 0; index < input.length; index += 1) {
309
+ hash = (hash * 31 + input.charCodeAt(index)) >>> 0;
310
+ }
311
+ return hash >>> 0;
312
+ };
313
+
314
+ const escapeXml = (value) =>
315
+ String(value || '').replace(/[&<>"']/g, (char) => {
316
+ if (char === '&') return '&amp;';
317
+ if (char === '<') return '&lt;';
318
+ if (char === '>') return '&gt;';
319
+ if (char === '"') return '&quot;';
320
+ return '&apos;';
321
+ });
322
+
323
+ const getAvatarUrl = (name) => {
324
+ const seed = String(name || 'OmniZap').trim() || 'OmniZap';
325
+ const cached = AVATAR_URL_CACHE.get(seed);
326
+ if (cached) return cached;
327
+
328
+ const paletteIndex = hashText(seed) % AVATAR_BG_PALETTE.length;
329
+ const background = AVATAR_BG_PALETTE[paletteIndex];
330
+ const initials = escapeXml(buildAvatarInitials(seed));
331
+ const ariaLabel = escapeXml(seed);
332
+ const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='80' height='80' viewBox='0 0 80 80' role='img' aria-label='${ariaLabel}'><rect width='80' height='80' rx='40' fill='${background}'/><text x='40' y='49' text-anchor='middle' font-family='Segoe UI, Roboto, Arial, sans-serif' font-size='30' font-weight='700' fill='#e2e8f0'>${initials}</text></svg>`;
333
+ const dataUrl = `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
334
+
335
+ if (AVATAR_URL_CACHE.size > 800) AVATAR_URL_CACHE.clear();
336
+ AVATAR_URL_CACHE.set(seed, dataUrl);
337
+ return dataUrl;
338
+ };
270
339
 
271
340
  const parseCatalogSearchState = (search = '') => {
272
341
  const params = new URLSearchParams(String(search || ''));
@@ -468,12 +537,53 @@ function UploadTaskWidget({ task, onClose }) {
468
537
  `;
469
538
  }
470
539
 
540
+ function LazyCatalogImage({ src, alt = '', className = '', eager = false, fallbackSrc = DEFAULT_STICKER_PLACEHOLDER_URL, rootMargin = '180px 0px', threshold = 0.01 }) {
541
+ const imageRef = useRef(null);
542
+ const resolvedSrc = String(src || '').trim() || fallbackSrc;
543
+ const [shouldLoad, setShouldLoad] = useState(() => Boolean(eager));
544
+
545
+ useEffect(() => {
546
+ if (eager || shouldLoad) return;
547
+ if (typeof window === 'undefined' || typeof window.IntersectionObserver !== 'function') {
548
+ setShouldLoad(true);
549
+ return;
550
+ }
551
+ const node = imageRef.current;
552
+ if (!node) {
553
+ setShouldLoad(true);
554
+ return;
555
+ }
556
+ const observer = new window.IntersectionObserver(
557
+ (entries) => {
558
+ const isVisible = entries.some((entry) => entry.isIntersecting || entry.intersectionRatio > 0);
559
+ if (isVisible) {
560
+ setShouldLoad(true);
561
+ observer.disconnect();
562
+ }
563
+ },
564
+ { rootMargin, threshold },
565
+ );
566
+ observer.observe(node);
567
+ return () => observer.disconnect();
568
+ }, [eager, shouldLoad, resolvedSrc, rootMargin, threshold]);
569
+
570
+ return html`<img
571
+ ref=${imageRef}
572
+ src=${shouldLoad ? resolvedSrc : LAZY_IMAGE_PLACEHOLDER_DATA_URL}
573
+ alt=${alt}
574
+ className=${className}
575
+ loading=${eager ? 'eager' : 'lazy'}
576
+ decoding="async"
577
+ fetchpriority=${eager ? 'high' : 'low'}
578
+ />`;
579
+ }
580
+
471
581
  function PackCard({ pack, index, onOpen, hasNsfwAccess = true, onRequireLogin }) {
472
582
  const isTrending = index < 4 || Number(pack?.sticker_count || 0) >= 30;
473
583
  const isNew = isRecent(pack?.created_at);
474
584
  const engagement = getPackEngagement(pack);
475
585
  const lockedByNsfw = isPackMarkedNsfw(pack) && !hasNsfwAccess;
476
- const coverUrl = lockedByNsfw ? NSFW_STICKER_PLACEHOLDER_URL : pack.cover_url || DEFAULT_STICKER_PLACEHOLDER_URL;
586
+ const coverUrl = lockedByNsfw ? NSFW_STICKER_PLACEHOLDER_URL : pack.cover_preview_url || pack.cover_url || DEFAULT_STICKER_PLACEHOLDER_URL;
477
587
  const handleOpen = () => {
478
588
  if (lockedByNsfw) {
479
589
  onRequireLogin?.();
@@ -485,14 +595,18 @@ function PackCard({ pack, index, onOpen, hasNsfwAccess = true, onRequireLogin })
485
595
  return html`
486
596
  <button type="button" onClick=${handleOpen} className="group w-full text-left rounded-2xl border border-slate-800 bg-slate-900/90 shadow-soft overflow-hidden transition-all duration-200 active:scale-[0.985] md:hover:scale-[1.02] hover:-translate-y-0.5 hover:shadow-lg touch-manipulation">
487
597
  <div className="relative aspect-[5/6] sm:aspect-[4/5] bg-slate-900 overflow-hidden">
488
- <img src=${coverUrl} alt=${`Capa de ${pack.name}`} className=${`w-full h-full object-cover transition-transform duration-300 ${lockedByNsfw ? 'blur-md scale-105' : 'md:group-hover:scale-[1.05] group-active:scale-[1.02]'}`} loading="lazy" />
598
+ <${LazyCatalogImage}
599
+ src=${coverUrl}
600
+ alt=${`Capa de ${pack.name}`}
601
+ className=${`w-full h-full object-cover transition-transform duration-300 ${lockedByNsfw ? 'blur-md scale-105' : 'md:group-hover:scale-[1.05] group-active:scale-[1.02]'}`}
602
+ />
489
603
  <div className="absolute inset-0 bg-gradient-to-t from-slate-950 via-slate-950/60 to-transparent"></div>
490
604
  <div className="absolute top-2 left-2 flex items-center gap-1">${lockedByNsfw ? html`<span className="rounded-full border border-amber-300/35 bg-amber-500/25 backdrop-blur px-1.5 py-0.5 text-[9px] font-bold text-amber-100">🔞 Login</span>` : null} ${isTrending ? html`<span className="rounded-full border border-emerald-300/30 bg-emerald-400/80 backdrop-blur px-1.5 py-0.5 text-[9px] font-bold text-slate-900">Trending</span>` : null} ${isNew ? html`<span className="rounded-full border border-white/15 bg-black/45 backdrop-blur px-1.5 py-0.5 text-[9px] font-semibold text-slate-100">Novo</span>` : null}</div>
491
605
 
492
606
  <div className="absolute inset-x-0 bottom-0 p-2">
493
607
  <h3 className="font-semibold text-sm leading-5 line-clamp-2">${pack.name || 'Pack sem nome'}</h3>
494
608
  <div className="mt-1 flex items-center gap-1.5 text-[10px] text-slate-300">
495
- <img src=${getAvatarUrl(pack.publisher)} alt="Criador" className="w-4 h-4 rounded-full bg-slate-700" loading="lazy" />
609
+ <${LazyCatalogImage} src=${getAvatarUrl(pack.publisher)} alt="Criador" className="w-4 h-4 rounded-full bg-slate-700" />
496
610
  <span className="truncate">${pack.publisher || 'Criador não informado'}</span>
497
611
  </div>
498
612
  <p className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[10px] text-slate-300">
@@ -533,7 +647,11 @@ function DiscoverPackRowItem({ pack, onOpen, rank = 0, hasNsfwAccess = true, onR
533
647
  };
534
648
  return html`
535
649
  <button type="button" onClick=${handleOpen} className="w-full flex items-center gap-2 rounded-xl border border-slate-800 bg-slate-900/50 px-2 py-1.5 text-left hover:bg-slate-800/90">
536
- <img src=${lockedByNsfw ? NSFW_STICKER_PLACEHOLDER_URL : pack.cover_url || DEFAULT_STICKER_PLACEHOLDER_URL} alt="" className=${`h-9 w-9 rounded-lg object-cover bg-slate-800 ${lockedByNsfw ? 'blur-sm' : ''}`} loading="lazy" />
650
+ <${LazyCatalogImage}
651
+ src=${lockedByNsfw ? NSFW_STICKER_PLACEHOLDER_URL : pack.cover_preview_url || pack.cover_url || DEFAULT_STICKER_PLACEHOLDER_URL}
652
+ alt=""
653
+ className=${`h-9 w-9 rounded-lg object-cover bg-slate-800 ${lockedByNsfw ? 'blur-sm' : ''}`}
654
+ />
537
655
  <span className="min-w-0 flex-1">
538
656
  <span className="block truncate text-xs font-medium text-slate-100">${rank > 0 ? `${rank}. ` : ''}${pack.name || 'Pack'}</span>
539
657
  <span className="block truncate text-[10px] text-slate-400"> ${lockedByNsfw ? '🔒 Entrar para desbloquear' : `${pack.publisher || '-'} · ❤️ ${shortNum(getPackEngagement(pack).likeCount)}`} </span>
@@ -557,7 +675,11 @@ function DiscoverPackMiniCard({ pack, onOpen, hasNsfwAccess = true, onRequireLog
557
675
  return html`
558
676
  <button type="button" onClick=${handleOpen} className="group w-[170px] shrink-0 overflow-hidden rounded-xl border border-slate-800 bg-slate-900/80 text-left">
559
677
  <div className="relative h-24 bg-slate-900">
560
- <img src=${lockedByNsfw ? NSFW_STICKER_PLACEHOLDER_URL : pack.cover_url || DEFAULT_STICKER_PLACEHOLDER_URL} alt="" className=${`h-full w-full object-cover transition-transform duration-200 ${lockedByNsfw ? 'blur-sm scale-105' : 'group-active:scale-[1.02]'}`} loading="lazy" />
678
+ <${LazyCatalogImage}
679
+ src=${lockedByNsfw ? NSFW_STICKER_PLACEHOLDER_URL : pack.cover_preview_url || pack.cover_url || DEFAULT_STICKER_PLACEHOLDER_URL}
680
+ alt=""
681
+ className=${`h-full w-full object-cover transition-transform duration-200 ${lockedByNsfw ? 'blur-sm scale-105' : 'group-active:scale-[1.02]'}`}
682
+ />
561
683
  <div className="absolute inset-0 bg-gradient-to-t from-slate-950/90 to-transparent"></div>
562
684
  ${lockedByNsfw ? html`<span className="absolute top-1.5 left-1.5 rounded-full border border-amber-300/35 bg-amber-500/25 px-1.5 py-0.5 text-[9px] font-semibold text-amber-100">🔞 Login</span>` : null}
563
685
  </div>
@@ -574,7 +696,7 @@ function DiscoverCreatorMiniCard({ creator, onPick }) {
574
696
  return html`
575
697
  <button type="button" onClick=${() => onPick(creator.publisher)} className="w-[190px] shrink-0 rounded-xl border border-slate-800 bg-slate-900/70 p-2 text-left hover:bg-slate-800/90">
576
698
  <div className="flex items-center gap-2">
577
- <img src=${getAvatarUrl(creator.publisher)} alt="" className="h-9 w-9 rounded-full bg-slate-800" />
699
+ <${LazyCatalogImage} src=${getAvatarUrl(creator.publisher)} alt="" className="h-9 w-9 rounded-full bg-slate-800" />
578
700
  <span className="min-w-0">
579
701
  <span className="block truncate text-xs font-semibold text-slate-100">${creator.publisher}</span>
580
702
  <span className="block truncate text-[10px] text-slate-400">${creator.packCount} packs · ❤️ ${shortNum(creator.likes)}</span>
@@ -1581,6 +1703,7 @@ function CreatorsRankingPage({ creators = [], loading = false, error = '', sort
1581
1703
  function StickerPreview({ item, onClose, onPrev, onNext, hasNsfwAccess = true, onRequireLogin }) {
1582
1704
  if (!item) return null;
1583
1705
  const lockedByNsfw = isStickerMarkedNsfw(item) && !hasNsfwAccess;
1706
+ const stickerPreviewSrc = item?.asset_preview_url || item?.asset_url || DEFAULT_STICKER_PLACEHOLDER_URL;
1584
1707
 
1585
1708
  const handleCopy = async () => {
1586
1709
  if (!item?.asset_url) return;
@@ -1594,7 +1717,7 @@ function StickerPreview({ item, onClose, onPrev, onNext, hasNsfwAccess = true, o
1594
1717
  <button type="button" className="absolute inset-0" aria-label="Fechar preview" onClick=${onClose}></button>
1595
1718
 
1596
1719
  <div className="relative w-full max-w-xl rounded-2xl border border-slate-700 bg-slate-900 p-3">
1597
- <img src=${lockedByNsfw ? NSFW_STICKER_PLACEHOLDER_URL : item.asset_url || DEFAULT_STICKER_PLACEHOLDER_URL} alt=${item.accessibility_label || 'Sticker'} className=${`w-full max-h-[70vh] object-contain rounded-xl bg-slate-950 ${lockedByNsfw ? 'blur-md' : ''}`} />
1720
+ <img src=${lockedByNsfw ? NSFW_STICKER_PLACEHOLDER_URL : stickerPreviewSrc} alt=${item.accessibility_label || 'Sticker'} className=${`w-full max-h-[70vh] object-contain rounded-xl bg-slate-950 ${lockedByNsfw ? 'blur-md' : ''}`} />
1598
1721
  ${lockedByNsfw
1599
1722
  ? html`
1600
1723
  <div className="absolute inset-x-3 top-3 bottom-[64px] flex items-center justify-center rounded-xl bg-slate-950/40 p-4">
@@ -1620,7 +1743,7 @@ function StickerPreview({ item, onClose, onPrev, onNext, hasNsfwAccess = true, o
1620
1743
  `;
1621
1744
  }
1622
1745
 
1623
- function PackPage({ pack, relatedPacks, onBack, onOpenRelated, onLike, onDislike, onTagClick, reactionLoading = '', reactionNotice = null, hasNsfwAccess = true, onRequireLogin }) {
1746
+ function PackPage({ pack, relatedPacks, relatedLoading = false, onLoadRelated, onBack, onOpenRelated, onLike, onDislike, onTagClick, reactionLoading = '', reactionNotice = null, hasNsfwAccess = true, onRequireLogin }) {
1624
1747
  if (!pack) {
1625
1748
  return html`
1626
1749
  <section className="space-y-4">
@@ -1636,12 +1759,21 @@ function PackPage({ pack, relatedPacks, onBack, onOpenRelated, onLike, onDislike
1636
1759
  const items = Array.isArray(pack?.items) ? pack.items : [];
1637
1760
  const tags = Array.isArray(pack?.tags) ? pack.tags : [];
1638
1761
  const packLockedByNsfw = isPackMarkedNsfw(pack) && !hasNsfwAccess;
1639
- const cover = packLockedByNsfw ? NSFW_STICKER_PLACEHOLDER_URL : pack?.cover_url || items?.[0]?.asset_url || DEFAULT_STICKER_PLACEHOLDER_URL;
1762
+ const cover = packLockedByNsfw ? NSFW_STICKER_PLACEHOLDER_URL : pack?.cover_preview_url || pack?.cover_url || items?.[0]?.asset_preview_url || items?.[0]?.asset_url || DEFAULT_STICKER_PLACEHOLDER_URL;
1640
1763
  const whatsappUrl = String(pack?.whatsapp?.url || '').trim();
1641
1764
  const engagement = getPackEngagement(pack);
1642
1765
  const hasReactionRequest = Boolean(reactionLoading);
1766
+ const initialVisibleCount = useMemo(() => resolveInitialPackStickerLimit(), []);
1767
+ const [visibleStickerCount, setVisibleStickerCount] = useState(initialVisibleCount);
1643
1768
  const [previewIndex, setPreviewIndex] = useState(-1);
1644
- const currentPreviewItem = previewIndex >= 0 ? items[previewIndex] : null;
1769
+ const visibleItems = useMemo(() => items.slice(0, Math.max(1, Number(visibleStickerCount || 0))), [items, visibleStickerCount]);
1770
+ const hasMoreVisibleItems = visibleItems.length < items.length;
1771
+ const currentPreviewItem = previewIndex >= 0 ? visibleItems[previewIndex] : null;
1772
+
1773
+ useEffect(() => {
1774
+ setVisibleStickerCount(initialVisibleCount);
1775
+ setPreviewIndex(-1);
1776
+ }, [pack?.pack_key, initialVisibleCount]);
1645
1777
 
1646
1778
  return html`
1647
1779
  <section className="space-y-4 pb-4">
@@ -1701,7 +1833,7 @@ function PackPage({ pack, relatedPacks, onBack, onOpenRelated, onLike, onDislike
1701
1833
  <section className="space-y-2.5">
1702
1834
  <div className="flex items-center justify-between gap-2">
1703
1835
  <h2 className="text-lg font-bold">Stickers do pack</h2>
1704
- <span className="text-xs text-slate-400">${items.length} itens</span>
1836
+ <span className="text-xs text-slate-400">${visibleItems.length}/${items.length} itens</span>
1705
1837
  </div>
1706
1838
 
1707
1839
  ${packLockedByNsfw
@@ -1712,14 +1844,21 @@ function PackPage({ pack, relatedPacks, onBack, onOpenRelated, onLike, onDislike
1712
1844
  <button type="button" onClick=${() => onRequireLogin?.()} className="mt-3 inline-flex h-9 items-center rounded-xl border border-amber-400/35 bg-amber-500/15 px-3 text-xs font-semibold text-amber-100">Entrar e desbloquear</button>
1713
1845
  </div>
1714
1846
  `
1715
- : items.length
1847
+ : visibleItems.length
1716
1848
  ? html`
1717
1849
  <div className="pack-stickers-grid gap-2 sm:gap-3">
1718
- ${items.map((item, index) => {
1850
+ ${visibleItems.map((item, index) => {
1719
1851
  const stickerLockedByNsfw = isStickerMarkedNsfw(item) && !hasNsfwAccess;
1852
+ const stickerSrc = item?.asset_preview_url || item?.asset_url || DEFAULT_STICKER_PLACEHOLDER_URL;
1720
1853
  return html`
1721
1854
  <button key=${item.sticker_id || item.position || index} type="button" onClick=${() => (stickerLockedByNsfw ? onRequireLogin?.() : setPreviewIndex(index))} className="pack-sticker-card group relative overflow-hidden rounded-xl border border-slate-800 bg-slate-900/80 text-left transition hover:-translate-y-0.5 hover:border-slate-600">
1722
- <img src=${stickerLockedByNsfw ? NSFW_STICKER_PLACEHOLDER_URL : item.asset_url || DEFAULT_STICKER_PLACEHOLDER_URL} alt=${item.accessibility_label || 'Sticker'} loading="lazy" className=${`w-full aspect-square object-contain bg-slate-950 transition-transform duration-300 ${stickerLockedByNsfw ? 'blur-md scale-105' : 'group-hover:scale-105'}`} />
1855
+ <${LazyCatalogImage}
1856
+ src=${stickerLockedByNsfw ? NSFW_STICKER_PLACEHOLDER_URL : stickerSrc}
1857
+ alt=${item.accessibility_label || 'Sticker'}
1858
+ className=${`w-full aspect-square object-contain bg-slate-950 transition-transform duration-300 ${stickerLockedByNsfw ? 'blur-md scale-105' : 'group-hover:scale-105'}`}
1859
+ rootMargin="40px 0px"
1860
+ threshold=${0.05}
1861
+ />
1723
1862
  ${stickerLockedByNsfw
1724
1863
  ? html`
1725
1864
  <div className="absolute inset-0 flex items-center justify-center bg-slate-950/35 p-2">
@@ -1731,6 +1870,19 @@ function PackPage({ pack, relatedPacks, onBack, onOpenRelated, onLike, onDislike
1731
1870
  `;
1732
1871
  })}
1733
1872
  </div>
1873
+ ${hasMoreVisibleItems
1874
+ ? html`
1875
+ <div className="flex justify-center pt-1">
1876
+ <button
1877
+ type="button"
1878
+ onClick=${() => setVisibleStickerCount((prev) => Math.min(items.length, Math.max(1, Number(prev || 0)) + PACK_STICKERS_LOAD_STEP))}
1879
+ className="inline-flex h-9 items-center rounded-xl border border-slate-700 bg-slate-900/80 px-3 text-xs text-slate-200 hover:bg-slate-800"
1880
+ >
1881
+ Carregar mais (${items.length - visibleItems.length} restantes)
1882
+ </button>
1883
+ </div>
1884
+ `
1885
+ : null}
1734
1886
  `
1735
1887
  : html`
1736
1888
  <div className="rounded-2xl border border-dashed border-slate-700 bg-slate-900/40 p-8 text-center">
@@ -1740,18 +1892,40 @@ function PackPage({ pack, relatedPacks, onBack, onOpenRelated, onLike, onDislike
1740
1892
  `}
1741
1893
  </section>
1742
1894
 
1743
- ${relatedPacks.length
1895
+ ${(relatedPacks.length || typeof onLoadRelated === 'function' || relatedLoading)
1744
1896
  ? html`
1745
1897
  <section className="space-y-3">
1746
1898
  <div className="flex items-center justify-between gap-2">
1747
1899
  <h2 className="text-lg font-bold">Packs relacionados</h2>
1748
- <span className="text-xs text-slate-500">${relatedPacks.length} sugestões</span>
1900
+ <span className="text-xs text-slate-500">${relatedPacks.length ? `${relatedPacks.length} sugestões` : 'sob demanda'}</span>
1749
1901
  </div>
1750
- <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-2.5 sm:gap-3">${relatedPacks.map((entry, index) => html`<div key=${entry.pack_key || entry.id} className="fade-card"><${PackCard} pack=${entry} index=${index} onOpen=${onOpenRelated} hasNsfwAccess=${hasNsfwAccess} onRequireLogin=${onRequireLogin} /></div>`)}</div>
1902
+ ${relatedPacks.length
1903
+ ? html`<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-2.5 sm:gap-3">${relatedPacks.map((entry, index) => html`<div key=${entry.pack_key || entry.id} className="fade-card"><${PackCard} pack=${entry} index=${index} onOpen=${onOpenRelated} hasNsfwAccess=${hasNsfwAccess} onRequireLogin=${onRequireLogin} /></div>`)}</div>`
1904
+ : html`
1905
+ <div className="rounded-2xl border border-slate-800 bg-slate-900/60 p-4 text-center">
1906
+ <button
1907
+ type="button"
1908
+ onClick=${() => onLoadRelated?.()}
1909
+ disabled=${relatedLoading}
1910
+ className="inline-flex h-9 items-center rounded-xl border border-slate-700 bg-slate-900 px-3 text-xs text-slate-200 hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-60"
1911
+ >
1912
+ ${relatedLoading ? 'Carregando relacionados...' : 'Carregar packs relacionados'}
1913
+ </button>
1914
+ </div>
1915
+ `}
1751
1916
  </section>
1752
1917
  `
1753
1918
  : null}
1754
- ${previewIndex >= 0 ? html` <${StickerPreview} item=${currentPreviewItem} onClose=${() => setPreviewIndex(-1)} onPrev=${() => setPreviewIndex((value) => (value <= 0 ? items.length - 1 : value - 1))} onNext=${() => setPreviewIndex((value) => (value >= items.length - 1 ? 0 : value + 1))} hasNsfwAccess=${hasNsfwAccess} onRequireLogin=${onRequireLogin} /> ` : null}
1919
+ ${previewIndex >= 0
1920
+ ? html` <${StickerPreview}
1921
+ item=${currentPreviewItem}
1922
+ onClose=${() => setPreviewIndex(-1)}
1923
+ onPrev=${() => setPreviewIndex((value) => (value <= 0 ? visibleItems.length - 1 : value - 1))}
1924
+ onNext=${() => setPreviewIndex((value) => (value >= visibleItems.length - 1 ? 0 : value + 1))}
1925
+ hasNsfwAccess=${hasNsfwAccess}
1926
+ onRequireLogin=${onRequireLogin}
1927
+ /> `
1928
+ : null}
1755
1929
  </section>
1756
1930
  `;
1757
1931
  }
@@ -1763,7 +1937,7 @@ function StickersApp() {
1763
1937
  webPath: root?.dataset.webPath || '/stickers',
1764
1938
  apiBasePath: root?.dataset.apiBasePath || '/api/sticker-packs',
1765
1939
  loginPath: root?.dataset.loginPath || '/login',
1766
- limit: parseIntSafe(root?.dataset.defaultLimit, 24),
1940
+ limit: resolveCatalogPageLimit(root?.dataset.defaultLimit),
1767
1941
  }),
1768
1942
  [root],
1769
1943
  );
@@ -1796,13 +1970,11 @@ function StickersApp() {
1796
1970
  const [reactionLoading, setReactionLoading] = useState('');
1797
1971
  const [reactionNotice, setReactionNotice] = useState(null);
1798
1972
  const [relatedPacks, setRelatedPacks] = useState([]);
1973
+ const [relatedPacksLoading, setRelatedPacksLoading] = useState(false);
1799
1974
  const [creatorRanking, setCreatorRanking] = useState([]);
1800
1975
  const [creatorRankingLoading, setCreatorRankingLoading] = useState(false);
1801
1976
  const [creatorRankingError, setCreatorRankingError] = useState('');
1802
1977
  const [creatorSort, setCreatorSort] = useState(normalizeCreatorsSort(initialCreatorsSearch.sort || DEFAULT_CREATORS_SORT));
1803
- const [globalMarketplaceStats, setGlobalMarketplaceStats] = useState(null);
1804
- const [globalMarketplaceStatsLoading, setGlobalMarketplaceStatsLoading] = useState(false);
1805
- const [globalMarketplaceStatsError, setGlobalMarketplaceStatsError] = useState('');
1806
1978
  const [isScrolled, setIsScrolled] = useState(false);
1807
1979
  const [supportInfo, setSupportInfo] = useState(null);
1808
1980
  const [uploadTask, setUploadTask] = useState(null);
@@ -1966,7 +2138,7 @@ function StickersApp() {
1966
2138
  if (completenessDelta !== 0) return completenessDelta;
1967
2139
  return b.growth - a.growth;
1968
2140
  })
1969
- .slice(0, 6)
2141
+ .slice(0, Math.max(DESKTOP_DISCOVER_GROWING_LIMIT, MOBILE_DISCOVER_CAROUSEL_LIMIT))
1970
2142
  .map((entry) => entry.pack);
1971
2143
  }, [packs]);
1972
2144
  const topWeekPacks = useMemo(
@@ -1981,7 +2153,7 @@ function StickersApp() {
1981
2153
  const sb = eb.openCount + eb.likeCount * 3 - eb.dislikeCount;
1982
2154
  return sb - sa;
1983
2155
  })
1984
- .slice(0, 10),
2156
+ .slice(0, Math.max(DESKTOP_DISCOVER_TOP_LIMIT, MOBILE_DISCOVER_CAROUSEL_LIMIT)),
1985
2157
  [packs],
1986
2158
  );
1987
2159
  const featuredCreators = useMemo(() => {
@@ -2052,14 +2224,13 @@ function StickersApp() {
2052
2224
  if (completenessDelta !== 0) return completenessDelta;
2053
2225
  return new Date(b?.created_at || b?.updated_at || 0).getTime() - new Date(a?.created_at || a?.updated_at || 0).getTime();
2054
2226
  })
2055
- .slice(0, 10),
2227
+ .slice(0, MOBILE_DISCOVER_CAROUSEL_LIMIT),
2056
2228
  [packs],
2057
2229
  );
2058
2230
 
2059
2231
  const hasAnyResult = packs.length > 0;
2060
2232
  const canGoCatalogPrev = catalogPage > FIRST_CATALOG_PAGE && !packsLoading;
2061
2233
  const canGoCatalogNext = packHasMore && !packsLoading;
2062
- const marketplaceGlobalStatsApiPath = '/api/marketplace/stats';
2063
2234
  const googleSessionApiPath = `${config.apiBasePath}/auth/google/session`;
2064
2235
  const myProfileApiPath = `${config.apiBasePath}/me`;
2065
2236
  const isProfileView = currentView === 'profile';
@@ -2424,41 +2595,6 @@ function StickersApp() {
2424
2595
  }
2425
2596
  };
2426
2597
 
2427
- const loadGlobalMarketplaceStats = async ({ silent = false } = {}) => {
2428
- if (!silent) setGlobalMarketplaceStatsLoading(true);
2429
- if (!silent) setGlobalMarketplaceStatsError('');
2430
- try {
2431
- const payload = await fetchJson(marketplaceGlobalStatsApiPath, { retry: 1 });
2432
- const source = payload?.data && typeof payload.data === 'object' ? payload.data : payload;
2433
- const series = Array.isArray(source?.series_last_7_days) ? source.series_last_7_days : [];
2434
- setGlobalMarketplaceStats({
2435
- totalPacks: safeNumber(source?.total_packs),
2436
- totalStickers: safeNumber(source?.total_stickers),
2437
- totalClicks: safeNumber(source?.total_clicks),
2438
- totalLikes: safeNumber(source?.total_likes),
2439
- packsLast7Days: safeNumber(source?.packs_last_7_days),
2440
- stickersWithoutPack: safeNumber(source?.stickers_without_pack),
2441
- clicksLast7Days: safeNumber(source?.clicks_last_7_days),
2442
- likesLast7Days: safeNumber(source?.likes_last_7_days),
2443
- cacheSeconds: safeNumber(source?.cache_seconds, 45),
2444
- updatedAt: String(source?.updated_at || ''),
2445
- stale: Boolean(source?.stale),
2446
- seriesLast7Days: series.map((entry) => ({
2447
- date: String(entry?.date || ''),
2448
- packsPublished: safeNumber(entry?.packs_published),
2449
- stickersCreated: safeNumber(entry?.stickers_created),
2450
- clicks: safeNumber(entry?.clicks),
2451
- likes: safeNumber(entry?.likes),
2452
- })),
2453
- });
2454
- setGlobalMarketplaceStatsError('');
2455
- } catch (err) {
2456
- setGlobalMarketplaceStatsError(err?.message || 'Falha ao carregar painel global.');
2457
- } finally {
2458
- if (!silent) setGlobalMarketplaceStatsLoading(false);
2459
- }
2460
- };
2461
-
2462
2598
  const loadCreatorRanking = async () => {
2463
2599
  setCreatorRankingLoading(true);
2464
2600
  setCreatorRankingError('');
@@ -2500,16 +2636,25 @@ function StickersApp() {
2500
2636
  };
2501
2637
 
2502
2638
  const loadRelatedPacksForPack = async (pack) => {
2639
+ const sourcePackKey = String(pack?.pack_key || '').trim();
2640
+ if (!sourcePackKey) return;
2503
2641
  const q = String(pack?.publisher || '').trim();
2504
2642
  const relatedParams = new URLSearchParams();
2505
2643
  relatedParams.set('visibility', 'public');
2506
- relatedParams.set('limit', '12');
2644
+ relatedParams.set('limit', '6');
2507
2645
  if (q) relatedParams.set('q', q);
2508
2646
  else if (Array.isArray(pack?.tags) && pack.tags[0]) relatedParams.set('categories', pack.tags[0]);
2509
2647
 
2510
- const relatedPayload = await fetchJson(`${config.apiBasePath}?${relatedParams.toString()}`);
2511
- const relatedList = (Array.isArray(relatedPayload?.data) ? relatedPayload.data : []).filter((entry) => entry.pack_key && entry.pack_key !== pack?.pack_key).slice(0, 8);
2512
- setRelatedPacks(relatedList);
2648
+ setRelatedPacksLoading(true);
2649
+ try {
2650
+ const relatedPayload = await fetchJson(`${config.apiBasePath}?${relatedParams.toString()}`);
2651
+ const relatedList = (Array.isArray(relatedPayload?.data) ? relatedPayload.data : []).filter((entry) => entry.pack_key && entry.pack_key !== sourcePackKey).slice(0, 4);
2652
+ setRelatedPacks(relatedList);
2653
+ } catch {
2654
+ setRelatedPacks([]);
2655
+ } finally {
2656
+ setRelatedPacksLoading(false);
2657
+ }
2513
2658
  };
2514
2659
 
2515
2660
  const tryLoadManagedPackDetail = async (packKey) => {
@@ -2527,6 +2672,7 @@ function StickersApp() {
2527
2672
  setPackLoading(true);
2528
2673
  setCurrentPack(null);
2529
2674
  setRelatedPacks([]);
2675
+ setRelatedPacksLoading(false);
2530
2676
  setError('');
2531
2677
 
2532
2678
  try {
@@ -2548,8 +2694,6 @@ function StickersApp() {
2548
2694
  }
2549
2695
 
2550
2696
  void registerPackInteraction(resolvedPackKey || packKey, 'open', { silent: true });
2551
-
2552
- await loadRelatedPacksForPack(pack);
2553
2697
  } catch (err) {
2554
2698
  setError(err?.message || 'Não foi possível abrir o pack');
2555
2699
  } finally {
@@ -2557,6 +2701,11 @@ function StickersApp() {
2557
2701
  }
2558
2702
  };
2559
2703
 
2704
+ const requestRelatedPacksForCurrentPack = async () => {
2705
+ if (!currentPack || relatedPacksLoading || relatedPacks.length) return;
2706
+ await loadRelatedPacksForPack(currentPack);
2707
+ };
2708
+
2560
2709
  const buildCatalogWebUrl = ({ q = appliedQuery, category = activeCategory, sort = sortBy, filter = catalogFilter, page = catalogPage } = {}) => {
2561
2710
  const params = new URLSearchParams();
2562
2711
  const safePage = Math.max(FIRST_CATALOG_PAGE, Number(page || FIRST_CATALOG_PAGE));
@@ -2632,6 +2781,7 @@ function StickersApp() {
2632
2781
  setCurrentPackKey('');
2633
2782
  setCurrentPack(null);
2634
2783
  setRelatedPacks([]);
2784
+ setRelatedPacksLoading(false);
2635
2785
  setSortPickerOpen(false);
2636
2786
  };
2637
2787
 
@@ -2654,6 +2804,7 @@ function StickersApp() {
2654
2804
  setCurrentPackKey('');
2655
2805
  setCurrentPack(null);
2656
2806
  setRelatedPacks([]);
2807
+ setRelatedPacksLoading(false);
2657
2808
  setError('');
2658
2809
  setSortPickerOpen(false);
2659
2810
  };
@@ -2704,6 +2855,7 @@ function StickersApp() {
2704
2855
  setCurrentPackKey('');
2705
2856
  setCurrentPack(null);
2706
2857
  setRelatedPacks([]);
2858
+ setRelatedPacksLoading(false);
2707
2859
  setError('');
2708
2860
  setSortPickerOpen(false);
2709
2861
  };
@@ -2758,6 +2910,7 @@ function StickersApp() {
2758
2910
  setCurrentPackKey('');
2759
2911
  setCurrentPack(null);
2760
2912
  setRelatedPacks([]);
2913
+ setRelatedPacksLoading(false);
2761
2914
  setError('');
2762
2915
  setSortPickerOpen(false);
2763
2916
  };
@@ -3215,14 +3368,6 @@ function StickersApp() {
3215
3368
  };
3216
3369
  }, [config.apiBasePath]);
3217
3370
 
3218
- useEffect(() => {
3219
- void loadGlobalMarketplaceStats({ silent: false });
3220
- const intervalId = window.setInterval(() => {
3221
- void loadGlobalMarketplaceStats({ silent: true });
3222
- }, 60 * 1000);
3223
- return () => window.clearInterval(intervalId);
3224
- }, [marketplaceGlobalStatsApiPath]);
3225
-
3226
3371
  useEffect(() => {
3227
3372
  if (currentView !== 'catalog' || currentPackKey) return;
3228
3373
  const nextUrl = buildCatalogWebUrl();
@@ -3461,7 +3606,7 @@ function StickersApp() {
3461
3606
  <header className=${`sticky top-0 z-30 border-b border-slate-800 bg-slate-950/95 backdrop-blur transition-shadow ${isScrolled ? 'shadow-[0_8px_24px_rgba(2,6,23,0.45)]' : ''}`}>
3462
3607
  <div className="max-w-7xl mx-auto h-14 px-3 flex items-center gap-2.5">
3463
3608
  <a href="/" className="shrink-0 flex items-center gap-2">
3464
- <img src="https://iili.io/FC3FABe.jpg" alt="OmniZap" className="w-7 h-7 rounded-full border border-slate-700" />
3609
+ <img src=${OMNIZAP_LOGO_DATA_URL} alt="OmniZap" className="w-7 h-7 rounded-full border border-slate-700" decoding="async" />
3465
3610
  <span className="hidden sm:inline text-sm font-semibold">OmniZap</span>
3466
3611
  </a>
3467
3612
 
@@ -3536,7 +3681,7 @@ function StickersApp() {
3536
3681
  : isCreatorsView
3537
3682
  ? html` <${CreatorsRankingPage} creators=${sortedCreatorRanking} loading=${creatorRankingLoading} error=${creatorRankingError} sort=${creatorSort} onSortChange=${handleCreatorsSortChange} onBack=${goCatalog} onRetry=${loadCreatorRanking} onOpenCreator=${openCreatorProfileFromRanking} onOpenPack=${openPack} /> `
3538
3683
  : currentPackKey
3539
- ? html` ${packLoading ? html`<${PackPageSkeleton} />` : html`<${PackPage} pack=${currentPack} relatedPacks=${relatedPacks} onBack=${goCatalog} onOpenRelated=${openPack} onLike=${handleLike} onDislike=${handleDislike} onTagClick=${openCatalogTagFilter} reactionLoading=${reactionLoading} reactionNotice=${reactionNotice} hasNsfwAccess=${hasNsfwAccess} onRequireLogin=${requestNsfwUnlock} />`} `
3684
+ ? html` ${packLoading ? html`<${PackPageSkeleton} />` : html`<${PackPage} pack=${currentPack} relatedPacks=${relatedPacks} relatedLoading=${relatedPacksLoading} onLoadRelated=${requestRelatedPacksForCurrentPack} onBack=${goCatalog} onOpenRelated=${openPack} onLike=${handleLike} onDislike=${handleDislike} onTagClick=${openCatalogTagFilter} reactionLoading=${reactionLoading} reactionNotice=${reactionNotice} hasNsfwAccess=${hasNsfwAccess} onRequireLogin=${requestNsfwUnlock} />`} `
3540
3685
  : html`
3541
3686
  <div className="lg:grid lg:grid-cols-[220px_minmax(0,1fr)] lg:gap-4">
3542
3687
  <aside className="hidden lg:block">
@@ -3576,9 +3721,6 @@ function StickersApp() {
3576
3721
  <p className="text-[11px] uppercase tracking-wide text-slate-400">Descobrir</p>
3577
3722
  <h3 className="text-sm font-semibold text-slate-100">Painel oficial do marketplace</h3>
3578
3723
  </div>
3579
- <div className="flex items-center gap-2">
3580
- ${globalMarketplaceStatsLoading && !globalMarketplaceStats ? html`<span className="inline-flex h-8 items-center rounded-lg border border-slate-700 bg-slate-900/60 px-3 text-[11px] text-slate-300">Carregando métricas...</span>` : null} ${globalMarketplaceStatsError ? html`<span className="inline-flex h-8 items-center rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 text-[11px] text-amber-100">Fallback local</span>` : null}
3581
- </div>
3582
3724
  </div>
3583
3725
 
3584
3726
  <div className="mt-2 flex flex-wrap gap-1.5">
@@ -3591,9 +3733,9 @@ function StickersApp() {
3591
3733
 
3592
3734
  <div className="mt-2 hidden lg:block">
3593
3735
  ${discoverTab === 'growing'
3594
- ? html` <div className="grid grid-cols-1 xl:grid-cols-2 gap-2">${growingNowPacks.slice(0, 6).map((entry) => html`<${DiscoverPackRowItem} key=${`grow-${entry.pack_key}`} pack=${entry} onOpen=${openPack} hasNsfwAccess=${hasNsfwAccess} onRequireLogin=${requestNsfwUnlock} />`)}</div> `
3736
+ ? html` <div className="grid grid-cols-1 xl:grid-cols-2 gap-2">${growingNowPacks.slice(0, DESKTOP_DISCOVER_GROWING_LIMIT).map((entry) => html`<${DiscoverPackRowItem} key=${`grow-${entry.pack_key}`} pack=${entry} onOpen=${openPack} hasNsfwAccess=${hasNsfwAccess} onRequireLogin=${requestNsfwUnlock} />`)}</div> `
3595
3737
  : discoverTab === 'top'
3596
- ? html` <div className="grid grid-cols-1 xl:grid-cols-2 gap-2">${topWeekPacks.slice(0, 8).map((entry, idx) => html`<${DiscoverPackRowItem} key=${`top-${entry.pack_key}`} pack=${entry} onOpen=${openPack} rank=${idx + 1} hasNsfwAccess=${hasNsfwAccess} onRequireLogin=${requestNsfwUnlock} />`)}</div> `
3738
+ ? html` <div className="grid grid-cols-1 xl:grid-cols-2 gap-2">${topWeekPacks.slice(0, DESKTOP_DISCOVER_TOP_LIMIT).map((entry, idx) => html`<${DiscoverPackRowItem} key=${`top-${entry.pack_key}`} pack=${entry} onOpen=${openPack} rank=${idx + 1} hasNsfwAccess=${hasNsfwAccess} onRequireLogin=${requestNsfwUnlock} />`)}</div> `
3597
3739
  : html`
3598
3740
  <div className="grid grid-cols-1 xl:grid-cols-2 gap-2">
3599
3741
  ${featuredCreators.map(
@@ -3628,7 +3770,7 @@ function StickersApp() {
3628
3770
  <span aria-hidden="true">↗</span>
3629
3771
  </button>
3630
3772
  </div>
3631
- <div className="flex gap-2 overflow-x-auto pb-1">${growingNowPacks.slice(0, 8).map((entry) => html`<${DiscoverPackMiniCard} key=${`mobile-grow-${entry.pack_key}`} pack=${entry} onOpen=${openPack} hasNsfwAccess=${hasNsfwAccess} onRequireLogin=${requestNsfwUnlock} />`)}</div>
3773
+ <div className="flex gap-2 overflow-x-auto pb-1">${growingNowPacks.slice(0, MOBILE_DISCOVER_CAROUSEL_LIMIT).map((entry) => html`<${DiscoverPackMiniCard} key=${`mobile-grow-${entry.pack_key}`} pack=${entry} onOpen=${openPack} hasNsfwAccess=${hasNsfwAccess} onRequireLogin=${requestNsfwUnlock} />`)}</div>
3632
3774
  </section>
3633
3775
  <section className="space-y-1.5">
3634
3776
  <div className="flex items-center justify-between">
@@ -3643,7 +3785,7 @@ function StickersApp() {
3643
3785
  <span aria-hidden="true">⇅</span>
3644
3786
  </button>
3645
3787
  </div>
3646
- <div className="flex gap-2 overflow-x-auto pb-1">${recentPublishedPacks.slice(0, 8).map((entry) => html`<${DiscoverPackMiniCard} key=${`mobile-new-${entry.pack_key}`} pack=${entry} onOpen=${openPack} hasNsfwAccess=${hasNsfwAccess} onRequireLogin=${requestNsfwUnlock} />`)}</div>
3788
+ <div className="flex gap-2 overflow-x-auto pb-1">${recentPublishedPacks.slice(0, MOBILE_DISCOVER_CAROUSEL_LIMIT).map((entry) => html`<${DiscoverPackMiniCard} key=${`mobile-new-${entry.pack_key}`} pack=${entry} onOpen=${openPack} hasNsfwAccess=${hasNsfwAccess} onRequireLogin=${requestNsfwUnlock} />`)}</div>
3647
3789
  </section>
3648
3790
  </div>
3649
3791
  `
@@ -3654,7 +3796,7 @@ function StickersApp() {
3654
3796
  <h4 className="text-xs font-semibold text-slate-200">🏆 Top 10 da semana</h4>
3655
3797
  <button type="button" onClick=${() => openCatalogWithState({ q: '', category: '', sort: 'trending', filter: '', push: true })} className="text-[10px] text-cyan-300">ver lista</button>
3656
3798
  </div>
3657
- <div className="flex gap-2 overflow-x-auto pb-1">${topWeekPacks.slice(0, 8).map((entry) => html`<${DiscoverPackMiniCard} key=${`mobile-top-${entry.pack_key}`} pack=${entry} onOpen=${openPack} hasNsfwAccess=${hasNsfwAccess} onRequireLogin=${requestNsfwUnlock} />`)}</div>
3799
+ <div className="flex gap-2 overflow-x-auto pb-1">${topWeekPacks.slice(0, MOBILE_DISCOVER_CAROUSEL_LIMIT).map((entry) => html`<${DiscoverPackMiniCard} key=${`mobile-top-${entry.pack_key}`} pack=${entry} onOpen=${openPack} hasNsfwAccess=${hasNsfwAccess} onRequireLogin=${requestNsfwUnlock} />`)}</div>
3658
3800
  </section>
3659
3801
  `
3660
3802
  : html`
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { createHash, randomUUID } from 'node:crypto';
4
+ import { spawn } from 'node:child_process';
4
5
  import { URL } from 'node:url';
5
6
 
6
7
  import { executeQuery, pool, TABLES } from '../../../database/index.js';
@@ -109,7 +110,7 @@ const CATALOG_TEMPLATE_PATH = path.join(CATALOG_PUBLIC_DIR, 'stickers', 'index.h
109
110
  const CREATE_PACK_TEMPLATE_PATH = path.join(CATALOG_PUBLIC_DIR, 'stickers', 'create', 'index.html');
110
111
  const CATALOG_STYLES_FILE_PATH = path.join(CATALOG_PUBLIC_DIR, 'css', 'styles.css');
111
112
  const CATALOG_SCRIPT_FILE_PATH = path.join(CATALOG_PUBLIC_DIR, 'js', 'catalog.js');
112
- const DEFAULT_LIST_LIMIT = clampInt(process.env.STICKER_WEB_LIST_LIMIT, 24, 1, 60);
113
+ const DEFAULT_LIST_LIMIT = clampInt(process.env.STICKER_WEB_LIST_LIMIT, 16, 1, 60);
113
114
  const MAX_LIST_LIMIT = clampInt(process.env.STICKER_WEB_LIST_MAX_LIMIT, 60, 1, 100);
114
115
  const DEFAULT_ORPHAN_LIST_LIMIT = clampInt(process.env.STICKER_ORPHAN_LIST_LIMIT, 120, 1, 300);
115
116
  const MAX_ORPHAN_LIST_LIMIT = clampInt(process.env.STICKER_ORPHAN_LIST_MAX_LIMIT, 300, 1, 500);
@@ -192,6 +193,12 @@ const WEB_DRAFT_CLEANUP_RUN_INTERVAL_MS = Math.max(60 * 1000, Number(process.env
192
193
  const WEB_UPLOAD_ID_MAX_LENGTH = 120;
193
194
  const WEB_VISITOR_COOKIE_TTL_SECONDS = clampInt(process.env.WEB_VISITOR_COOKIE_TTL_SECONDS, 60 * 60 * 24 * 365, 60 * 60, 60 * 60 * 24 * 3650);
194
195
  const WEB_SESSION_COOKIE_TTL_SECONDS = clampInt(process.env.WEB_SESSION_COOKIE_TTL_SECONDS, 60 * 60 * 24 * 30, 30 * 60, 60 * 60 * 24 * 365);
196
+ const STICKER_PREVIEW_SIDE_PX = clampInt(process.env.STICKER_PREVIEW_SIDE_PX, 112, 96, 512);
197
+ const STICKER_PREVIEW_QUALITY = clampInt(process.env.STICKER_PREVIEW_QUALITY, 20, 10, 80);
198
+ const STICKER_PREVIEW_TIMEOUT_MS = clampInt(process.env.STICKER_PREVIEW_TIMEOUT_MS, 2500, 500, 12000);
199
+ const STICKER_PREVIEW_CACHE_TTL_MS = clampInt(process.env.STICKER_PREVIEW_CACHE_TTL_MS, 6 * 60 * 60 * 1000, 60 * 1000, 7 * 24 * 60 * 60 * 1000);
200
+ const STICKER_PREVIEW_CACHE_MAX_ITEMS = clampInt(process.env.STICKER_PREVIEW_CACHE_MAX_ITEMS, 2000, 100, 20000);
201
+ const STICKER_PREVIEW_TEMP_DIR = path.join(process.cwd(), 'temp', 'stickers', 'web-preview');
195
202
  const staleDraftCleanupState = {
196
203
  running: false,
197
204
  lastRunAt: 0,
@@ -226,6 +233,7 @@ const HOME_MARKETPLACE_STATS_CACHE = new Map();
226
233
  const CATALOG_LIST_CACHE = new Map();
227
234
  const CATALOG_CREATOR_RANKING_CACHE = new Map();
228
235
  const CATALOG_PACK_PAYLOAD_CACHE = new Map();
236
+ const STICKER_PREVIEW_CACHE = new Map();
229
237
  const SYSTEM_SUMMARY_CACHE = {
230
238
  expiresAt: 0,
231
239
  value: null,
@@ -877,13 +885,13 @@ const googleWebAuth = createGoogleWebAuthService({
877
885
  const { upsertGoogleWebUserRecord, resolveGoogleWebSessionFromRequest, mapGoogleSessionResponseData, handleGoogleAuthSessionRequest, revokeGoogleWebSessionsByIdentity } = googleWebAuth;
878
886
  revokeGoogleWebSessionsByIdentityBridge = revokeGoogleWebSessionsByIdentity;
879
887
 
880
- const sendAsset = (req, res, buffer, mimetype = 'image/webp') => {
888
+ const sendAsset = (req, res, buffer, mimetype = 'image/webp', cacheControlOverride = '') => {
881
889
  const maxAgeSeconds = Math.max(60 * 60 * 24, ASSET_CACHE_SECONDS);
882
890
  const staleWhileRevalidateSeconds = Math.min(60 * 60 * 24 * 7, Math.max(300, maxAgeSeconds));
883
891
  res.statusCode = 200;
884
892
  res.setHeader('Content-Type', mimetype);
885
893
  res.setHeader('Content-Length', String(buffer.length));
886
- res.setHeader('Cache-Control', `public, max-age=${maxAgeSeconds}, stale-while-revalidate=${staleWhileRevalidateSeconds}`);
894
+ res.setHeader('Cache-Control', cacheControlOverride || `public, max-age=${maxAgeSeconds}, stale-while-revalidate=${staleWhileRevalidateSeconds}`);
887
895
  if (req.method === 'HEAD') {
888
896
  res.end();
889
897
  return;
@@ -1151,6 +1159,15 @@ const fetchPrometheusSummary = async () => {
1151
1159
  const buildPackApiUrl = (packKey) => `${STICKER_API_BASE_PATH}/${encodeURIComponent(packKey)}`;
1152
1160
  const buildPackWebUrl = (packKey) => `${STICKER_WEB_PATH}/${encodeURIComponent(packKey)}`;
1153
1161
  const buildStickerAssetUrl = (packKey, stickerId) => `${STICKER_API_BASE_PATH}/${encodeURIComponent(packKey)}/stickers/${encodeURIComponent(stickerId)}.webp`;
1162
+ const buildStickerAssetPreviewUrl = (packKey, stickerId, versionToken = '') => {
1163
+ const params = new URLSearchParams();
1164
+ params.set('variant', 'preview');
1165
+ params.set('sz', String(STICKER_PREVIEW_SIDE_PX));
1166
+ params.set('q', String(STICKER_PREVIEW_QUALITY));
1167
+ const normalizedVersion = String(versionToken || '').trim();
1168
+ if (normalizedVersion) params.set('v', normalizedVersion);
1169
+ return `${buildStickerAssetUrl(packKey, stickerId)}?${params.toString()}`;
1170
+ };
1154
1171
  const buildOrphanStickersApiUrl = () => STICKER_ORPHAN_API_PATH;
1155
1172
  const buildDataAssetApiBaseUrl = () => `${STICKER_API_BASE_PATH}/data-files`;
1156
1173
  const CATALOG_STYLES_WEB_PATH = `${STICKER_WEB_PATH}/assets/styles.css`;
@@ -1359,6 +1376,122 @@ const resolveExtensionFromMimetype = (mimetype) => {
1359
1376
  return 'bin';
1360
1377
  };
1361
1378
 
1379
+ const isPreviewVariantRequested = (url) => {
1380
+ const variant = String(url?.searchParams?.get('variant') || url?.searchParams?.get('mode') || '')
1381
+ .trim()
1382
+ .toLowerCase();
1383
+ if (['preview', 'thumb', 'thumbnail', 'small'].includes(variant)) return true;
1384
+
1385
+ const previewFlag = String(url?.searchParams?.get('preview') || '')
1386
+ .trim()
1387
+ .toLowerCase();
1388
+ return ['1', 'true', 'yes', 'y', 'on'].includes(previewFlag);
1389
+ };
1390
+
1391
+ const getStickerPreviewFromCache = (cacheKey) => {
1392
+ const entry = STICKER_PREVIEW_CACHE.get(cacheKey);
1393
+ if (!entry) return null;
1394
+ if (entry.expiresAt <= Date.now()) {
1395
+ STICKER_PREVIEW_CACHE.delete(cacheKey);
1396
+ return null;
1397
+ }
1398
+ return entry.buffer;
1399
+ };
1400
+
1401
+ const saveStickerPreviewToCache = (cacheKey, buffer) => {
1402
+ if (!cacheKey || !Buffer.isBuffer(buffer) || !buffer.length) return;
1403
+ if (STICKER_PREVIEW_CACHE.size >= STICKER_PREVIEW_CACHE_MAX_ITEMS) {
1404
+ const overflow = STICKER_PREVIEW_CACHE.size - STICKER_PREVIEW_CACHE_MAX_ITEMS + 1;
1405
+ const keys = STICKER_PREVIEW_CACHE.keys();
1406
+ for (let index = 0; index < overflow; index += 1) {
1407
+ const next = keys.next();
1408
+ if (next.done) break;
1409
+ STICKER_PREVIEW_CACHE.delete(next.value);
1410
+ }
1411
+ }
1412
+ STICKER_PREVIEW_CACHE.set(cacheKey, {
1413
+ buffer,
1414
+ expiresAt: Date.now() + STICKER_PREVIEW_CACHE_TTL_MS,
1415
+ });
1416
+ };
1417
+
1418
+ const runPreviewFfmpeg = (args, timeoutMs = STICKER_PREVIEW_TIMEOUT_MS) =>
1419
+ new Promise((resolve, reject) => {
1420
+ const child = spawn('ffmpeg', args, { stdio: ['ignore', 'ignore', 'pipe'] });
1421
+ let stderr = '';
1422
+ let timedOut = false;
1423
+
1424
+ const timer = setTimeout(() => {
1425
+ timedOut = true;
1426
+ try {
1427
+ child.kill('SIGTERM');
1428
+ } catch {}
1429
+ setTimeout(() => {
1430
+ try {
1431
+ child.kill('SIGKILL');
1432
+ } catch {}
1433
+ }, 1200);
1434
+ }, Math.max(400, Number(timeoutMs || STICKER_PREVIEW_TIMEOUT_MS)));
1435
+
1436
+ child.stderr.on('data', (chunk) => {
1437
+ stderr = `${stderr}${String(chunk || '')}`.slice(-16 * 1024);
1438
+ });
1439
+
1440
+ child.on('error', (error) => {
1441
+ clearTimeout(timer);
1442
+ reject(error);
1443
+ });
1444
+
1445
+ child.on('close', (code) => {
1446
+ clearTimeout(timer);
1447
+ if (timedOut) {
1448
+ const timeoutError = new Error('preview_ffmpeg_timeout');
1449
+ timeoutError.code = 'ETIMEDOUT';
1450
+ timeoutError.stderr = stderr;
1451
+ reject(timeoutError);
1452
+ return;
1453
+ }
1454
+ if (code !== 0) {
1455
+ const processError = new Error(`preview_ffmpeg_failed_code_${code}`);
1456
+ processError.code = code;
1457
+ processError.stderr = stderr;
1458
+ reject(processError);
1459
+ return;
1460
+ }
1461
+ resolve();
1462
+ });
1463
+ });
1464
+
1465
+ const generateStickerPreviewBuffer = async ({ sourceBuffer, mimetype = 'image/webp', cacheKey = '' } = {}) => {
1466
+ if (!Buffer.isBuffer(sourceBuffer) || !sourceBuffer.length) return null;
1467
+ if (cacheKey) {
1468
+ const cached = getStickerPreviewFromCache(cacheKey);
1469
+ if (cached) return cached;
1470
+ }
1471
+
1472
+ const uniqueId = randomUUID();
1473
+ const extension = resolveExtensionFromMimetype(mimetype || 'image/webp');
1474
+ const inputPath = path.join(STICKER_PREVIEW_TEMP_DIR, `${uniqueId}.in.${extension}`);
1475
+ const outputPath = path.join(STICKER_PREVIEW_TEMP_DIR, `${uniqueId}.preview.webp`);
1476
+
1477
+ try {
1478
+ await fs.mkdir(STICKER_PREVIEW_TEMP_DIR, { recursive: true });
1479
+ await fs.writeFile(inputPath, sourceBuffer);
1480
+ const side = Math.max(96, Math.min(512, Number(STICKER_PREVIEW_SIDE_PX || 112)));
1481
+ const quality = Math.max(10, Math.min(80, Number(STICKER_PREVIEW_QUALITY || 20)));
1482
+ const filter = `scale=if(gte(iw,ih),${side},-1):if(gte(iw,ih),-1,${side}):flags=lanczos`;
1483
+ await runPreviewFfmpeg(['-y', '-i', inputPath, '-vf', filter, '-frames:v', '1', '-vcodec', 'libwebp', '-lossless', '0', '-q:v', String(quality), '-compression_level', '6', '-preset', 'picture', '-an', outputPath], STICKER_PREVIEW_TIMEOUT_MS);
1484
+ const previewBuffer = await fs.readFile(outputPath);
1485
+ if (cacheKey && previewBuffer.length) {
1486
+ saveStickerPreviewToCache(cacheKey, previewBuffer);
1487
+ }
1488
+ return previewBuffer.length ? previewBuffer : null;
1489
+ } finally {
1490
+ await fs.unlink(inputPath).catch(() => {});
1491
+ await fs.unlink(outputPath).catch(() => {});
1492
+ }
1493
+ };
1494
+
1362
1495
  const convertUploadMediaToWebp = async ({ ownerJid, buffer, mimetype }) => {
1363
1496
  const normalizedMimetype =
1364
1497
  String(mimetype || '')
@@ -1649,6 +1782,7 @@ const mapPackSummary = (pack, engagement = null, signals = null) => {
1649
1782
  const safeEngagement = engagement || getEmptyStickerPackEngagement();
1650
1783
  const metadata = parsePackDescriptionMetadata(pack.description);
1651
1784
  const stickerCount = Number(pack.sticker_count || 0);
1785
+ const coverVersionToken = toIsoOrNull(pack.updated_at) || toIsoOrNull(pack.created_at) || '';
1652
1786
  return {
1653
1787
  id: pack.id,
1654
1788
  pack_key: pack.pack_key,
@@ -1661,6 +1795,7 @@ const mapPackSummary = (pack, engagement = null, signals = null) => {
1661
1795
  is_complete: stickerCount >= PACK_CREATE_MAX_ITEMS,
1662
1796
  cover_sticker_id: pack.cover_sticker_id || null,
1663
1797
  cover_url: pack.cover_sticker_id ? buildStickerAssetUrl(pack.pack_key, pack.cover_sticker_id) : null,
1798
+ cover_preview_url: pack.cover_sticker_id ? buildStickerAssetPreviewUrl(pack.pack_key, pack.cover_sticker_id, coverVersionToken) : null,
1664
1799
  api_url: buildPackApiUrl(pack.pack_key),
1665
1800
  web_url: buildPackWebUrl(pack.pack_key),
1666
1801
  whatsapp: buildPackWhatsAppInfo(pack),
@@ -1756,7 +1891,7 @@ const mapPackDetails = (pack, items, { byAssetClassification = new Map(), packCl
1756
1891
  tags: mergedTags,
1757
1892
  };
1758
1893
  const packIsNsfw = isPackSummaryMarkedNsfw(packPreview);
1759
- const safeSummary = hideSensitiveAssets && packIsNsfw ? { ...summary, cover_url: null } : summary;
1894
+ const safeSummary = hideSensitiveAssets && packIsNsfw ? { ...summary, cover_url: null, cover_preview_url: null } : summary;
1760
1895
 
1761
1896
  return {
1762
1897
  ...safeSummary,
@@ -1764,6 +1899,8 @@ const mapPackDetails = (pack, items, { byAssetClassification = new Map(), packCl
1764
1899
  items: items.map((item) => {
1765
1900
  const decoratedItemClassification = decorateStickerClassification(byAssetClassification.get(item.sticker_id) || null);
1766
1901
  const itemIsNsfw = isClassificationMarkedNsfw(decoratedItemClassification);
1902
+ const hideAsset = hideSensitiveAssets && (packIsNsfw || itemIsNsfw);
1903
+ const previewVersionToken = String(item?.asset?.id || item?.asset?.size_bytes || item?.created_at || pack?.updated_at || '').trim();
1767
1904
  return {
1768
1905
  // `tags` facilita renderização direta no front sem precisar reprocessar score.
1769
1906
  id: item.id,
@@ -1772,7 +1909,8 @@ const mapPackDetails = (pack, items, { byAssetClassification = new Map(), packCl
1772
1909
  emojis: Array.isArray(item.emojis) ? item.emojis : [],
1773
1910
  accessibility_label: item.accessibility_label || null,
1774
1911
  created_at: toIsoOrNull(item.created_at),
1775
- asset_url: hideSensitiveAssets && (packIsNsfw || itemIsNsfw) ? null : buildStickerAssetUrl(pack.pack_key, item.sticker_id),
1912
+ asset_url: hideAsset ? null : buildStickerAssetUrl(pack.pack_key, item.sticker_id),
1913
+ asset_preview_url: hideAsset ? null : buildStickerAssetPreviewUrl(pack.pack_key, item.sticker_id, previewVersionToken),
1776
1914
  tags: decoratedItemClassification?.tags || [],
1777
1915
  is_nsfw: itemIsNsfw,
1778
1916
  asset: item.asset
@@ -1823,7 +1961,7 @@ const toSummaryEntry = (entry, { hideSensitiveCover = false } = {}) => {
1823
1961
  tags: mergeUniqueTags(entry.packClassification?.tags || [], parsePackDescriptionMetadata(entry.pack?.description).tags),
1824
1962
  };
1825
1963
  const isNsfw = isPackSummaryMarkedNsfw(summary);
1826
- const safeSummary = hideSensitiveCover && isNsfw ? { ...summary, cover_url: null } : summary;
1964
+ const safeSummary = hideSensitiveCover && isNsfw ? { ...summary, cover_url: null, cover_preview_url: null } : summary;
1827
1965
  return {
1828
1966
  ...safeSummary,
1829
1967
  is_nsfw: isNsfw,
@@ -3337,6 +3475,7 @@ const handleMyProfileRequest = async (req, res, url = null) => {
3337
3475
  ...safeSummary,
3338
3476
  is_publicly_visible: publicVisible,
3339
3477
  cover_url: publicVisible ? safeSummary.cover_url : null,
3478
+ cover_preview_url: publicVisible ? safeSummary.cover_preview_url : null,
3340
3479
  };
3341
3480
  });
3342
3481
 
@@ -6062,9 +6201,10 @@ const handleDetailsRequest = async (req, res, packKey, url) => {
6062
6201
  });
6063
6202
  };
6064
6203
 
6065
- const handleAssetRequest = async (req, res, packKey, stickerToken) => {
6204
+ const handleAssetRequest = async (req, res, packKey, stickerToken, url) => {
6066
6205
  const normalizedPackKey = sanitizeText(packKey, 160, { allowEmpty: false });
6067
6206
  const normalizedStickerId = sanitizeText(stripWebpExtension(stickerToken), 36, { allowEmpty: false });
6207
+ const previewVariant = isPreviewVariantRequested(url);
6068
6208
 
6069
6209
  if (!normalizedPackKey || !normalizedStickerId) {
6070
6210
  sendJson(req, res, 400, { error: 'Parametros invalidos.' });
@@ -6114,11 +6254,13 @@ const handleAssetRequest = async (req, res, packKey, stickerToken) => {
6114
6254
  }
6115
6255
  }
6116
6256
 
6117
- const externalAssetUrl = await getStickerAssetExternalUrl(item.asset, {
6118
- secure: true,
6119
- expiresInSeconds: Math.max(60, Math.min(3600, Number(process.env.STICKER_OBJECT_STORAGE_SIGNED_URL_TTL_SECONDS) || 300)),
6120
- }).catch(() => null);
6121
- if (externalAssetUrl) {
6257
+ const externalAssetUrl = previewVariant
6258
+ ? null
6259
+ : await getStickerAssetExternalUrl(item.asset, {
6260
+ secure: true,
6261
+ expiresInSeconds: Math.max(60, Math.min(3600, Number(process.env.STICKER_OBJECT_STORAGE_SIGNED_URL_TTL_SECONDS) || 300)),
6262
+ }).catch(() => null);
6263
+ if (!previewVariant && externalAssetUrl) {
6122
6264
  res.statusCode = 302;
6123
6265
  res.setHeader('Location', externalAssetUrl);
6124
6266
  res.setHeader('Cache-Control', 'private, max-age=45');
@@ -6137,6 +6279,27 @@ const handleAssetRequest = async (req, res, packKey, stickerToken) => {
6137
6279
  res.setHeader('X-Sticker-Tags', decorated.tags.join(','));
6138
6280
  }
6139
6281
  }
6282
+ if (previewVariant) {
6283
+ const previewCacheKey = [normalizedPackKey, normalizedStickerId, Number(item.asset?.size_bytes || 0), STICKER_PREVIEW_SIDE_PX, STICKER_PREVIEW_QUALITY].join(':');
6284
+ const previewBuffer = await generateStickerPreviewBuffer({
6285
+ sourceBuffer: buffer,
6286
+ mimetype: item.asset?.mimetype || 'image/webp',
6287
+ cacheKey: previewCacheKey,
6288
+ }).catch((previewError) => {
6289
+ logger.warn('Falha ao gerar preview de sticker para catálogo.', {
6290
+ action: 'sticker_catalog_preview_generate_failed',
6291
+ pack_key: normalizedPackKey,
6292
+ sticker_id: normalizedStickerId,
6293
+ error: previewError?.message,
6294
+ });
6295
+ return null;
6296
+ });
6297
+ if (previewBuffer && previewBuffer.length) {
6298
+ res.setHeader('X-Sticker-Preview', '1');
6299
+ sendAsset(req, res, previewBuffer, 'image/webp', `public, max-age=${IMMUTABLE_ASSET_CACHE_SECONDS}, immutable`);
6300
+ return;
6301
+ }
6302
+ }
6140
6303
  sendAsset(req, res, buffer, item.asset.mimetype || 'image/webp');
6141
6304
  } catch (error) {
6142
6305
  logger.warn('Falha ao ler asset de sticker para rota web.', {
@@ -170,7 +170,7 @@ export const handleCatalogPublicRoutes = async ({ req, res, pathname, url, segme
170
170
  sendJson(req, res, 405, METHOD_NOT_ALLOWED_BODY);
171
171
  return true;
172
172
  }
173
- await handlers.handleAssetRequest(req, res, segments[0], segments[2]);
173
+ await handlers.handleAssetRequest(req, res, segments[0], segments[2], url);
174
174
  return true;
175
175
  }
176
176