Initial commit

This commit is contained in:
twotalesanimation
2026-05-19 22:20:29 +02:00
commit 0fbe856dce
173 changed files with 38316 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
"use client";
import { useState, useCallback, useRef } from "react";
import { saveAnnotation, getAnnotationsForVersion } from "@/actions/annotations";
import type { AnnotationShape, AnnotationDrawingData } from "@/types";
import { useReviewStore } from "@/hooks/use-review-player";
import { useToast } from "@/components/ui/use-toast";
import { v4 as uuidv4 } from "uuid";
interface UseAnnotationsOptions {
versionId: string;
fps: number;
}
export function useAnnotations({ versionId, fps }: UseAnnotationsOptions) {
const [savedAnnotations, setSavedAnnotations] = useState<
{ id: string; frameNumber: number; drawingData: AnnotationDrawingData }[]
>([]);
const [sessionShapes, setSessionShapes] = useState<AnnotationShape[]>([]);
const [currentShape, setCurrentShape] = useState<AnnotationShape | null>(null);
const [isDrawing, setIsDrawing] = useState(false);
const { selectedTool, selectedColor, strokeWidth } = useReviewStore();
const { toast } = useToast();
const startShape = useCallback(
(frameNumber: number, point: { x: number; y: number }) => {
const shape: AnnotationShape = {
id: uuidv4(),
tool: selectedTool,
points: [point],
color: selectedColor,
strokeWidth,
frameNumber,
};
setCurrentShape(shape);
setIsDrawing(true);
},
[selectedTool, selectedColor, strokeWidth]
);
const addPoint = useCallback((point: { x: number; y: number }) => {
setCurrentShape((prev) =>
prev ? { ...prev, points: [...prev.points, point] } : null
);
}, []);
const finishShape = useCallback(
async (canvasWidth: number, canvasHeight: number) => {
if (!currentShape) return;
const finished = currentShape;
setSessionShapes((prev) => [...prev, finished]);
setCurrentShape(null);
setIsDrawing(false);
const drawingData: AnnotationDrawingData = {
shapes: [finished],
canvasWidth,
canvasHeight,
version: "1.0",
};
try {
await saveAnnotation({
versionId,
frameNumber: finished.frameNumber,
drawingData,
color: selectedColor,
});
} catch {
toast({ title: "Failed to save annotation", variant: "destructive" });
}
},
[currentShape, versionId, selectedColor, toast]
);
const clearSessionShapes = useCallback((frameNumber?: number) => {
if (frameNumber !== undefined) {
setSessionShapes((prev) => prev.filter((s) => s.frameNumber !== frameNumber));
} else {
setSessionShapes([]);
}
}, []);
const loadAnnotations = useCallback(async () => {
try {
const data = await getAnnotationsForVersion(versionId);
setSavedAnnotations(data as any);
} catch {
// non-fatal
}
}, [versionId]);
return {
savedAnnotations,
sessionShapes,
currentShape,
isDrawing,
startShape,
addPoint,
finishShape,
clearSessionShapes,
loadAnnotations,
};
}
+44
View File
@@ -0,0 +1,44 @@
"use client";
import { useMemo } from "react";
import type { CommentWithReplies } from "@/types";
export function useFrameComments(comments: CommentWithReplies[], frameNumber: number) {
const frameComments = useMemo(
() => comments.filter((c) => c.frameNumber === frameNumber),
[comments, frameNumber]
);
const nearbyComments = useMemo(
() => comments.filter((c) => Math.abs(c.frameNumber - frameNumber) <= 3 && c.frameNumber !== frameNumber),
[comments, frameNumber]
);
const unresolvedCount = useMemo(
() => comments.filter((c) => !c.isResolved).length,
[comments]
);
const commentsByFrame = useMemo(() => {
const map = new Map<number, CommentWithReplies[]>();
comments.forEach((c) => {
const existing = map.get(c.frameNumber) ?? [];
map.set(c.frameNumber, [...existing, c]);
});
return map;
}, [comments]);
const hasCommentAtFrame = (frame: number) => commentsByFrame.has(frame);
const getCommentsAtFrame = (frame: number) =>
commentsByFrame.get(frame) ?? [];
return {
frameComments,
nearbyComments,
unresolvedCount,
commentsByFrame,
hasCommentAtFrame,
getCommentsAtFrame,
};
}
+81
View File
@@ -0,0 +1,81 @@
import { create } from "zustand";
import type { AnnotationTool } from "@/types";
interface ReviewPlayerStore {
// Playback
isPlaying: boolean;
currentFrame: number;
currentTime: number;
duration: number;
playbackRate: number;
volume: number;
isMuted: boolean;
isFullscreen: boolean;
// Annotation
isAnnotating: boolean;
selectedTool: AnnotationTool;
selectedColor: string;
strokeWidth: number;
showAnnotations: boolean;
// Config (set from version data)
fps: number;
totalFrames: number;
// Actions
setPlaying: (playing: boolean) => void;
setCurrentFrame: (frame: number) => void;
setCurrentTime: (time: number) => void;
setDuration: (duration: number) => void;
setPlaybackRate: (rate: number) => void;
setVolume: (volume: number) => void;
setMuted: (muted: boolean) => void;
setFullscreen: (fullscreen: boolean) => void;
setAnnotating: (annotating: boolean) => void;
setSelectedTool: (tool: AnnotationTool) => void;
setSelectedColor: (color: string) => void;
setStrokeWidth: (width: number) => void;
setShowAnnotations: (show: boolean) => void;
setFps: (fps: number) => void;
setTotalFrames: (frames: number) => void;
reset: () => void;
}
const initialState = {
isPlaying: false,
currentFrame: 0,
currentTime: 0,
duration: 0,
playbackRate: 1,
volume: 1,
isMuted: false,
isFullscreen: false,
isAnnotating: false,
selectedTool: "freehand" as AnnotationTool,
selectedColor: "#ef4444",
strokeWidth: 2,
showAnnotations: true,
fps: 24,
totalFrames: 0,
};
export const useReviewStore = create<ReviewPlayerStore>((set) => ({
...initialState,
setPlaying: (isPlaying) => set({ isPlaying }),
setCurrentFrame: (currentFrame) => set({ currentFrame }),
setCurrentTime: (currentTime) => set({ currentTime }),
setDuration: (duration) => set({ duration }),
setPlaybackRate: (playbackRate) => set({ playbackRate }),
setVolume: (volume) => set({ volume }),
setMuted: (isMuted) => set({ isMuted }),
setFullscreen: (isFullscreen) => set({ isFullscreen }),
setAnnotating: (isAnnotating) => set({ isAnnotating }),
setSelectedTool: (selectedTool) => set({ selectedTool }),
setSelectedColor: (selectedColor) => set({ selectedColor }),
setStrokeWidth: (strokeWidth) => set({ strokeWidth }),
setShowAnnotations: (showAnnotations) => set({ showAnnotations }),
setFps: (fps) => set({ fps }),
setTotalFrames: (totalFrames) => set({ totalFrames }),
reset: () => set(initialState),
}));