mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
feat: add license filtering for sound effects
This commit is contained in:
@@ -12,6 +12,7 @@ const searchParamsSchema = z.object({
|
|||||||
.enum(["downloads", "rating", "created", "score"])
|
.enum(["downloads", "rating", "created", "score"])
|
||||||
.default("downloads"),
|
.default("downloads"),
|
||||||
min_rating: z.coerce.number().min(0).max(5).default(3),
|
min_rating: z.coerce.number().min(0).max(5).default(3),
|
||||||
|
commercial_only: z.coerce.boolean().default(true),
|
||||||
});
|
});
|
||||||
|
|
||||||
const freesoundResultSchema = z.object({
|
const freesoundResultSchema = z.object({
|
||||||
@@ -124,6 +125,7 @@ export async function GET(request: NextRequest) {
|
|||||||
page_size: pageSize,
|
page_size: pageSize,
|
||||||
sort,
|
sort,
|
||||||
min_rating,
|
min_rating,
|
||||||
|
commercial_only,
|
||||||
} = validationResult.data;
|
} = validationResult.data;
|
||||||
|
|
||||||
if (type === "songs") {
|
if (type === "songs") {
|
||||||
@@ -160,6 +162,15 @@ export async function GET(request: NextRequest) {
|
|||||||
if (type === "effects" || !type) {
|
if (type === "effects" || !type) {
|
||||||
params.append("filter", "duration:[* TO 30.0]");
|
params.append("filter", "duration:[* TO 30.0]");
|
||||||
params.append("filter", `avg_rating:[${min_rating} TO *]`);
|
params.append("filter", `avg_rating:[${min_rating} TO *]`);
|
||||||
|
|
||||||
|
// Filter by license if commercial_only is true
|
||||||
|
if (commercial_only) {
|
||||||
|
params.append(
|
||||||
|
"filter",
|
||||||
|
'license:("Attribution" OR "Creative Commons 0" OR "Attribution Noncommercial" OR "Attribution Commercial")'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
params.append(
|
params.append(
|
||||||
"filter",
|
"filter",
|
||||||
"tag:sound-effect OR tag:sfx OR tag:foley OR tag:ambient OR tag:nature OR tag:mechanical OR tag:electronic OR tag:impact OR tag:whoosh OR tag:explosion"
|
"tag:sound-effect OR tag:sfx OR tag:foley OR tag:ambient OR tag:nature OR tag:mechanical OR tag:electronic OR tag:impact OR tag:whoosh OR tag:explosion"
|
||||||
|
|||||||
@@ -5,11 +5,23 @@ import { useState, useMemo, useRef, useEffect } from "react";
|
|||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { PlayIcon, PauseIcon, HeartIcon, PlusIcon } from "lucide-react";
|
import {
|
||||||
|
PlayIcon,
|
||||||
|
PauseIcon,
|
||||||
|
HeartIcon,
|
||||||
|
PlusIcon,
|
||||||
|
ListFilter,
|
||||||
|
} from "lucide-react";
|
||||||
import { useSoundsStore } from "@/stores/sounds-store";
|
import { useSoundsStore } from "@/stores/sounds-store";
|
||||||
import { useSoundSearch } from "@/hooks/use-sound-search";
|
import { useSoundSearch } from "@/hooks/use-sound-search";
|
||||||
import type { SoundEffect, SavedSound } from "@/types/sounds";
|
import type { SoundEffect, SavedSound } from "@/types/sounds";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -19,6 +31,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export function SoundsView() {
|
export function SoundsView() {
|
||||||
return (
|
return (
|
||||||
@@ -66,6 +79,8 @@ function SoundEffectsView() {
|
|||||||
loadSavedSounds,
|
loadSavedSounds,
|
||||||
isSoundSaved,
|
isSoundSaved,
|
||||||
toggleSavedSound,
|
toggleSavedSound,
|
||||||
|
showCommercialOnly,
|
||||||
|
toggleCommercialFilter,
|
||||||
} = useSoundsStore();
|
} = useSoundsStore();
|
||||||
const {
|
const {
|
||||||
results: searchResults,
|
results: searchResults,
|
||||||
@@ -73,7 +88,7 @@ function SoundEffectsView() {
|
|||||||
loadMore,
|
loadMore,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
isLoadingMore,
|
isLoadingMore,
|
||||||
} = useSoundSearch(searchQuery);
|
} = useSoundSearch(searchQuery, showCommercialOnly);
|
||||||
|
|
||||||
// Audio playback state
|
// Audio playback state
|
||||||
const [playingId, setPlayingId] = useState<number | null>(null);
|
const [playingId, setPlayingId] = useState<number | null>(null);
|
||||||
@@ -144,14 +159,41 @@ function SoundEffectsView() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-5 mt-1 h-full">
|
<div className="flex flex-col gap-5 mt-1 h-full">
|
||||||
<Input
|
<div className="flex items-center gap-3">
|
||||||
placeholder="Search sound effects"
|
<Input
|
||||||
className="bg-panel-accent"
|
placeholder="Search sound effects"
|
||||||
value={searchQuery}
|
className="bg-panel-accent w-full"
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
containerClassName="w-full"
|
||||||
showClearIcon
|
value={searchQuery}
|
||||||
onClear={() => setSearchQuery("")}
|
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">
|
<div className="relative h-full overflow-hidden">
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
|
|||||||
@@ -126,20 +126,24 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
dropdownMenuItemVariants({ variant }),
|
dropdownMenuItemVariants({ variant }),
|
||||||
"pl-8 pr-2",
|
"pl-2 pr-8",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
|
onSelect={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
{children}
|
||||||
|
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<Check className="h-4 w-4" />
|
<Check className="h-4 w-4" />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
));
|
));
|
||||||
|
|
||||||
DropdownMenuCheckboxItem.displayName =
|
DropdownMenuCheckboxItem.displayName =
|
||||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface InputProps extends React.ComponentProps<"input"> {
|
|||||||
onShowPasswordChange?: (show: boolean) => void;
|
onShowPasswordChange?: (show: boolean) => void;
|
||||||
showClearIcon?: boolean;
|
showClearIcon?: boolean;
|
||||||
onClear?: () => void;
|
onClear?: () => void;
|
||||||
|
containerClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
@@ -16,6 +17,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
{
|
{
|
||||||
className,
|
className,
|
||||||
type,
|
type,
|
||||||
|
containerClassName,
|
||||||
showPassword,
|
showPassword,
|
||||||
onShowPasswordChange,
|
onShowPasswordChange,
|
||||||
showClearIcon,
|
showClearIcon,
|
||||||
@@ -45,7 +47,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
iconCount === 2 ? "pr-20" : iconCount === 1 ? "pr-10" : "";
|
iconCount === 2 ? "pr-20" : iconCount === 1 ? "pr-10" : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={hasIcons ? "relative w-full" : ""}>
|
<div className={cn(hasIcons ? "relative w-full" : "", containerClassName)}>
|
||||||
<input
|
<input
|
||||||
type={inputType}
|
type={inputType}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { useSoundsStore } from "@/stores/sounds-store";
|
|||||||
* - Proper error handling
|
* - Proper error handling
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function useSoundSearch(query: string) {
|
export function useSoundSearch(query: string, commercialOnly: boolean) {
|
||||||
const {
|
const {
|
||||||
searchResults,
|
searchResults,
|
||||||
isSearching,
|
isSearching,
|
||||||
@@ -49,6 +49,7 @@ export function useSoundSearch(query: string) {
|
|||||||
searchParams.set("q", query);
|
searchParams.set("q", query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
searchParams.set("commercial_only", commercialOnly.toString());
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/sounds/search?${searchParams.toString()}`
|
`/api/sounds/search?${searchParams.toString()}`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ interface SoundsStore {
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
hasLoaded: boolean;
|
hasLoaded: boolean;
|
||||||
|
|
||||||
|
// Filter state
|
||||||
|
showCommercialOnly: boolean;
|
||||||
|
toggleCommercialFilter: () => void;
|
||||||
|
|
||||||
// Search state
|
// Search state
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
searchResults: SoundEffect[];
|
searchResults: SoundEffect[];
|
||||||
@@ -72,6 +76,11 @@ export const useSoundsStore = create<SoundsStore>((set, get) => ({
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
hasLoaded: false,
|
hasLoaded: false,
|
||||||
|
showCommercialOnly: true,
|
||||||
|
|
||||||
|
toggleCommercialFilter: () => {
|
||||||
|
set((state) => ({ showCommercialOnly: !state.showCommercialOnly }));
|
||||||
|
},
|
||||||
|
|
||||||
// Search state
|
// Search state
|
||||||
searchQuery: "",
|
searchQuery: "",
|
||||||
|
|||||||
Reference in New Issue
Block a user