Merge branch 'staging' of https://github.com/mazeincoding/AppCut into staging

This commit is contained in:
Maze Winther
2025-08-15 02:12:16 +02:00
17 changed files with 1261 additions and 48 deletions
+2
View File
@@ -16,6 +16,8 @@ export interface MediaItem {
width?: number; // For video/image width
height?: number; // For video/image height
fps?: number; // For video frame rate
// Ephemeral items are used by timeline directly and should not appear in the media library or be persisted
ephemeral?: boolean;
// Text-specific properties
content?: string; // Text content
fontSize?: number; // Font size
+178
View File
@@ -0,0 +1,178 @@
import { create } from "zustand";
import {
getCollections,
getCollection,
searchIcons,
downloadSvgAsText,
svgToFile,
type IconSet,
type CollectionInfo,
type IconSearchResult,
} from "@/lib/iconify-api";
export type StickerCategory = "all" | "general" | "brands" | "emoji";
interface StickersStore {
searchQuery: string;
selectedCategory: StickerCategory;
selectedCollection: string | null;
viewMode: "search" | "browse" | "collection";
collections: Record<string, IconSet>;
currentCollection: CollectionInfo | null;
searchResults: IconSearchResult | null;
recentStickers: string[];
isLoadingCollections: boolean;
isLoadingCollection: boolean;
isSearching: boolean;
isDownloading: boolean;
setSearchQuery: (query: string) => void;
setSelectedCategory: (category: StickerCategory) => void;
setSelectedCollection: (collection: string | null) => void;
setViewMode: (mode: "search" | "browse" | "collection") => void;
loadCollections: () => Promise<void>;
loadCollection: (prefix: string) => Promise<void>;
searchStickers: (query: string) => Promise<void>;
downloadSticker: (iconName: string) => Promise<File | null>;
addToRecentStickers: (iconName: string) => void;
clearRecentStickers: () => void;
}
const MAX_RECENT_STICKERS = 50;
export const useStickersStore = create<StickersStore>((set, get) => ({
searchQuery: "",
selectedCategory: "all",
selectedCollection: null,
viewMode: "browse",
collections: {},
currentCollection: null,
searchResults: null,
recentStickers: [],
isLoadingCollections: false,
isLoadingCollection: false,
isSearching: false,
isDownloading: false,
setSearchQuery: (query) => set({ searchQuery: query }),
setSelectedCategory: (category) =>
set({
selectedCategory: category,
viewMode: "browse",
selectedCollection: null,
currentCollection: null,
}),
setSelectedCollection: (collection) => {
set({
selectedCollection: collection,
viewMode: collection ? "collection" : "browse",
currentCollection: null,
});
if (collection) {
get().loadCollection(collection);
}
},
setViewMode: (mode) => set({ viewMode: mode }),
loadCollections: async () => {
set({ isLoadingCollections: true });
try {
const collections = await getCollections();
set({ collections });
} catch (error) {
console.error("Failed to load collections:", error);
} finally {
set({ isLoadingCollections: false });
}
},
loadCollection: async (prefix: string) => {
set({ isLoadingCollection: true });
try {
const collection = await getCollection(prefix);
set({ currentCollection: collection });
} catch (error) {
console.error(`Failed to load collection ${prefix}:`, error);
set({ currentCollection: null });
} finally {
set({ isLoadingCollection: false });
}
},
searchStickers: async (query: string) => {
if (!query.trim()) {
set({ searchResults: null, viewMode: "browse" });
return;
}
const { selectedCategory } = get();
set({ isSearching: true, viewMode: "search" });
try {
let category: string | undefined;
if (selectedCategory !== "all") {
if (selectedCategory === "general") {
category = "General";
} else if (selectedCategory === "brands") {
category = "Brands / Social";
} else if (selectedCategory === "emoji") {
category = "Emoji";
}
}
const results = await searchIcons(query, 100, undefined, category);
set({ searchResults: results });
} catch (error) {
console.error("Search failed:", error);
set({ searchResults: null });
} finally {
set({ isSearching: false });
}
},
downloadSticker: async (iconName: string) => {
set({ isDownloading: true });
try {
const svgText = await downloadSvgAsText(iconName, {
width: 200,
height: 200,
});
const fileName = `${iconName.replace(":", "-")}.svg`;
const file = svgToFile(svgText, fileName);
get().addToRecentStickers(iconName);
return file;
} catch (error) {
console.error(`Failed to download sticker ${iconName}:`, error);
return null;
} finally {
set({ isDownloading: false });
}
},
addToRecentStickers: (iconName: string) => {
set((state) => {
const recent = [
iconName,
...state.recentStickers.filter((s) => s !== iconName),
];
return {
recentStickers: recent.slice(0, MAX_RECENT_STICKERS),
};
});
},
clearRecentStickers: () => set({ recentStickers: [] }),
}));