summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/stickers/index.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-03-25 16:47:22 +0100
committerMaze Winther <mazewinther@gmail.com>2026-03-29 15:48:22 +0200
commit8db3bead13441a5882d8689dfc6a9f09c82375d9 (patch)
treee9d0cafa93904ffcbec3188c37961bcc16c6422d /apps/web/src/lib/stickers/index.ts
parent39ea298a9ccba3104c549c0ca079c231981811f5 (diff)
downloadopencut-8db3bead13441a5882d8689dfc6a9f09c82375d9.tar.gz
feat: masks, properties refactor, shaders, storage migrations, and more
Diffstat (limited to 'apps/web/src/lib/stickers/index.ts')
-rw-r--r--apps/web/src/lib/stickers/index.ts230
1 files changed, 186 insertions, 44 deletions
diff --git a/apps/web/src/lib/stickers/index.ts b/apps/web/src/lib/stickers/index.ts
index 1f924c6d..47be18fe 100644
--- a/apps/web/src/lib/stickers/index.ts
+++ b/apps/web/src/lib/stickers/index.ts
@@ -1,10 +1,20 @@
-import { STICKER_CATEGORIES } from "@/constants/sticker-constants";
-import type { StickerCategory } from "@/types/stickers";
-import { getAllProviders, getProvider } from "./registry";
+import {
+ STICKER_CATEGORIES,
+ STICKER_INTRINSIC_SIZE_FALLBACK,
+} from "@/constants/sticker-constants";
+import type { StickerCategory } from "@/lib/stickers/types";
+import { stickersRegistry } from "./registry";
import { resolveStickerId } from "./resolver";
import { registerDefaultStickerProviders } from "./providers";
-import type { StickerProvider, StickerSearchResult } from "./types";
+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({
@@ -46,7 +56,46 @@ function getProviderByCategory({
}
try {
- return getProvider({ providerId: category });
+ 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;
}
@@ -79,7 +128,7 @@ export async function searchStickers({
});
}
- const providers = getAllProviders();
+ const providers = stickersRegistry.getAll();
if (providers.length === 0) {
return {
items: [],
@@ -110,64 +159,157 @@ export async function searchStickers({
});
}
-export async function browseStickers({
- category,
- page = 1,
+export async function searchAll({
+ query,
limit = DEFAULT_SEARCH_LIMIT,
}: {
- category: StickerCategory;
- page?: number;
+ query: string;
limit?: number;
-}): Promise<StickerSearchResult> {
+}): Promise<StickerBrowseResult> {
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,
- };
+ 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;
}
- return provider.browse({
- options: { page, limit },
+ 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",
});
}
- const providers = getAllProviders();
- if (providers.length === 0) {
- return {
- items: [],
- total: 0,
- hasMore: false,
- };
+ 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 perProviderLimit = Math.max(1, Math.ceil(limit / providers.length));
const settledResults = await Promise.allSettled(
- providers.map((provider) =>
- provider.browse({
- options: { page, limit: perProviderLimit },
- }),
- ),
+ 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,
+ },
+ };
+ }),
);
- const fulfilledResults = settledResults
- .filter(
- (result): result is PromiseFulfilledResult<StickerSearchResult> =>
- result.status === "fulfilled",
- )
- .map((result) => result.value);
+ for (const result of settledResults) {
+ if (result.status === "fulfilled" && result.value) {
+ sections.push(result.value);
+ }
+ }
- return mergeSearchResults({
- results: fulfilledResults,
+ 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,