@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=
|
|
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:
|
|
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.
|
|
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.
|
|
95
|
-
| `figurinha` | 4.
|
|
94
|
+
| `texto` | 15.340 |
|
|
95
|
+
| `figurinha` | 4.861 |
|
|
96
96
|
| `reacao` | 1.818 |
|
|
97
|
-
| `imagem` | 1.
|
|
97
|
+
| `imagem` | 1.512 |
|
|
98
98
|
| `outros` | 1.081 |
|
|
99
99
|
| `video` | 205 |
|
|
100
100
|
| `audio` | 178 |
|
package/package.json
CHANGED
|
@@ -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
|
|
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 '&';
|
|
317
|
+
if (char === '<') return '<';
|
|
318
|
+
if (char === '>') return '>';
|
|
319
|
+
if (char === '"') return '"';
|
|
320
|
+
return ''';
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 :
|
|
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
|
|
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
|
-
:
|
|
1847
|
+
: visibleItems.length
|
|
1716
1848
|
? html`
|
|
1717
1849
|
<div className="pack-stickers-grid gap-2 sm:gap-3">
|
|
1718
|
-
${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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,
|
|
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,
|
|
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,
|
|
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', '
|
|
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
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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:
|
|
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 =
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
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
|
|