summaryrefslogtreecommitdiff
path: root/apps/web/src/hooks
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/hooks')
-rw-r--r--apps/web/src/hooks/use-sound-search.ts156
-rw-r--r--apps/web/src/hooks/use-timeline-element-resize.ts64
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
);
}