diff options
Diffstat (limited to 'apps/web/src/hooks')
| -rw-r--r-- | apps/web/src/hooks/use-sound-search.ts | 156 | ||||
| -rw-r--r-- | apps/web/src/hooks/use-timeline-element-resize.ts | 64 |
2 files changed, 210 insertions, 10 deletions
diff --git a/apps/web/src/hooks/use-sound-search.ts b/apps/web/src/hooks/use-sound-search.ts new file mode 100644 index 00000000..db9a7e8e --- /dev/null +++ b/apps/web/src/hooks/use-sound-search.ts @@ -0,0 +1,156 @@ +import { useEffect } from "react";
+import { useSoundsStore } from "@/stores/sounds-store";
+
+/**
+ * Custom hook for searching sound effects with race condition protection.
+ * Uses global Zustand store to persist search state across tab switches.
+ * - Debounced search (300ms)
+ * - Race condition protection with cleanup
+ * - Proper error handling
+ */
+
+export function useSoundSearch(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();
+
+ // Load more function for infinite scroll
+ const loadMore = async () => {
+ if (isLoadingMore || !hasNextPage) return;
+
+ try {
+ setLoadingMore(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();
+
+ // Append to appropriate array based on whether we have a query
+ if (query.trim()) {
+ appendSearchResults(data.results);
+ } else {
+ appendTopSounds(data.results);
+ }
+
+ setCurrentPage(nextPage);
+ setHasNextPage(!!data.next);
+ setTotalCount(data.count);
+ } else {
+ setSearchError(`Load more failed: ${response.status}`);
+ }
+ } catch (err) {
+ setSearchError(err instanceof Error ? err.message : "Load more failed");
+ } finally {
+ setLoadingMore(false);
+ }
+ };
+
+ useEffect(() => {
+ if (!query.trim()) {
+ setSearchResults([]);
+ setSearchError(null);
+ setLastSearchQuery("");
+ // Don't reset pagination here - top sounds pagination is managed by prefetcher
+ return;
+ }
+
+ // If we already searched for this query and have results, don't search again
+ if (query === lastSearchQuery && searchResults.length > 0) {
+ return;
+ }
+
+ let ignore = false;
+
+ const timeoutId = setTimeout(async () => {
+ try {
+ setSearching(true);
+ setSearchError(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(data.results);
+ setLastSearchQuery(query);
+ setHasNextPage(!!data.next);
+ setTotalCount(data.count);
+ setCurrentPage(1);
+ } else {
+ setSearchError(`Search failed: ${response.status}`);
+ }
+ }
+ } catch (err) {
+ if (!ignore) {
+ setSearchError(err instanceof Error ? err.message : "Search failed");
+ }
+ } finally {
+ if (!ignore) {
+ setSearching(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,
+ };
+}
diff --git a/apps/web/src/hooks/use-timeline-element-resize.ts b/apps/web/src/hooks/use-timeline-element-resize.ts index e7d46dff..ba13e05a 100644 --- a/apps/web/src/hooks/use-timeline-element-resize.ts +++ b/apps/web/src/hooks/use-timeline-element-resize.ts @@ -2,6 +2,8 @@ import { useState, useEffect } from "react"; import { ResizeState, TimelineElement, TimelineTrack } from "@/types/timeline"; import { useMediaStore } from "@/stores/media-store"; import { useTimelineStore } from "@/stores/timeline-store"; +import { useProjectStore } from "@/stores/project-store"; +import { snapTimeToFrame } from "@/constants/timeline-constants"; interface UseTimelineElementResizeProps { element: TimelineElement; @@ -109,6 +111,10 @@ export function useTimelineElementResize({ // Reasonable sensitivity for resize operations - similar to timeline scale const deltaTime = deltaX / (50 * zoomLevel); + // Get project FPS for frame snapping + const projectStore = useProjectStore.getState(); + const projectFps = projectStore.activeProject?.fps || 30; + if (resizing.side === "left") { // Left resize - different behavior for media vs text/image elements const maxAllowed = element.duration - resizing.initialTrimEnd - 0.1; @@ -116,9 +122,15 @@ export function useTimelineElementResize({ if (calculated >= 0) { // Normal trimming within available content - const newTrimStart = Math.min(maxAllowed, calculated); + const newTrimStart = snapTimeToFrame( + Math.min(maxAllowed, calculated), + projectFps + ); const trimDelta = newTrimStart - resizing.initialTrimStart; - const newStartTime = element.startTime + trimDelta; + const newStartTime = snapTimeToFrame( + element.startTime + trimDelta, + projectFps + ); updateElementTrim( track.id, @@ -135,8 +147,14 @@ export function useTimelineElementResize({ const extensionAmount = Math.abs(calculated); const maxExtension = element.startTime; const actualExtension = Math.min(extensionAmount, maxExtension); - const newStartTime = element.startTime - actualExtension; - const newDuration = element.duration + actualExtension; + const newStartTime = snapTimeToFrame( + element.startTime - actualExtension, + projectFps + ); + const newDuration = snapTimeToFrame( + element.duration + actualExtension, + projectFps + ); // Keep trimStart at 0 and extend the element updateElementTrim( @@ -152,7 +170,10 @@ export function useTimelineElementResize({ // Video/Audio: can't extend beyond original content - limit to trimStart = 0 const newTrimStart = 0; const trimDelta = newTrimStart - resizing.initialTrimStart; - const newStartTime = element.startTime + trimDelta; + const newStartTime = snapTimeToFrame( + element.startTime + trimDelta, + projectFps + ); updateElementTrim( track.id, @@ -173,7 +194,10 @@ export function useTimelineElementResize({ if (canExtendElementDuration()) { // Extend the duration instead of reducing trimEnd further const extensionNeeded = Math.abs(calculated); - const newDuration = element.duration + extensionNeeded; + const newDuration = snapTimeToFrame( + element.duration + extensionNeeded, + projectFps + ); const newTrimEnd = 0; // Reset trimEnd to 0 since we're extending // Update duration first, then trim @@ -197,14 +221,34 @@ export function useTimelineElementResize({ } } else { // Normal trimming within original duration - const maxTrimEnd = element.duration - resizing.initialTrimStart - 0.1; // Leave at least 0.1s visible - const newTrimEnd = Math.max(0, Math.min(maxTrimEnd, calculated)); + // Calculate the desired end time based on mouse movement + const currentEndTime = + element.startTime + + element.duration - + element.trimStart - + element.trimEnd; + const desiredEndTime = currentEndTime + deltaTime; + + // Snap the desired end time to frame + const snappedEndTime = snapTimeToFrame(desiredEndTime, projectFps); + + // Calculate what trimEnd should be to achieve this snapped end time + const newTrimEnd = Math.max( + 0, + element.duration - + element.trimStart - + (snappedEndTime - element.startTime) + ); + + // Ensure we don't trim more than available content (leave at least 0.1s visible) + const maxTrimEnd = element.duration - element.trimStart - 0.1; + const finalTrimEnd = Math.min(maxTrimEnd, newTrimEnd); updateElementTrim( track.id, element.id, - resizing.initialTrimStart, - newTrimEnd, + element.trimStart, + finalTrimEnd, false ); } |
