diff options
Diffstat (limited to 'apps/web/src/components/editor/media-panel/views/sounds.tsx')
| -rw-r--r-- | apps/web/src/components/editor/media-panel/views/sounds.tsx | 562 |
1 files changed, 0 insertions, 562 deletions
diff --git a/apps/web/src/components/editor/media-panel/views/sounds.tsx b/apps/web/src/components/editor/media-panel/views/sounds.tsx deleted file mode 100644 index 700eaea7..00000000 --- a/apps/web/src/components/editor/media-panel/views/sounds.tsx +++ /dev/null @@ -1,562 +0,0 @@ -"use client"; - -import { Input } from "@/components/ui/input"; -import { useState, useMemo, useEffect } from "react"; -import { Separator } from "@/components/ui/separator"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { - PlayIcon, - PauseIcon, - HeartIcon, - PlusIcon, - ListFilter, -} from "lucide-react"; -import { useSoundsStore } from "@/stores/sounds-store"; -import { useSoundSearch } from "@/hooks/use-sound-search"; -import type { SoundEffect, SavedSound } from "@/types/sounds"; -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuCheckboxItem, -} from "@/components/ui/dropdown-menu"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { cn } from "@/lib/utils"; -import { useInfiniteScroll } from "@/hooks/use-infinite-scroll"; - -export function SoundsView() { - return ( - <div className="h-full flex flex-col"> - <Tabs defaultValue="sound-effects" className="flex flex-col h-full"> - <div className="px-3 pt-4 pb-0"> - <TabsList> - <TabsTrigger value="sound-effects">Sound effects</TabsTrigger> - <TabsTrigger value="songs">Songs</TabsTrigger> - <TabsTrigger value="saved">Saved</TabsTrigger> - </TabsList> - </div> - <Separator className="my-4" /> - <TabsContent - value="sound-effects" - className="p-5 pt-0 mt-0 flex-1 flex flex-col min-h-0" - > - <SoundEffectsView /> - </TabsContent> - <TabsContent - value="saved" - className="p-5 pt-0 mt-0 flex-1 flex flex-col min-h-0" - > - <SavedSoundsView /> - </TabsContent> - <TabsContent - value="songs" - className="p-5 pt-0 mt-0 flex-1 flex flex-col min-h-0" - > - <SongsView /> - </TabsContent> - </Tabs> - </div> - ); -} - -function SoundEffectsView() { - const { - topSoundEffects, - isLoading, - searchQuery, - setSearchQuery, - scrollPosition, - setScrollPosition, - loadSavedSounds, - isSoundSaved, - toggleSavedSound, - showCommercialOnly, - toggleCommercialFilter, - hasLoaded, - setTopSoundEffects, - setLoading, - setError, - setHasLoaded, - setCurrentPage, - setHasNextPage, - setTotalCount, - } = useSoundsStore(); - const { - results: searchResults, - isLoading: isSearching, - loadMore, - hasNextPage, - isLoadingMore, - } = useSoundSearch(searchQuery, showCommercialOnly); - - // Audio playback state - const [playingId, setPlayingId] = useState<number | null>(null); - const [audioElement, setAudioElement] = useState<HTMLAudioElement | null>( - null - ); - - const { scrollAreaRef, handleScroll } = useInfiniteScroll({ - onLoadMore: loadMore, - hasMore: hasNextPage, - isLoading: isLoadingMore || isSearching, - }); - - 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 }); - }, 100); - - 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; - }, [searchQuery, searchResults, topSoundEffects]); - - const playSound = (sound: SoundEffect) => { - if (playingId === sound.id) { - audioElement?.pause(); - setPlayingId(null); - return; - } - - // Stop previous sound - audioElement?.pause(); - - if (sound.previewUrl) { - const audio = new Audio(sound.previewUrl); - audio.addEventListener("ended", () => { - setPlayingId(null); - }); - audio.addEventListener("error", (e) => { - setPlayingId(null); - }); - audio.play().catch((error) => { - setPlayingId(null); - }); - - setAudioElement(audio); - setPlayingId(sound.id); - } - }; - - return ( - <div className="flex flex-col gap-5 mt-1 h-full"> - <div className="flex items-center gap-3"> - <Input - placeholder="Search sound effects" - className="bg-panel-accent w-full" - containerClassName="w-full" - value={searchQuery} - onChange={(e) => setSearchQuery(e.target.value)} - showClearIcon - onClear={() => setSearchQuery("")} - /> - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Button - variant="text" - size="icon" - className={cn(showCommercialOnly && "text-primary")} - > - <ListFilter className="w-4 h-4" /> - </Button> - </DropdownMenuTrigger> - <DropdownMenuContent align="end" className="w-56"> - <DropdownMenuCheckboxItem - checked={showCommercialOnly} - onCheckedChange={toggleCommercialFilter} - > - Show only commercially licensed - </DropdownMenuCheckboxItem> - <div className="px-2 py-1.5 text-xs text-muted-foreground"> - {showCommercialOnly - ? "Only showing sounds licensed for commercial use" - : "Showing all sounds regardless of license"} - </div> - </DropdownMenuContent> - </DropdownMenu> - </div> - - <div className="relative h-full overflow-hidden"> - <ScrollArea - className="flex-1 h-full" - ref={scrollAreaRef} - onScrollCapture={handleScrollWithPosition} - > - <div className="flex flex-col gap-4"> - {isLoading && !searchQuery && ( - <div className="text-muted-foreground text-sm"> - Loading sounds... - </div> - )} - {isSearching && searchQuery && ( - <div className="text-muted-foreground text-sm">Searching...</div> - )} - {displayedSounds.map((sound) => ( - <AudioItem - key={sound.id} - sound={sound} - isPlaying={playingId === sound.id} - onPlay={() => playSound(sound)} - isSaved={isSoundSaved(sound.id)} - onToggleSaved={() => toggleSavedSound(sound)} - /> - ))} - {!isLoading && !isSearching && displayedSounds.length === 0 && ( - <div className="text-muted-foreground text-sm"> - {searchQuery ? "No sounds found" : "No sounds available"} - </div> - )} - {isLoadingMore && ( - <div className="text-muted-foreground text-sm text-center py-4"> - Loading more sounds... - </div> - )} - </div> - </ScrollArea> - </div> - </div> - ); -} - -function SavedSoundsView() { - const { - savedSounds, - isLoadingSavedSounds, - savedSoundsError, - loadSavedSounds, - isSoundSaved, - toggleSavedSound, - clearSavedSounds, - } = useSoundsStore(); - - // Audio playback state - const [playingId, setPlayingId] = useState<number | null>(null); - const [audioElement, setAudioElement] = useState<HTMLAudioElement | null>( - null - ); - - // Clear confirmation dialog state - const [showClearDialog, setShowClearDialog] = useState(false); - - // Load saved sounds when tab becomes active - useEffect(() => { - loadSavedSounds(); - }, [loadSavedSounds]); - - const playSound = (sound: SavedSound) => { - if (playingId === sound.id) { - audioElement?.pause(); - setPlayingId(null); - return; - } - - // Stop previous sound - audioElement?.pause(); - - if (sound.previewUrl) { - const audio = new Audio(sound.previewUrl); - audio.addEventListener("ended", () => { - setPlayingId(null); - }); - audio.addEventListener("error", (e) => { - setPlayingId(null); - }); - audio.play().catch((error) => { - setPlayingId(null); - }); - - setAudioElement(audio); - setPlayingId(sound.id); - } - }; - - // Convert SavedSound to SoundEffect for compatibility with AudioItem - const convertToSoundEffect = (savedSound: SavedSound): SoundEffect => ({ - id: savedSound.id, - name: savedSound.name, - description: "", - url: "", - previewUrl: savedSound.previewUrl, - downloadUrl: savedSound.downloadUrl, - duration: savedSound.duration, - filesize: 0, - type: "audio", - channels: 0, - bitrate: 0, - bitdepth: 0, - samplerate: 0, - username: savedSound.username, - tags: savedSound.tags, - license: savedSound.license, - created: savedSound.savedAt, - downloads: 0, - rating: 0, - ratingCount: 0, - }); - - if (isLoadingSavedSounds) { - return ( - <div className="flex items-center justify-center h-full"> - <div className="text-muted-foreground text-sm"> - Loading saved sounds... - </div> - </div> - ); - } - - if (savedSoundsError) { - return ( - <div className="flex items-center justify-center h-full"> - <div className="text-destructive text-sm"> - Error: {savedSoundsError} - </div> - </div> - ); - } - - if (savedSounds.length === 0) { - return ( - <div className="bg-panel h-full p-4 flex flex-col items-center justify-center gap-3"> - <HeartIcon - className="w-10 h-10 text-muted-foreground" - strokeWidth={1.5} - /> - <div className="flex flex-col gap-2 text-center"> - <p className="text-lg font-medium">No saved sounds</p> - <p className="text-sm text-muted-foreground text-balance"> - Click the heart icon on any sound to save it here - </p> - </div> - </div> - ); - } - - return ( - <div className="flex flex-col gap-5 mt-1 h-full"> - <div className="flex items-center justify-between"> - <p className="text-sm text-muted-foreground"> - {savedSounds.length} saved{" "} - {savedSounds.length === 1 ? "sound" : "sounds"} - </p> - <Dialog open={showClearDialog} onOpenChange={setShowClearDialog}> - <DialogTrigger asChild> - <Button - variant="text" - size="sm" - className="h-auto text-muted-foreground hover:text-destructive !opacity-100" - > - Clear all - </Button> - </DialogTrigger> - <DialogContent> - <DialogHeader> - <DialogTitle>Clear all saved sounds?</DialogTitle> - <DialogDescription> - This will permanently remove all {savedSounds.length} saved - sounds from your collection. This action cannot be undone. - </DialogDescription> - </DialogHeader> - <DialogFooter> - <Button variant="text" onClick={() => setShowClearDialog(false)}> - Cancel - </Button> - <Button - variant="destructive" - onClick={async () => { - await clearSavedSounds(); - setShowClearDialog(false); - }} - > - Clear all sounds - </Button> - </DialogFooter> - </DialogContent> - </Dialog> - </div> - - <div className="relative h-full overflow-hidden"> - <ScrollArea className="flex-1 h-full"> - <div className="flex flex-col gap-4"> - {savedSounds.map((sound) => ( - <AudioItem - key={sound.id} - sound={convertToSoundEffect(sound)} - isPlaying={playingId === sound.id} - onPlay={() => playSound(sound)} - isSaved={isSoundSaved(sound.id)} - onToggleSaved={() => - toggleSavedSound(convertToSoundEffect(sound)) - } - /> - ))} - </div> - </ScrollArea> - </div> - </div> - ); -} - -function SongsView() { - return <div>Songs</div>; -} - -interface AudioItemProps { - sound: SoundEffect; - isPlaying: boolean; - onPlay: () => void; - isSaved: boolean; - onToggleSaved: () => void; -} - -function AudioItem({ - sound, - isPlaying, - onPlay, - isSaved, - onToggleSaved, -}: AudioItemProps) { - const { addSoundToTimeline } = useSoundsStore(); - - const handleClick = () => { - onPlay(); - }; - - const handleSaveClick = (e: React.MouseEvent) => { - e.stopPropagation(); - onToggleSaved(); - }; - - const handleAddToTimeline = async (e: React.MouseEvent) => { - e.stopPropagation(); - await addSoundToTimeline(sound); - }; - - return ( - <div - className="group flex items-center gap-3 opacity-100 hover:opacity-75 transition-opacity cursor-pointer" - onClick={handleClick} - > - <div className="relative w-12 h-12 bg-accent rounded-md flex items-center justify-center overflow-hidden shrink-0"> - <div className="absolute inset-0 bg-gradient-to-br from-primary/20 to-transparent" /> - {isPlaying ? ( - <PauseIcon className="w-5 h-5" /> - ) : ( - <PlayIcon className="w-5 h-5" /> - )} - </div> - - <div className="flex-1 min-w-0 overflow-hidden"> - <p className="font-medium truncate text-sm">{sound.name}</p> - <span className="text-xs text-muted-foreground truncate block"> - {sound.username} - </span> - </div> - - <div className="flex items-center gap-3 pr-2"> - <Button - variant="text" - size="icon" - className="text-muted-foreground hover:text-foreground !opacity-100 w-auto" - onClick={handleAddToTimeline} - title="Add to timeline" - > - <PlusIcon className="w-4 h-4" /> - </Button> - <Button - variant="text" - size="icon" - className={`hover:text-foreground !opacity-100 w-auto ${ - isSaved - ? "text-red-500 hover:text-red-600" - : "text-muted-foreground" - }`} - onClick={handleSaveClick} - title={isSaved ? "Remove from saved" : "Save sound"} - > - <HeartIcon className={`w-4 h-4 ${isSaved ? "fill-current" : ""}`} /> - </Button> - </div> - </div> - ); -} |
