summaryrefslogtreecommitdiff
path: root/apps/web/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/lib')
-rw-r--r--apps/web/src/lib/iconify-api.ts242
-rw-r--r--apps/web/src/lib/storage/storage-service.ts21
-rw-r--r--apps/web/src/lib/storage/types.ts2
3 files changed, 262 insertions, 3 deletions
diff --git a/apps/web/src/lib/iconify-api.ts b/apps/web/src/lib/iconify-api.ts
new file mode 100644
index 00000000..9d1bcb97
--- /dev/null
+++ b/apps/web/src/lib/iconify-api.ts
@@ -0,0 +1,242 @@
+
+export const ICONIFY_HOSTS = [
+ "https://api.iconify.design",
+ "https://api.simplesvg.com",
+ "https://api.unisvg.com",
+];
+
+let currentHost = ICONIFY_HOSTS[0];
+
+async function fetchWithFallback(path: string): Promise<Response> {
+ for (const host of ICONIFY_HOSTS) {
+ try {
+ const response = await fetch(`${host}${path}`, {
+ signal: AbortSignal.timeout(2000),
+ });
+ if (response.ok) {
+ currentHost = host;
+ return response;
+ }
+ } catch (error) {
+ console.warn(`Failed to fetch from ${host}:`, error);
+ }
+ }
+ throw new Error("All API hosts failed");
+}
+
+export interface IconSet {
+ prefix: string;
+ name: string;
+ total: number;
+ author?: {
+ name: string;
+ url?: string;
+ };
+ license?: {
+ title: string;
+ spdx?: string;
+ url?: string;
+ };
+ samples?: string[];
+ category?: string;
+ palette?: boolean;
+}
+
+export interface IconSearchResult {
+ icons: string[];
+ total: number;
+ limit: number;
+ start: number;
+ collections: Record<string, IconSet>;
+}
+
+export interface CollectionInfo {
+ prefix: string;
+ total: number;
+ title?: string;
+ uncategorized?: string[];
+ categories?: Record<string, string[]>;
+ hidden?: string[];
+ aliases?: Record<string, string>;
+}
+
+export async function getCollections(
+ category?: string
+): Promise<Record<string, IconSet>> {
+ try {
+ const response = await fetchWithFallback("/collections?pretty=1");
+ const data = (await response.json()) as Record<string, IconSet>;
+
+ if (category) {
+ const filtered = Object.fromEntries(
+ Object.entries(data).filter(
+ ([_key, info]) => info.category === category
+ )
+ ) as Record<string, IconSet>;
+ return filtered;
+ }
+
+ return data;
+ } catch (error) {
+ console.error("Failed to fetch collections:", error);
+ return {};
+ }
+}
+
+export async function getCollection(
+ prefix: string
+): Promise<CollectionInfo | null> {
+ try {
+ const response = await fetchWithFallback(
+ `/collection?prefix=${prefix}&pretty=1`
+ );
+ return await response.json();
+ } catch (error) {
+ console.error(`Failed to fetch collection ${prefix}:`, error);
+ return null;
+ }
+}
+
+export async function searchIcons(
+ query: string,
+ limit: number = 64,
+ prefixes?: string[],
+ category?: string
+): Promise<IconSearchResult> {
+ const params = new URLSearchParams({
+ query,
+ limit: limit.toString(),
+ pretty: "1",
+ });
+
+ if (prefixes?.length) {
+ params.append("prefixes", prefixes.join(","));
+ }
+
+ if (category) {
+ params.append("category", category);
+ }
+
+ try {
+ const response = await fetchWithFallback(`/search?${params}`);
+ return await response.json();
+ } catch (error) {
+ console.error("Failed to search icons:", error);
+ return {
+ icons: [],
+ total: 0,
+ limit,
+ start: 0,
+ collections: {},
+ };
+ }
+}
+
+export function buildIconSvgUrl(
+ host: string,
+ iconName: string,
+ params?: {
+ color?: string;
+ width?: number;
+ height?: number;
+ flip?: "horizontal" | "vertical" | "horizontal,vertical";
+ rotate?: number | string;
+ }
+): string {
+ const [prefix, name] = iconName.includes(":")
+ ? iconName.split(":")
+ : ["", iconName];
+
+ if (!prefix || !name) {
+ throw new Error('Invalid icon name format. Expected "prefix:name"');
+ }
+
+ const urlParams = new URLSearchParams();
+
+ if (params?.color) {
+ urlParams.append("color", params.color.replace("#", "%23"));
+ }
+
+ if (params?.width) {
+ urlParams.append("width", params.width.toString());
+ }
+
+ if (params?.height) {
+ urlParams.append("height", params.height.toString());
+ }
+
+ if (params?.flip) {
+ urlParams.append("flip", params.flip);
+ }
+
+ if (params?.rotate) {
+ urlParams.append("rotate", params.rotate.toString());
+ }
+
+ const queryString = urlParams.toString();
+ return `${host}/${prefix}/${name}.svg${queryString ? "?" + queryString : ""}`;
+}
+
+export function getIconSvgUrl(
+ iconName: string,
+ params?: Parameters<typeof buildIconSvgUrl>[2]
+): string {
+ return buildIconSvgUrl(currentHost, iconName, params);
+}
+
+export async function downloadSvgAsText(
+ iconName: string,
+ params?: Parameters<typeof getIconSvgUrl>[1]
+): Promise<string> {
+ const url = getIconSvgUrl(iconName, params);
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`Failed to download SVG: ${response.statusText}`);
+ }
+ return await response.text();
+}
+
+export function svgToFile(svgText: string, fileName: string): File {
+ const blob = new Blob([svgText], { type: "image/svg+xml" });
+ return new File([blob], fileName, { type: "image/svg+xml" });
+}
+
+export const POPULAR_COLLECTIONS = {
+ general: [
+ { prefix: "mdi", name: "Material Design Icons" },
+ { prefix: "ic", name: "Google Material Icons" },
+ { prefix: "ph", name: "Phosphor" },
+ { prefix: "heroicons", name: "Heroicons" },
+ { prefix: "lucide", name: "Lucide" },
+ { prefix: "tabler", name: "Tabler Icons" },
+ { prefix: "fe", name: "Feather Icons" },
+ { prefix: "bi", name: "Bootstrap Icons" },
+ ],
+ brands: [
+ { prefix: "simple-icons", name: "Simple Icons" },
+ { prefix: "logos", name: "SVG Logos" },
+ { prefix: "skill-icons", name: "Skill Icons" },
+ { prefix: "devicon", name: "Devicon" },
+ { prefix: "fa-brands", name: "Font Awesome Brands" },
+ ],
+ emoji: [
+ { prefix: "noto", name: "Noto Emoji" },
+ { prefix: "twemoji", name: "Twemoji" },
+ { prefix: "fluent-emoji", name: "Fluent Emoji" },
+ { prefix: "fluent-emoji-flat", name: "Fluent Emoji Flat" },
+ { prefix: "emojione", name: "EmojiOne" },
+ { prefix: "openmoji", name: "OpenMoji" },
+ ],
+};
+
+export function getCategoriesFromCollections(
+ collections: Record<string, IconSet>
+): string[] {
+ const categories = new Set<string>();
+ Object.values(collections).forEach((collection) => {
+ if (collection.category) {
+ categories.add(collection.category);
+ }
+ });
+ return Array.from(categories).sort();
+}
diff --git a/apps/web/src/lib/storage/storage-service.ts b/apps/web/src/lib/storage/storage-service.ts
index 00d60c47..b3376a5f 100644
--- a/apps/web/src/lib/storage/storage-service.ts
+++ b/apps/web/src/lib/storage/storage-service.ts
@@ -142,6 +142,7 @@ class StorageService {
width: mediaItem.width,
height: mediaItem.height,
duration: mediaItem.duration,
+ ephemeral: mediaItem.ephemeral,
};
await mediaMetadataAdapter.set(mediaItem.id, metadata);
@@ -161,8 +162,22 @@ class StorageService {
if (!file || !metadata) return null;
- // Create new object URL for the file
- const url = URL.createObjectURL(file);
+ let url: string;
+ if (metadata.type === "image" && (!file.type || file.type === "")) {
+ try {
+ const text = await file.text();
+ if (text.trim().startsWith("<svg")) {
+ const svgBlob = new Blob([text], { type: "image/svg+xml" });
+ url = URL.createObjectURL(svgBlob);
+ } else {
+ url = URL.createObjectURL(file);
+ }
+ } catch {
+ url = URL.createObjectURL(file);
+ }
+ } else {
+ url = URL.createObjectURL(file);
+ }
return {
id: metadata.id,
@@ -173,7 +188,7 @@ class StorageService {
width: metadata.width,
height: metadata.height,
duration: metadata.duration,
- // thumbnailUrl would need to be regenerated or cached separately
+ ephemeral: metadata.ephemeral,
};
}
diff --git a/apps/web/src/lib/storage/types.ts b/apps/web/src/lib/storage/types.ts
index 127b1690..8f593867 100644
--- a/apps/web/src/lib/storage/types.ts
+++ b/apps/web/src/lib/storage/types.ts
@@ -18,6 +18,8 @@ export interface MediaFileData {
width?: number;
height?: number;
duration?: number;
+ ephemeral?: boolean;
+ sourceStickerIconName?: string;
// File will be stored separately in OPFS
}