From 31961eb3d4d7abc1f0e049f7f8b808c14ffd57b5 Mon Sep 17 00:00:00 2001 From: Kha Nguyen Date: Thu, 17 Jul 2025 17:23:05 -0500 Subject: [PATCH 1/5] fix: show skeletons when media items are loading --- .../editor/media-panel/views/media.tsx | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/editor/media-panel/views/media.tsx b/apps/web/src/components/editor/media-panel/views/media.tsx index 631fe39ac..88fbc876b 100644 --- a/apps/web/src/components/editor/media-panel/views/media.tsx +++ b/apps/web/src/components/editor/media-panel/views/media.tsx @@ -22,12 +22,13 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Skeleton } from "@/components/ui/skeleton"; import { DraggableMediaItem } from "@/components/ui/draggable-item"; import { useProjectStore } from "@/stores/project-store"; import { useTimelineStore } from "@/stores/timeline-store"; export function MediaView() { - const { mediaItems, addMediaItem, removeMediaItem } = useMediaStore(); + const { mediaItems, addMediaItem, removeMediaItem, isLoading } = useMediaStore(); const { activeProject } = useProjectStore(); const fileInputRef = useRef(null); const [isProcessing, setIsProcessing] = useState(false); @@ -188,6 +189,39 @@ export function MediaView() { ); }; + // render skeletons while loading media item thumbnails + if (isLoading) { + return ( +
+ {/* Search and filter controls skeleton */} +
+
+ {/* Filter dropdown */} + {/* Search input */} +
+
+ + {/* Media grid skeleton */} +
+
+ {/* 8 thumbnail skeletons */} + {Array.from({ length: 8 }).map((_, i) => ( +
+ + +
+ ))} +
+
+
+ ); + } + return ( <> {/* Hidden file input for uploading media */} From 8f7773979e36224827907fe0d423a45671c9ab8f Mon Sep 17 00:00:00 2001 From: Kha Nguyen Date: Fri, 18 Jul 2025 00:11:02 -0500 Subject: [PATCH 2/5] add mediaCount in MediaStore --- apps/web/src/stores/media-store.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/web/src/stores/media-store.ts b/apps/web/src/stores/media-store.ts index c4ee5f3ef..2dbd120a1 100644 --- a/apps/web/src/stores/media-store.ts +++ b/apps/web/src/stores/media-store.ts @@ -28,6 +28,7 @@ export interface MediaItem { interface MediaStore { mediaItems: MediaItem[]; isLoading: boolean; + mediaCount: number; // keep a separate count for loading state // Actions - now require projectId addMediaItem: ( @@ -159,6 +160,7 @@ export const getMediaAspectRatio = (item: MediaItem): number => { export const useMediaStore = create((set, get) => ({ mediaItems: [], isLoading: false, + mediaCount: 0, addMediaItem: async (projectId, item) => { const newItem: MediaItem = { @@ -169,6 +171,7 @@ export const useMediaStore = create((set, get) => ({ // Add to local state immediately for UI responsiveness set((state) => ({ mediaItems: [...state.mediaItems, newItem], + mediaCount: state.mediaItems.length + 1, })); // Save to persistent storage in background @@ -198,6 +201,7 @@ export const useMediaStore = create((set, get) => ({ // Remove from local state immediately set((state) => ({ mediaItems: state.mediaItems.filter((media) => media.id !== id), + mediaCount: state.mediaItems.length - 1, })); // Remove from persistent storage @@ -213,6 +217,7 @@ export const useMediaStore = create((set, get) => ({ try { const mediaItems = await storageService.loadAllMediaItems(projectId); + set({ mediaCount: mediaItems.length }); // Regenerate thumbnails for video items const updatedMediaItems = await Promise.all( @@ -257,7 +262,7 @@ export const useMediaStore = create((set, get) => ({ }); // Clear local state - set({ mediaItems: [] }); + set({ mediaItems: [], mediaCount: 0 }); // Clear persistent storage try { @@ -284,6 +289,6 @@ export const useMediaStore = create((set, get) => ({ }); // Clear local state - set({ mediaItems: [] }); + set({ mediaItems: [], mediaCount: 0 }); }, })); From 81422fb348632a7a5ac24cde0cc5ada77f51084e Mon Sep 17 00:00:00 2001 From: Kha Nguyen Date: Fri, 18 Jul 2025 00:12:14 -0500 Subject: [PATCH 3/5] match skeleton thumbnails count with actual media item count --- .../src/components/editor/media-panel/views/media.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/editor/media-panel/views/media.tsx b/apps/web/src/components/editor/media-panel/views/media.tsx index 88fbc876b..27e9a80fa 100644 --- a/apps/web/src/components/editor/media-panel/views/media.tsx +++ b/apps/web/src/components/editor/media-panel/views/media.tsx @@ -28,7 +28,7 @@ import { useProjectStore } from "@/stores/project-store"; import { useTimelineStore } from "@/stores/timeline-store"; export function MediaView() { - const { mediaItems, addMediaItem, removeMediaItem, isLoading } = useMediaStore(); + const { mediaItems, addMediaItem, removeMediaItem, isLoading, mediaCount } = useMediaStore(); const { activeProject } = useProjectStore(); const fileInputRef = useRef(null); const [isProcessing, setIsProcessing] = useState(false); @@ -196,8 +196,8 @@ export function MediaView() { {/* Search and filter controls skeleton */}
- {/* Filter dropdown */} - {/* Search input */} + {/* Filter dropdown */} + {/* Search input */}
@@ -209,8 +209,8 @@ export function MediaView() { gridTemplateColumns: "repeat(auto-fill, 160px)", }} > - {/* 8 thumbnail skeletons */} - {Array.from({ length: 8 }).map((_, i) => ( + {/* thumbnail skeletons */} + {Array.from({ length: mediaCount }).map((_, i) => (
From d76ef55c3c514577dfdf0c0d3f70a36e779a319f Mon Sep 17 00:00:00 2001 From: Kha Nguyen Date: Fri, 18 Jul 2025 00:50:34 -0500 Subject: [PATCH 4/5] change mediaCount to initialMediaCount this name provides clear intention, it's used for keeping track of number of media items when we're loading a project --- .../components/editor/media-panel/views/media.tsx | 12 +++++++++--- apps/web/src/stores/media-store.ts | 10 +++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/editor/media-panel/views/media.tsx b/apps/web/src/components/editor/media-panel/views/media.tsx index 27e9a80fa..81f1c0e1e 100644 --- a/apps/web/src/components/editor/media-panel/views/media.tsx +++ b/apps/web/src/components/editor/media-panel/views/media.tsx @@ -28,7 +28,13 @@ import { useProjectStore } from "@/stores/project-store"; import { useTimelineStore } from "@/stores/timeline-store"; export function MediaView() { - const { mediaItems, addMediaItem, removeMediaItem, isLoading, mediaCount } = useMediaStore(); + const { + mediaItems, + addMediaItem, + removeMediaItem, + isLoading, + initialMediaCount, + } = useMediaStore(); const { activeProject } = useProjectStore(); const fileInputRef = useRef(null); const [isProcessing, setIsProcessing] = useState(false); @@ -48,7 +54,7 @@ export function MediaView() { try { // Process files (extract metadata, generate thumbnails, etc.) const processedItems = await processMediaFiles(files, (p) => - setProgress(p) + setProgress(p), ); // Add each processed media item to the store for (const item of processedItems) { @@ -210,7 +216,7 @@ export function MediaView() { }} > {/* thumbnail skeletons */} - {Array.from({ length: mediaCount }).map((_, i) => ( + {Array.from({ length: initialMediaCount }).map((_, i) => (
diff --git a/apps/web/src/stores/media-store.ts b/apps/web/src/stores/media-store.ts index 2dbd120a1..c81ca64fe 100644 --- a/apps/web/src/stores/media-store.ts +++ b/apps/web/src/stores/media-store.ts @@ -28,7 +28,7 @@ export interface MediaItem { interface MediaStore { mediaItems: MediaItem[]; isLoading: boolean; - mediaCount: number; // keep a separate count for loading state + initialMediaCount: number; // keep track of initial media count for loading state // Actions - now require projectId addMediaItem: ( @@ -160,7 +160,7 @@ export const getMediaAspectRatio = (item: MediaItem): number => { export const useMediaStore = create((set, get) => ({ mediaItems: [], isLoading: false, - mediaCount: 0, + initialMediaCount: 0, addMediaItem: async (projectId, item) => { const newItem: MediaItem = { @@ -217,7 +217,7 @@ export const useMediaStore = create((set, get) => ({ try { const mediaItems = await storageService.loadAllMediaItems(projectId); - set({ mediaCount: mediaItems.length }); + set({ initialMediaCount: mediaItems.length }); // Regenerate thumbnails for video items const updatedMediaItems = await Promise.all( @@ -262,7 +262,7 @@ export const useMediaStore = create((set, get) => ({ }); // Clear local state - set({ mediaItems: [], mediaCount: 0 }); + set({ mediaItems: [] }); // Clear persistent storage try { @@ -289,6 +289,6 @@ export const useMediaStore = create((set, get) => ({ }); // Clear local state - set({ mediaItems: [], mediaCount: 0 }); + set({ mediaItems: [] }); }, })); From 8fb62fa17518783e8f7f464c8097bd74dbb0054b Mon Sep 17 00:00:00 2001 From: Kha Nguyen Date: Sun, 20 Jul 2025 08:20:02 -0500 Subject: [PATCH 5/5] remove unused mediaCount state field --- apps/web/src/stores/media-store.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/apps/web/src/stores/media-store.ts b/apps/web/src/stores/media-store.ts index c81ca64fe..96da4d711 100644 --- a/apps/web/src/stores/media-store.ts +++ b/apps/web/src/stores/media-store.ts @@ -33,7 +33,7 @@ interface MediaStore { // Actions - now require projectId addMediaItem: ( projectId: string, - item: Omit + item: Omit, ) => Promise; removeMediaItem: (projectId: string, id: string) => Promise; loadProjectMedia: (projectId: string) => Promise; @@ -60,7 +60,7 @@ export const getFileType = (file: File): MediaType | null => { // Helper function to get image dimensions export const getImageDimensions = ( - file: File + file: File, ): Promise<{ width: number; height: number }> => { return new Promise((resolve, reject) => { const img = new window.Image(); @@ -83,7 +83,7 @@ export const getImageDimensions = ( // Helper function to generate video thumbnail and get dimensions export const generateVideoThumbnail = ( - file: File + file: File, ): Promise<{ thumbnailUrl: string; width: number; height: number }> => { return new Promise((resolve, reject) => { const video = document.createElement("video") as HTMLVideoElement; @@ -131,7 +131,7 @@ export const generateVideoThumbnail = ( export const getMediaDuration = (file: File): Promise => { return new Promise((resolve, reject) => { const element = document.createElement( - file.type.startsWith("video/") ? "video" : "audio" + file.type.startsWith("video/") ? "video" : "audio", ) as HTMLVideoElement; element.addEventListener("loadedmetadata", () => { @@ -171,7 +171,6 @@ export const useMediaStore = create((set, get) => ({ // Add to local state immediately for UI responsiveness set((state) => ({ mediaItems: [...state.mediaItems, newItem], - mediaCount: state.mediaItems.length + 1, })); // Save to persistent storage in background @@ -201,7 +200,6 @@ export const useMediaStore = create((set, get) => ({ // Remove from local state immediately set((state) => ({ mediaItems: state.mediaItems.filter((media) => media.id !== id), - mediaCount: state.mediaItems.length - 1, })); // Remove from persistent storage @@ -224,20 +222,24 @@ export const useMediaStore = create((set, get) => ({ mediaItems.map(async (item) => { if (item.type === "video" && item.file) { try { - const { thumbnailUrl, width, height } = await generateVideoThumbnail(item.file); + const { thumbnailUrl, width, height } = + await generateVideoThumbnail(item.file); return { ...item, thumbnailUrl, width: width || item.width, - height: height || item.height + height: height || item.height, }; } catch (error) { - console.error(`Failed to regenerate thumbnail for video ${item.id}:`, error); + console.error( + `Failed to regenerate thumbnail for video ${item.id}:`, + error, + ); return item; } } return item; - }) + }), ); set({ mediaItems: updatedMediaItems }); @@ -268,7 +270,7 @@ export const useMediaStore = create((set, get) => ({ try { const mediaIds = state.mediaItems.map((item) => item.id); await Promise.all( - mediaIds.map((id) => storageService.deleteMediaItem(projectId, id)) + mediaIds.map((id) => storageService.deleteMediaItem(projectId, id)), ); } catch (error) { console.error("Failed to clear media items from storage:", error);