diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-20 11:31:17 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-20 11:31:17 +0200 |
| commit | 3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch) | |
| tree | 57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/stickers/index.ts | |
| parent | 729d10592f4e802f91c555c309ff3ba4b119f843 (diff) | |
| download | opencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz | |
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/stickers/index.ts')
| -rw-r--r-- | apps/web/src/stickers/index.ts | 317 |
1 files changed, 317 insertions, 0 deletions
diff --git a/apps/web/src/stickers/index.ts b/apps/web/src/stickers/index.ts new file mode 100644 index 00000000..d41e277d --- /dev/null +++ b/apps/web/src/stickers/index.ts @@ -0,0 +1,317 @@ +import { + STICKER_CATEGORIES, +} from "@/stickers/categories"; +import { STICKER_INTRINSIC_SIZE_FALLBACK } from "@/stickers/intrinsic-size"; +import type { StickerCategory } from "@/stickers/types"; +import { stickersRegistry } from "./registry"; +import { resolveStickerId } from "./resolver"; +import { registerDefaultStickerProviders } from "./providers"; +import { parseStickerId } from "./sticker-id"; +import type { + StickerBrowseResult, + StickerItem, + StickerProvider, + StickerSearchResult, +} from "./types"; + +const DEFAULT_BROWSE_LIMIT = 12; +const DEFAULT_SEARCH_LIMIT = 100; + +function mergeSearchResults({ + results, +}: { + results: StickerSearchResult[]; +}): StickerSearchResult { + const deduplicatedItems = new Map< + string, + StickerSearchResult["items"][number] + >(); + let total = 0; + let hasMore = false; + + for (const result of results) { + total += result.total; + hasMore = hasMore || result.hasMore; + for (const item of result.items) { + if (!deduplicatedItems.has(item.id)) { + deduplicatedItems.set(item.id, item); + } + } + } + + return { + items: Array.from(deduplicatedItems.values()), + total, + hasMore, + }; +} + +function getProviderByCategory({ + category, +}: { + category: StickerCategory; +}): StickerProvider | null { + if (category === "all") { + return null; + } + + try { + return stickersRegistry.get(category); + } catch { + return null; + } +} + +function getEmptyBrowseResult(): StickerBrowseResult { + return { + sections: [], + }; +} + +function getStickerNameFromId({ stickerId }: { stickerId: string }): string { + const stickerIdParts = stickerId.split(":"); + if (stickerIdParts.length <= 1) { + return stickerId; + } + return ( + stickerIdParts.slice(1).join(":").split(":").pop()?.replaceAll("-", " ") ?? + stickerId + ); +} + +function toRecentStickerItem({ + stickerId, +}: { + stickerId: string; +}): StickerItem | null { + try { + const { providerId } = parseStickerId({ stickerId }); + return { + id: stickerId, + provider: providerId, + name: getStickerNameFromId({ stickerId }), + previewUrl: resolveStickerId({ + stickerId, + options: { width: 64, height: 64 }, + }), + metadata: {}, + }; + } catch { + return null; + } +} + +export async function searchStickers({ + query, + category, + limit = DEFAULT_SEARCH_LIMIT, +}: { + query: string; + category: StickerCategory; + limit?: number; +}): Promise<StickerSearchResult> { + registerDefaultStickerProviders({}); + + const effectiveCategory = category in STICKER_CATEGORIES ? category : "all"; + if (effectiveCategory !== "all") { + const provider = getProviderByCategory({ category: effectiveCategory }); + if (!provider) { + return { + items: [], + total: 0, + hasMore: false, + }; + } + return provider.search({ + query, + options: { limit }, + }); + } + + const providers = stickersRegistry.getAll(); + if (providers.length === 0) { + return { + items: [], + total: 0, + hasMore: false, + }; + } + + const perProviderLimit = Math.max(1, Math.ceil(limit / providers.length)); + const settledResults = await Promise.allSettled( + providers.map((provider) => + provider.search({ + query, + options: { limit: perProviderLimit }, + }), + ), + ); + + const fulfilledResults = settledResults + .filter( + (result): result is PromiseFulfilledResult<StickerSearchResult> => + result.status === "fulfilled", + ) + .map((result) => result.value); + + return mergeSearchResults({ + results: fulfilledResults, + }); +} + +export async function searchAll({ + query, + limit = DEFAULT_SEARCH_LIMIT, +}: { + query: string; + limit?: number; +}): Promise<StickerBrowseResult> { + registerDefaultStickerProviders({}); + + const providers = stickersRegistry.getAll(); + if (providers.length === 0) { + return { sections: [] }; + } + + const perProviderLimit = Math.max(1, Math.ceil(limit / providers.length)); + const settledResults = await Promise.allSettled( + providers.map(async (provider) => { + const result = await provider.search({ + query, + options: { limit: perProviderLimit }, + }); + return { provider, result }; + }), + ); + + const sections: StickerBrowseResult["sections"] = []; + for (const settled of settledResults) { + if (settled.status !== "fulfilled") { + continue; + } + const { provider, result } = settled.value; + if (result.items.length === 0) { + continue; + } + const category = provider.id as StickerCategory; + sections.push({ + id: category, + title: STICKER_CATEGORIES[category] ?? provider.id, + items: result.items, + hasMore: result.hasMore, + layout: "grid", + }); + } + + return { sections }; +} + +export async function browseCategory({ + category, +}: { + category: StickerCategory; +}): Promise<StickerBrowseResult> { + registerDefaultStickerProviders({}); + + const effectiveCategory = category in STICKER_CATEGORIES ? category : "all"; + if (effectiveCategory === "all") { + return getEmptyBrowseResult(); + } + + const provider = getProviderByCategory({ category: effectiveCategory }); + if (!provider) { + return getEmptyBrowseResult(); + } + + return provider.browse({ options: {} }); +} + +export async function browseAll({ + recentStickers, + limit = DEFAULT_BROWSE_LIMIT, +}: { + recentStickers: string[]; + limit?: number; +}): Promise<StickerBrowseResult> { + registerDefaultStickerProviders({}); + + const sections: StickerBrowseResult["sections"] = []; + const recentItems = recentStickers + .map((stickerId) => toRecentStickerItem({ stickerId })) + .filter((item): item is StickerItem => item !== null); + + if (recentItems.length > 0) { + sections.push({ + id: "recent", + title: "Recently used", + items: recentItems.slice(0, limit), + hasMore: recentItems.length > limit, + layout: "row", + }); + } + + const settledResults = await Promise.allSettled( + stickersRegistry.getAll().map(async (provider) => { + const browseResult = await provider.browse({ + options: { limit }, + }); + const firstSection = browseResult.sections[0]; + + if (!firstSection || firstSection.items.length === 0) { + return null; + } + + const category = provider.id as StickerCategory; + return { + ...firstSection, + id: category, + title: STICKER_CATEGORIES[category] ?? firstSection.title, + layout: "row" as const, + action: { + type: "see-all" as const, + category, + sectionId: firstSection.id, + }, + }; + }), + ); + + for (const result of settledResults) { + if (result.status === "fulfilled" && result.value) { + sections.push(result.value); + } + } + + return { sections }; +} + +export async function resolveStickerIntrinsicSize({ + stickerId, +}: { + stickerId: string; +}): Promise<{ width: number; height: number }> { + const url = resolveStickerId({ stickerId }); + return new Promise((resolve) => { + const img = new Image(); + img.onload = () => + resolve({ width: img.naturalWidth, height: img.naturalHeight }); + img.onerror = () => + resolve({ + width: STICKER_INTRINSIC_SIZE_FALLBACK, + height: STICKER_INTRINSIC_SIZE_FALLBACK, + }); + img.src = url; + }); +} + +export { resolveStickerId }; +export { resolveQueryToRegions, getRegionLabel } from "./providers/flags"; +export type { + StickerBrowseResult, + StickerBrowseSection, + StickerCategory, + StickerItem, + StickerProvider, + StickerResolveOptions, + StickerSearchResult, +} from "./types"; |
