diff --git a/rollup.config.mjs b/rollup.config.mjs index 9e79408..95d8e44 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -4,7 +4,6 @@ import json from '@rollup/plugin-json'; import postcss from 'rollup-plugin-postcss'; import postcssImport from 'postcss-import'; import postcssUrl from 'postcss-url'; -import copy from 'rollup-plugin-copy'; import tailwind from 'tailwindcss'; import nodeResolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; diff --git a/src/components/atoms/Button.tsx b/src/components/atoms/Button.tsx index 90b0c90..a48c618 100644 --- a/src/components/atoms/Button.tsx +++ b/src/components/atoms/Button.tsx @@ -4,16 +4,32 @@ function Button({ children, variant = "default", className, + onClick, ...props }: React.ComponentPropsWithoutRef<"button"> & { children: React.ReactNode; className?: string; + onClick?: () => void; variant?: "primary" | "danger" | "secondary" | "default"; }) { const variantClass = getVariantClass(variant); + const handleClick = () => { + onClick?.(); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" || e.key === " ") { + onClick?.(); + } + }; + return ( + + + Highlights diff --git a/src/types.ts b/src/types.ts index 3b8fa19..ee4e176 100644 --- a/src/types.ts +++ b/src/types.ts @@ -102,3 +102,22 @@ export interface ChatMessage extends LogEvent { */ message: string; } + +/** + * A selection within a video. + */ +export type VideoClip = { + id: string; + /** + * Start time in milliseconds + */ + start: number; + /** + * End time in milliseconds + */ + end: number; + /** + * URL to the keyframe image, if available. + */ + keyframeSrc?: string; +}; diff --git a/src/utils/findGaps.test.ts b/src/utils/findGaps.test.ts new file mode 100644 index 0000000..bc32ac3 --- /dev/null +++ b/src/utils/findGaps.test.ts @@ -0,0 +1,101 @@ +import type { Section } from "types"; +import { describe, expect, it } from "vitest"; +import findGaps from "./findGaps"; + +describe("findGaps", () => { + it("should find gaps between sections", async () => { + const sections: Section[] = [ + { timestamp: 0, timestamp_end: 10 }, + { timestamp: 20, timestamp_end: 30 }, + { timestamp: 40, timestamp_end: 50 }, + ]; + const minGapDuration = 5; + + const gaps = await findGaps(sections, minGapDuration); + + expect(gaps).toEqual([ + { id: "10-20", start: 10, end: 20 }, + { id: "30-40", start: 30, end: 40 }, + ]); + }); + + it("should return an empty array if no gaps are found", async () => { + const sections: Section[] = [ + { timestamp: 0, timestamp_end: 10 }, + { timestamp: 11, timestamp_end: 20 }, + ]; + const minGapDuration = 5; + + const gaps = await findGaps(sections, minGapDuration); + + expect(gaps).toEqual([]); + }); + + it("should handle sections with undefined timestamp_end", async () => { + const sections: Section[] = [ + { timestamp: 0, timestamp_end: 10 }, + { timestamp: 20 }, + { timestamp: 30, timestamp_end: 40 }, + ]; + const minGapDuration = 5; + + const gaps = await findGaps(sections, minGapDuration); + + expect(gaps).toEqual([{ id: "10-30", start: 10, end: 30 }]); + }); + + it("should ignore gaps that are less than the minimum gap duration", async () => { + const sections: Section[] = [ + { timestamp: 0, timestamp_end: 10 }, + { timestamp: 15, timestamp_end: 25 }, + ]; + const minGapDuration = 10; + + const gaps = await findGaps(sections, minGapDuration); + + expect(gaps).toEqual([]); + }); + + it("should handle an empty array of sections", async () => { + const sections: Section[] = []; + const minGapDuration = 5; + + const gaps = await findGaps(sections, minGapDuration); + + expect(gaps).toEqual([]); + }); + + it("should handle a single section", async () => { + const sections: Section[] = [{ timestamp: 0, timestamp_end: 10 }]; + const minGapDuration = 5; + + const gaps = await findGaps(sections, minGapDuration); + + expect(gaps).toEqual([]); + }); + + it("should find gaps greater than or equal to the minimum gap duration but ignore gaps less than the minimum gap duration", async () => { + const sections: Section[] = [ + { timestamp: 0, timestamp_end: 10 }, + { timestamp: 15, timestamp_end: 25 }, + { timestamp: 35, timestamp_end: 40 }, + ]; + const minGapDuration = 10; + + const gaps = await findGaps(sections, minGapDuration); + + expect(gaps).toEqual([{ id: "25-35", start: 25, end: 35 }]); + }); + + it("should handle timestamps that are floats", async () => { + const sections: Section[] = [ + { timestamp: 0, timestamp_end: 10.5 }, + { timestamp: 20.5, timestamp_end: 30.5 }, + ]; + const minGapDuration = 5; + + const gaps = await findGaps(sections, minGapDuration); + + expect(gaps).toEqual([{ id: "10.5-20.5", start: 10.5, end: 20.5 }]); + }); +}); diff --git a/src/utils/findGaps.ts b/src/utils/findGaps.ts new file mode 100644 index 0000000..b9345f3 --- /dev/null +++ b/src/utils/findGaps.ts @@ -0,0 +1,37 @@ +import type { Section, VideoClip } from "types"; + +/** + * Finds gaps between video sections that are greater than or equal to the specified minimum gap duration. + * + * @param {Section[]} sections - The list of video sections. + * @param {number} minGapDuration - The minimum duration of a gap to be considered. + * @returns {Promise} - A promise that resolves to an array of video clips representing the gaps. + */ +export default async function findGaps( + sections: Section[], + minGapDuration: number, +): Promise { + const gaps: VideoClip[] = []; + + const validSections = sections.filter( + (section): section is Section & { timestamp_end: number } => + section.timestamp_end !== undefined, + ); + + for (let i = 0; i < validSections.length - 1; i++) { + const currentSection = validSections[i]; + const nextSection = validSections[i + 1]; + + const gapDuration = nextSection.timestamp - currentSection.timestamp_end; + + if (gapDuration >= minGapDuration) { + gaps.push({ + id: `${currentSection.timestamp_end}-${nextSection.timestamp}`, + start: currentSection.timestamp_end, + end: nextSection.timestamp, + }); + } + } + + return gaps; +}