diff options
Diffstat (limited to 'apps/web/src/components/editor/media-panel')
5 files changed, 198 insertions, 114 deletions
diff --git a/apps/web/src/components/editor/media-panel/tabbar.tsx b/apps/web/src/components/editor/media-panel/tabbar.tsx index 56437508..e2bfdff7 100644 --- a/apps/web/src/components/editor/media-panel/tabbar.tsx +++ b/apps/web/src/components/editor/media-panel/tabbar.tsx @@ -7,13 +7,45 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; +import { useEffect, useRef, useState } from "react"; export function TabBar() { const { activeTab, setActiveTab } = useMediaPanelStore(); + const scrollRef = useRef<HTMLDivElement>(null); + const [showTopFade, setShowTopFade] = useState(false); + const [showBottomFade, setShowBottomFade] = useState(false); + + const checkScrollPosition = () => { + const element = scrollRef.current; + if (!element) return; + + const { scrollTop, scrollHeight, clientHeight } = element; + setShowTopFade(scrollTop > 0); + setShowBottomFade(scrollTop < scrollHeight - clientHeight - 1); + }; + + useEffect(() => { + const element = scrollRef.current; + if (!element) return; + + checkScrollPosition(); + element.addEventListener("scroll", checkScrollPosition); + + const resizeObserver = new ResizeObserver(checkScrollPosition); + resizeObserver.observe(element); + + return () => { + element.removeEventListener("scroll", checkScrollPosition); + resizeObserver.disconnect(); + }; + }, []); return ( - <div className="flex"> - <div className="h-full px-4 flex flex-col justify-start items-center gap-5 overflow-x-auto scrollbar-x-hidden relative w-full py-4"> + <div className="flex relative"> + <div + ref={scrollRef} + className="h-full px-4 flex flex-col justify-start items-center gap-5 overflow-y-auto scrollbar-hidden relative w-full py-4" + > {(Object.keys(tabs) as Tab[]).map((tabKey) => { const tab = tabs[tabKey]; return ( @@ -46,6 +78,22 @@ export function TabBar() { ); })} </div> + + <FadeOverlay direction="top" show={showTopFade} /> + <FadeOverlay direction="bottom" show={showBottomFade} /> </div> ); } + +function FadeOverlay({ direction, show }: { direction: "top" | "bottom", show: boolean }) { + return ( + <div + className={cn( + "absolute left-0 right-0 h-6 pointer-events-none z-[101] transition-opacity duration-200", + direction === "top" && show + ? "top-0 bg-gradient-to-b from-panel to-transparent" + : "bottom-0 bg-gradient-to-t from-panel to-transparent" + )} + /> + ); +}
\ No newline at end of file diff --git a/apps/web/src/components/editor/media-panel/views/captions.tsx b/apps/web/src/components/editor/media-panel/views/captions.tsx index 59403a8d..fbe5b418 100644 --- a/apps/web/src/components/editor/media-panel/views/captions.tsx +++ b/apps/web/src/components/editor/media-panel/views/captions.tsx @@ -188,7 +188,7 @@ export function Captions() { }; return ( - <BaseView ref={containerRef} className="flex flex-col justify-between"> + <BaseView ref={containerRef} className="flex flex-col justify-between h-full"> <PropertyGroup title="Language"> <LanguageSelect selectedCountry={selectedCountry} diff --git a/apps/web/src/components/editor/media-panel/views/settings.tsx b/apps/web/src/components/editor/media-panel/views/settings.tsx index df1ec54f..be54a9c4 100644 --- a/apps/web/src/components/editor/media-panel/views/settings.tsx +++ b/apps/web/src/components/editor/media-panel/views/settings.tsx @@ -16,15 +16,18 @@ import { } from "../../properties-panel/property-item"; import { FPS_PRESETS } from "@/constants/timeline-constants"; import { useProjectStore } from "@/stores/project-store"; +import type { BlurIntensity } from "@/types/project"; import { useEditorStore } from "@/stores/editor-store"; import { useAspectRatio } from "@/hooks/use-aspect-ratio"; import Image from "next/image"; import { cn } from "@/lib/utils"; import { colors } from "@/data/colors/solid"; import { patternCraftGradients } from "@/data/colors/pattern-craft"; -import { PipetteIcon } from "lucide-react"; +import { PipetteIcon, PlusIcon } from "lucide-react"; import { useMemo, memo, useCallback } from "react"; import { syntaxUIGradients } from "@/data/colors/syntax-ui"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; export function SettingsView() { return <ProjectSettingsTabs />; @@ -38,14 +41,40 @@ function ProjectSettingsTabs() { { value: "project-info", label: "Project info", - content: <ProjectInfoView />, + content: ( + <div className="p-5"> + <ProjectInfoView /> + </div> + ), }, { value: "background", label: "Background", - content: <BackgroundView />, + content: ( + <div className="flex flex-col justify-between h-full"> + <div className="flex-1 p-5"> + <BackgroundView /> + </div> + <div className="flex flex-col sticky -bottom-0 bg-panel/85 backdrop-blur-lg"> + <Separator /> + <Button className="w-fit h-auto p-5 py-4 !bg-transparent shadow-none text-muted-foreground hover:text-foreground/85 text-xs"> + Custom background + <PlusIcon /> + </Button> + </div> + + {/* Another UI, looks so beautiful i don't wanna remove it */} + {/* <div className="flex flex-col justify-center items-center pb-5 sticky bottom-0"> + <Button className="w-fit h-auto gap-1.5 px-3.5 py-1.5 bg-foreground hover:bg-foreground/85 text-background rounded-full"> + <span className="text-sm">Custom</span> + <PlusIcon className="" /> + </Button> + </div> */} + </div> + ), }, ]} + className="flex flex-col justify-between h-full p-0" /> ); } @@ -177,9 +206,9 @@ const BackgroundPreviews = memo( }) => { return useMemo( () => - backgrounds.map((bg) => ( + backgrounds.map((bg, index) => ( <div - key={bg} + key={`${index}-${bg}`} className={cn( "w-full aspect-square rounded-sm cursor-pointer border border-foreground/15 hover:border-primary", isColorBackground && @@ -215,7 +244,7 @@ BackgroundPreviews.displayName = "BackgroundPreviews"; function BackgroundView() { const { activeProject, updateBackgroundType } = useProjectStore(); - const blurLevels = useMemo( + const blurLevels = useMemo<Array<{ label: string; value: BlurIntensity }>>( () => [ { label: "Light", value: 4 }, { label: "Medium", value: 8 }, @@ -225,7 +254,7 @@ function BackgroundView() { ); const handleBlurSelect = useCallback( - async (blurIntensity: number) => { + async (blurIntensity: BlurIntensity) => { await updateBackgroundType("blur", { blurIntensity }); }, [updateBackgroundType] @@ -257,7 +286,7 @@ function BackgroundView() { ); return ( - <div className="flex flex-col gap-4"> + <div className="flex flex-col gap-4 h-full"> <PropertyGroup title="Blur" defaultExpanded={false}> <div className="grid grid-cols-4 gap-2 w-full">{blurPreviews}</div> </PropertyGroup> @@ -277,7 +306,7 @@ function BackgroundView() { </div> </PropertyGroup> - <PropertyGroup title="Pattern Craft" defaultExpanded={false}> + <PropertyGroup title="Pattern craft" defaultExpanded={false}> <div className="grid grid-cols-4 gap-2 w-full"> <BackgroundPreviews backgrounds={patternCraftGradients} diff --git a/apps/web/src/components/editor/media-panel/views/sounds.tsx b/apps/web/src/components/editor/media-panel/views/sounds.tsx index 303eb858..700eaea7 100644 --- a/apps/web/src/components/editor/media-panel/views/sounds.tsx +++ b/apps/web/src/components/editor/media-panel/views/sounds.tsx @@ -82,6 +82,14 @@ function SoundEffectsView() { toggleSavedSound, showCommercialOnly, toggleCommercialFilter, + hasLoaded, + setTopSoundEffects, + setLoading, + setError, + setHasLoaded, + setCurrentPage, + setHasNextPage, + setTotalCount, } = useSoundsStore(); const { results: searchResults, @@ -106,6 +114,55 @@ function SoundEffectsView() { useEffect(() => { loadSavedSounds(); + if (!hasLoaded) { + let ignore = false; + + const fetchTopSounds = async () => { + try { + if (!ignore) { + setLoading(true); + setError(null); + } + + const response = await fetch( + "/api/sounds/search?page_size=50&sort=downloads" + ); + + if (!ignore) { + if (!response.ok) { + throw new Error(`Failed to fetch: ${response.status}`); + } + + const data = await response.json(); + setTopSoundEffects(data.results); + setHasLoaded(true); + + setCurrentPage(1); + setHasNextPage(!!data.next); + setTotalCount(data.count); + } + } catch (error) { + if (!ignore) { + console.error("Failed to fetch top sounds:", error); + setError( + error instanceof Error ? error.message : "Failed to load sounds" + ); + } + } finally { + if (!ignore) { + setLoading(false); + } + } + }; + + const timeoutId = setTimeout(fetchTopSounds, 100); + + return () => { + clearTimeout(timeoutId); + ignore = true; + }; + } + if (scrollAreaRef.current && scrollPosition > 0) { const timeoutId = setTimeout(() => { scrollAreaRef.current?.scrollTo({ top: scrollPosition }); @@ -113,14 +170,23 @@ function SoundEffectsView() { return () => clearTimeout(timeoutId); } - }, []); + }, [ + hasLoaded, + setTopSoundEffects, + setLoading, + setError, + setHasLoaded, + setCurrentPage, + setHasNextPage, + setTotalCount, + ]); const handleScrollWithPosition = (event: React.UIEvent<HTMLDivElement>) => { const { scrollTop } = event.currentTarget; setScrollPosition(scrollTop); handleScroll(event); }; - + const displayedSounds = useMemo(() => { const sounds = searchQuery ? searchResults : topSoundEffects; return sounds; diff --git a/apps/web/src/components/editor/media-panel/views/stickers.tsx b/apps/web/src/components/editor/media-panel/views/stickers.tsx index 41afe285..f67450cc 100644 --- a/apps/web/src/components/editor/media-panel/views/stickers.tsx +++ b/apps/web/src/components/editor/media-panel/views/stickers.tsx @@ -20,8 +20,7 @@ import { import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; -import { Separator } from "@/components/ui/separator"; +import { PanelBaseView as BaseView } from "@/components/editor/panel-base-view"; import { Tooltip, TooltipContent, @@ -48,63 +47,41 @@ export function StickersView() { const { selectedCategory, setSelectedCategory } = useStickersStore(); return ( - <div className="h-full flex flex-col"> - <Tabs - value={selectedCategory} - onValueChange={(v) => { - if (["all", "general", "brands", "emoji"].includes(v)) { - setSelectedCategory(v as StickerCategory); - } - }} - className="flex flex-col h-full" - > - <div className="px-3 pt-4 pb-0"> - <TabsList> - <TabsTrigger value="all" className="gap-1"> - <Grid3X3 className="h-3 w-3" /> - All - </TabsTrigger> - <TabsTrigger value="general" className="gap-1"> - <Sparkles className="h-3 w-3" /> - Icons - </TabsTrigger> - <TabsTrigger value="brands" className="gap-1"> - <Hash className="h-3 w-3" /> - Brands - </TabsTrigger> - <TabsTrigger value="emoji" className="gap-1"> - <Smile className="h-3 w-3" /> - Emoji - </TabsTrigger> - </TabsList> - </div> - <Separator className="my-4" /> - <TabsContent - value="all" - className="px-3 pt-0 mt-0 flex-1 flex flex-col min-h-0" - > - <StickersContentView category="all" /> - </TabsContent> - <TabsContent - value="general" - className="px-3 pt-0 mt-0 flex-1 flex flex-col min-h-0" - > - <StickersContentView category="general" /> - </TabsContent> - <TabsContent - value="brands" - className="px-3 pt-0 mt-0 flex-1 flex flex-col min-h-0" - > - <StickersContentView category="brands" /> - </TabsContent> - <TabsContent - value="emoji" - className="px-3 pt-0 mt-0 flex-1 flex flex-col min-h-0" - > - <StickersContentView category="emoji" /> - </TabsContent> - </Tabs> - </div> + <BaseView + value={selectedCategory} + onValueChange={(v) => { + if (["all", "general", "brands", "emoji"].includes(v)) { + setSelectedCategory(v as StickerCategory); + } + }} + tabs={[ + { + value: "all", + label: "All", + icon: <Grid3X3 className="h-3 w-3" />, + content: <StickersContentView category="all" />, + }, + { + value: "general", + label: "Icons", + icon: <Sparkles className="h-3 w-3" />, + content: <StickersContentView category="general" />, + }, + { + value: "brands", + label: "Brands", + icon: <Hash className="h-3 w-3" />, + content: <StickersContentView category="brands" />, + }, + { + value: "emoji", + label: "Emoji", + icon: <Smile className="h-3 w-3" />, + content: <StickersContentView category="emoji" />, + }, + ]} + className="flex flex-col h-full p-0 overflow-hidden" + /> ); } @@ -204,12 +181,12 @@ function StickersContentView({ category }: { category: StickerCategory }) { setSelectedCollection, loadCollections, searchStickers, - downloadSticker, + addStickerToTimeline, clearRecentStickers, setSelectedCategory, + addingSticker, } = useStickersStore(); - const [addingSticker, setAddingSticker] = useState<string | null>(null); const [localSearchQuery, setLocalSearchQuery] = useState(searchQuery); const [collectionsToShow, setCollectionsToShow] = useState(20); const [showCollectionItems, setShowCollectionItems] = useState(false); @@ -273,48 +250,11 @@ function StickersContentView({ category }: { category: StickerCategory }) { }, [localSearchQuery]); const handleAddSticker = async (iconName: string) => { - if (!activeProject) { - toast.error("No active project"); - return; - } - - setAddingSticker(iconName); - try { - const file = await downloadSticker(iconName); - - if (!file) { - throw new Error("Failed to download sticker"); - } - - const mediaItem: Omit<MediaFile, "id"> = { - name: iconName.replace(":", "-"), - type: "image", - file, - url: URL.createObjectURL(file), - width: 200, - height: 200, - duration: TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION, - ephemeral: false, - }; - - await addMediaFile(activeProject.id, mediaItem); - - const added = useMediaStore - .getState() - .mediaFiles.find( - (m) => m.url === mediaItem.url && m.name === mediaItem.name - ); - if (!added) throw new Error("Sticker not in media store"); - - addElementAtTime(added, currentTime); - - toast.success(`Added "${iconName}" to timeline`); + await addStickerToTimeline(iconName); } catch (error) { console.error("Failed to add sticker:", error); toast.error("Failed to add sticker to timeline"); - } finally { - setAddingSticker(null); } }; @@ -363,7 +303,7 @@ function StickersContentView({ category }: { category: StickerCategory }) { }, [isInCollection]); return ( - <div className="flex flex-col gap-5 mt-1 h-full"> + <div className="flex flex-col gap-5 mt-1 h-full p-4"> <div className="space-y-3"> <InputWithBack isExpanded={isInCollection} @@ -383,6 +323,7 @@ function StickersContentView({ category }: { category: StickerCategory }) { } value={localSearchQuery} onChange={setLocalSearchQuery} + disableAnimation={true} /> </div> |
