mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
* feat: major editor overhaul (assets, properties, timeline, fonts) Refactor editor core systems to standardize UI architecture and improve performance. Assets & Properties: - Replace monolithic property items with composable `Section` architecture. - Add specialized sections for Transform, Blending, and Text. - Implement `NumberField` with scrubbing and math evaluation. - Add new ColorPicker with EyeDropper and multiple format support. - Standardize asset panels using new `PanelView` layout. Fonts & Stickers: - Implement custom font atlas/sprite system for high-performance previews. - Add virtualized FontPicker with search and favorites. - Refactor stickers to use a provider-based architecture (icons, emoji, flags, shapes). - Standardize sticker IDs to `provider:value` format. Timeline & Interaction: - Convert bookmarks to rich objects with notes, colors, and duration. - Refactor drag-and-drop to use Command pattern (enabling proper undo/redo). - Add Shift modifier to disable snapping during moves/resizes. - Add new overlays for layout guides and text editing. Renderer: - Add support for multi-line text, custom line-height, and letter-spacing. - Implement global composite operation (blend modes). - Update sticker node to resolve dynamic provider IDs. Infrastructure: - Add storage migrations (v3->v6) for text weights, sticker IDs, and bookmarks. - Update global styles and core UI components (Button, Input, Popover). * add ts-nocheck directive to settings-legacy.tsx to suppress TypeScript errors * fix: correct global composite operation assignment in TextNode to ensure proper blend mode handling * deleted shadcn components with errors * formatting * fix linter issues * migrate from next middleware to proxy * add missing component back * add breadcrumb back * chore: add @radix-ui/react-primitive deps * chore: more deps * chore: add missing env vars to bun-ci * next env
155 lines
3.2 KiB
TypeScript
155 lines
3.2 KiB
TypeScript
import { useEffect } from "react";
|
|
import { useSoundsStore } from "@/stores/sounds-store";
|
|
|
|
export function useSoundSearch({
|
|
query,
|
|
commercialOnly,
|
|
}: {
|
|
query: string;
|
|
commercialOnly: boolean;
|
|
}) {
|
|
const {
|
|
searchResults,
|
|
isSearching,
|
|
searchError,
|
|
lastSearchQuery,
|
|
currentPage,
|
|
hasNextPage,
|
|
isLoadingMore,
|
|
totalCount,
|
|
setSearchResults,
|
|
setSearching,
|
|
setSearchError,
|
|
setLastSearchQuery,
|
|
setCurrentPage,
|
|
setHasNextPage,
|
|
setTotalCount,
|
|
setLoadingMore,
|
|
appendSearchResults,
|
|
appendTopSounds,
|
|
resetPagination,
|
|
} = useSoundsStore();
|
|
|
|
const loadMore = async () => {
|
|
if (isLoadingMore || !hasNextPage) return;
|
|
|
|
try {
|
|
setLoadingMore({ loading: true });
|
|
const nextPage = currentPage + 1;
|
|
|
|
const searchParams = new URLSearchParams({
|
|
page: nextPage.toString(),
|
|
type: "effects",
|
|
});
|
|
|
|
if (query.trim()) {
|
|
searchParams.set("q", query);
|
|
}
|
|
|
|
searchParams.set("commercial_only", commercialOnly.toString());
|
|
const response = await fetch(
|
|
`/api/sounds/search?${searchParams.toString()}`,
|
|
);
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
|
|
if (query.trim()) {
|
|
appendSearchResults(data.results);
|
|
} else {
|
|
appendTopSounds(data.results);
|
|
}
|
|
|
|
setCurrentPage({ page: nextPage });
|
|
setHasNextPage({ hasNext: !!data.next });
|
|
setTotalCount(data.count);
|
|
} else {
|
|
setSearchError({ error: `Load more failed: ${response.status}` });
|
|
}
|
|
} catch (err) {
|
|
setSearchError({
|
|
error: err instanceof Error ? err.message : "Load more failed",
|
|
});
|
|
} finally {
|
|
setLoadingMore({ loading: false });
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!query.trim()) {
|
|
setSearchResults({ results: [] });
|
|
setSearchError({ error: null });
|
|
setLastSearchQuery({ query: "" });
|
|
return;
|
|
}
|
|
|
|
if (query === lastSearchQuery && searchResults.length > 0) {
|
|
return;
|
|
}
|
|
|
|
let ignore = false;
|
|
|
|
const timeoutId = setTimeout(async () => {
|
|
try {
|
|
setSearching({ searching: true });
|
|
setSearchError({ error: null });
|
|
resetPagination();
|
|
|
|
const response = await fetch(
|
|
`/api/sounds/search?q=${encodeURIComponent(query)}&type=effects&page=1`,
|
|
);
|
|
|
|
if (!ignore) {
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setSearchResults({ results: data.results });
|
|
setLastSearchQuery({ query: query });
|
|
setHasNextPage({ hasNext: !!data.next });
|
|
setTotalCount({ count: data.count });
|
|
setCurrentPage({ page: 1 });
|
|
} else {
|
|
setSearchError({ error: `Search failed: ${response.status}` });
|
|
}
|
|
}
|
|
} catch (err) {
|
|
if (!ignore) {
|
|
setSearchError({
|
|
error: err instanceof Error ? err.message : "Search failed",
|
|
});
|
|
}
|
|
} finally {
|
|
if (!ignore) {
|
|
setSearching({ searching: false });
|
|
}
|
|
}
|
|
}, 300);
|
|
|
|
return () => {
|
|
clearTimeout(timeoutId);
|
|
ignore = true;
|
|
};
|
|
}, [
|
|
query,
|
|
lastSearchQuery,
|
|
searchResults.length,
|
|
setSearchResults,
|
|
setSearching,
|
|
setSearchError,
|
|
setLastSearchQuery,
|
|
setCurrentPage,
|
|
setHasNextPage,
|
|
setTotalCount,
|
|
resetPagination,
|
|
]);
|
|
|
|
return {
|
|
results: searchResults,
|
|
isLoading: isSearching,
|
|
error: searchError,
|
|
loadMore,
|
|
hasNextPage,
|
|
isLoadingMore,
|
|
totalCount,
|
|
};
|
|
}
|