mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ca32e5e39 | ||
|
|
5120e2efa6 | ||
|
|
1aec9c5cf5 | ||
|
|
e88a059222 | ||
|
|
c3f0d640ef | ||
|
|
22ea6b8925 | ||
|
|
dfccf905bb | ||
|
|
3e4584edde | ||
|
|
4cd15fcf00 | ||
|
|
748856f4c4 | ||
|
|
2759d7ed31 | ||
|
|
03c2b16064 | ||
|
|
e15ccefbe5 | ||
|
|
5e1e509c11 | ||
|
|
bfcf20e108 | ||
|
|
4f64ff50cd | ||
|
|
58f23e8f2a | ||
|
|
33f4a54768 | ||
|
|
1d55b903d7 | ||
|
|
af047ba4a7 | ||
|
|
ef3563745a | ||
|
|
d06b7c3b0f | ||
|
|
b7293dc552 | ||
|
|
82d92934cb | ||
|
|
b2a976ba82 | ||
|
|
02d4f9c314 | ||
|
|
1d31b7a586 | ||
|
|
e00ba706fe | ||
|
|
f820d9a894 | ||
|
|
7878c8bb7b | ||
|
|
1f618601c7 | ||
|
|
59c189ca27 | ||
|
|
7bf0984698 |
@@ -0,0 +1,109 @@
|
|||||||
|
# Code Review Checklist
|
||||||
|
|
||||||
|
Review every point below carefully to ensure files follow consistent code style and best practices.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Function Signatures & Parameters
|
||||||
|
|
||||||
|
- [ ] Every function accepts a single object parameter with destructuring in the signature (for readability and future extensibility)
|
||||||
|
- Exception: tiny one-liner callbacks (e.g. `array.find(x => ...)`, `map`, `filter`, `sort`) do not need destructuring if it hurts readability
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ wrong
|
||||||
|
function formatTime(seconds: number, fps: number) { ... }
|
||||||
|
|
||||||
|
// ✅ correct
|
||||||
|
function formatTime({ seconds, fps }: { seconds: number; fps: number }) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
## TypeScript & Type Safety
|
||||||
|
|
||||||
|
- [ ] No `any` references
|
||||||
|
- [ ] General interfaces are in the `types` folder, not scattered in components
|
||||||
|
- Example: `TimelineTrack` interface belongs in `src/types/timeline.ts`, not `src/components/timeline/index.tsx`
|
||||||
|
|
||||||
|
## JSX & Components
|
||||||
|
|
||||||
|
- [ ] JSX is clean — no comments explaining what each part does
|
||||||
|
- [ ] Complex/reusable JSX is extracted into sub-components (placed below the main component)
|
||||||
|
- [ ] Components shared across multiple files are in separate files
|
||||||
|
- [ ] File order: constants specific to file (top) -> utils specific to file -> main component → sub-components (bottom)
|
||||||
|
- [ ] Components render UI only — domain logic lives in hooks, utilities, or managers
|
||||||
|
- Simple interaction logic (gestures, modifier keys) can stay if not complex
|
||||||
|
|
||||||
|
## Code Organization & File Structure
|
||||||
|
|
||||||
|
- [ ] Each file has one single purpose/responsibility
|
||||||
|
- Example: `timeline/index.tsx` should not define `validateElementTrackCompatibility` — that belongs in a lib file
|
||||||
|
- Example: `lib/timeline-utils.ts` should not declare `TRACK_COLORS` — that belongs in `constants/`
|
||||||
|
- [ ] Business logic lives in either `src/lib`, `src/core` or `src/services` folder
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
- [ ] No AI comments — only human comments that explain _why_, not _what_
|
||||||
|
- Bad: changelog-style comments, explaining readable code, using more words than necessary
|
||||||
|
- [ ] All comments are lowercase
|
||||||
|
|
||||||
|
## Naming Conventions
|
||||||
|
|
||||||
|
- [ ] Readability over brevity — use `element` not `el`, `event` not `e`
|
||||||
|
- [ ] Booleans are named `isSomething`, `hasSomething`, or `shouldSomething` — not `something`
|
||||||
|
- [ ] No title case in text/UI — use `Hello world` not `Hello World`
|
||||||
|
|
||||||
|
## Tailwind & Styling
|
||||||
|
|
||||||
|
- [ ] Use `gap-*` instead of `mb-*` or `mt-*` for consistent spacing
|
||||||
|
- [ ] Use `size-*` instead of `h-* w-*` when width and height are the same
|
||||||
|
- [ ] When using `size-*` on icons inside `<Button>`, use `!` modifier to override default `size-4`
|
||||||
|
```tsx
|
||||||
|
<Button>
|
||||||
|
<PlusIcon className="!size-6" /> {/* ✅ correct */}
|
||||||
|
<PlusIcon className="size-6" /> {/* ❌ wrong */}
|
||||||
|
<PlusIcon className="!size-4" /> {/* ❌ unnecessary, size-4 is default */}
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
## State Management (Zustand)
|
||||||
|
|
||||||
|
- [ ] React components never use `someStore.getState()` — use the `useSomeStore` hook instead
|
||||||
|
- [ ] Store/manager methods are not passed as props — sub-components access them directly
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ wrong
|
||||||
|
function Parent() {
|
||||||
|
const { selectedElements } = useTimelineStore();
|
||||||
|
return <Child selectedElements={selectedElements} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ correct
|
||||||
|
function Parent() {
|
||||||
|
return <Child />;
|
||||||
|
}
|
||||||
|
function Child() {
|
||||||
|
const { selectedElements } = useTimelineStore();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Components and hooks should use the `useEditor` hook. Only use `EditorCore.getInstance()` if you are outside of a react component/hook. Eg: in a utility function, event handler.
|
||||||
|
|
||||||
|
## Code Quality
|
||||||
|
|
||||||
|
- [ ] Code is scannable — use variables and helper functions to make intent clear at a glance
|
||||||
|
- [ ] Complex logic is extracted into well-named variables or helpers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Function Keywords
|
||||||
|
|
||||||
|
| Context | Keyword |
|
||||||
|
| --------------------------------- | ------------------------- |
|
||||||
|
| Next.js page components | `export default function` |
|
||||||
|
| Main react component | `export function` |
|
||||||
|
| Sub-components | `function` |
|
||||||
|
| Utility functions | `export function` |
|
||||||
|
| Functions inside react components | `const` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> Every decision, every edit must be carefully considered. Everything matters.
|
||||||
+763
-758
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
<table width="100%">
|
<table width="100%">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" width="120">
|
<td align="left" width="120">
|
||||||
<img src="apps/web/public/logos/opencut/1k/logo.png" alt="OpenCut Logo" width="100" />
|
<img src="apps/web/public/logos/opencut/1k/logo-white-black.png" alt="OpenCut Logo" width="100" />
|
||||||
</td>
|
</td>
|
||||||
<td align="right">
|
<td align="right">
|
||||||
<h1>OpenCut</span></h1>
|
<h1>OpenCut</span></h1>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
## Why?
|
## Why?
|
||||||
|
|
||||||
- **Privacy**: Your videos stay on your device
|
- **Privacy**: Your videos stay on your device
|
||||||
- **Free features**: Every basic feature of CapCut is paywalled now
|
- **Free features**: Most basic CapCut features are now paywalled
|
||||||
- **Simple**: People want editors that are easy to use - CapCut proved that
|
- **Simple**: People want editors that are easy to use - CapCut proved that
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|||||||
@@ -55,9 +55,9 @@
|
|||||||
"pg": "^8.16.2",
|
"pg": "^8.16.2",
|
||||||
"postgres": "^3.4.5",
|
"postgres": "^3.4.5",
|
||||||
"radix-ui": "^1.4.2",
|
"radix-ui": "^1.4.2",
|
||||||
"react": "^18.2.0",
|
"react": "^19.0.0",
|
||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^8.10.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.54.0",
|
"react-hook-form": "^7.54.0",
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.4.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
@@ -85,8 +85,8 @@
|
|||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/node": "^24.2.1",
|
"@types/node": "^24.2.1",
|
||||||
"@types/pg": "^8.15.4",
|
"@types/pg": "^8.15.4",
|
||||||
"@types/react": "^18.2.48",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^19",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"drizzle-kit": "^0.31.4",
|
"drizzle-kit": "^0.31.4",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
@@ -585,7 +585,7 @@ function ProjectItem({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{durationLabel && (
|
{durationLabel && (
|
||||||
<div className="absolute bottom-2 right-2 bg-black/60 text-white text-xs font-semibold px-2 py-1 rounded-lg">
|
<div className="absolute bottom-2 right-2 bg-black/60 text-white text-xs font-semibold px-2 py-1 rounded-sm">
|
||||||
{durationLabel}
|
{durationLabel}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ export function Onboarding() {
|
|||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Title title={getStepTitle()} />
|
<Title title={getStepTitle()} />
|
||||||
<Description description="OpenCut started just one month ago. There's still a ton of things to do to make this editor amazing." />
|
<Description description="There's still a ton of things to do to make this editor amazing." />
|
||||||
<Description description="A lot of features are still missing, like the export functionality. We're working hard to build them out!" />
|
<Description description="A lot of features are still missing. We're working hard to build them out!" />
|
||||||
<Description description="If you're curious, check out our roadmap [here](https://opencut.app/roadmap)" />
|
<Description description="If you're curious, check out our roadmap [here](https://opencut.app/roadmap)" />
|
||||||
</div>
|
</div>
|
||||||
<NextButton onClick={handleNext}>Next</NextButton>
|
<NextButton onClick={handleNext}>Next</NextButton>
|
||||||
@@ -85,9 +85,7 @@ export function Onboarding() {
|
|||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
<span className="sr-only">{getStepTitle()}</span>
|
<span className="sr-only">{getStepTitle()}</span>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogBody>
|
<DialogBody>{renderStepContent()}</DialogBody>
|
||||||
{renderStepContent()}
|
|
||||||
</DialogBody>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import type {
|
|||||||
TranscriptionLanguage,
|
TranscriptionLanguage,
|
||||||
TranscriptionProgress,
|
TranscriptionProgress,
|
||||||
} from "@/types/transcription";
|
} from "@/types/transcription";
|
||||||
import { transcriptionService } from "@/services/transcription";
|
import { transcriptionService } from "@/services/transcription/service";
|
||||||
import { decodeAudioToFloat32 } from "@/lib/media/audio";
|
import { decodeAudioToFloat32 } from "@/lib/media/audio";
|
||||||
import { buildCaptionChunks } from "@/lib/transcription/caption";
|
import { buildCaptionChunks } from "@/lib/transcription/caption";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ interface PanelBaseViewProps {
|
|||||||
content: React.ReactNode;
|
content: React.ReactNode;
|
||||||
}[];
|
}[];
|
||||||
className?: string;
|
className?: string;
|
||||||
ref?: React.RefObject<HTMLDivElement>;
|
ref?: React.Ref<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ViewContent({
|
function ViewContent({
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useMemo } from "react";
|
|||||||
interface SelectionBoxProps {
|
interface SelectionBoxProps {
|
||||||
startPos: { x: number; y: number } | null;
|
startPos: { x: number; y: number } | null;
|
||||||
currentPos: { x: number; y: number } | null;
|
currentPos: { x: number; y: number } | null;
|
||||||
containerRef: React.RefObject<HTMLElement>;
|
containerRef: React.RefObject<HTMLElement | null>;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -423,7 +423,11 @@ export function Timeline() {
|
|||||||
onResizeStateChange={handleResizeStateChange}
|
onResizeStateChange={handleResizeStateChange}
|
||||||
onElementMouseDown={handleElementMouseDown}
|
onElementMouseDown={handleElementMouseDown}
|
||||||
onElementClick={handleElementClick}
|
onElementClick={handleElementClick}
|
||||||
onTrackMouseDown={handleSelectionMouseDown}
|
onTrackMouseDown={(event) => {
|
||||||
|
handleSelectionMouseDown(event);
|
||||||
|
handleTracksMouseDown(event);
|
||||||
|
}}
|
||||||
|
onTrackClick={handleTracksClick}
|
||||||
shouldIgnoreClick={shouldIgnoreClick}
|
shouldIgnoreClick={shouldIgnoreClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ interface SnapIndicatorProps {
|
|||||||
zoomLevel: number;
|
zoomLevel: number;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
tracks: TimelineTrack[];
|
tracks: TimelineTrack[];
|
||||||
timelineRef: React.RefObject<HTMLDivElement>;
|
timelineRef: React.RefObject<HTMLDivElement | null>;
|
||||||
trackLabelsRef?: React.RefObject<HTMLDivElement>;
|
trackLabelsRef?: React.RefObject<HTMLDivElement | null>;
|
||||||
tracksScrollRef: React.RefObject<HTMLDivElement>;
|
tracksScrollRef: React.RefObject<HTMLDivElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SnapIndicator({
|
export function SnapIndicator({
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import { useEditor } from "@/hooks/use-editor";
|
|||||||
|
|
||||||
interface TimelinePlayheadProps {
|
interface TimelinePlayheadProps {
|
||||||
zoomLevel: number;
|
zoomLevel: number;
|
||||||
rulerRef: React.RefObject<HTMLDivElement>;
|
rulerRef: React.RefObject<HTMLDivElement | null>;
|
||||||
rulerScrollRef: React.RefObject<HTMLDivElement>;
|
rulerScrollRef: React.RefObject<HTMLDivElement | null>;
|
||||||
tracksScrollRef: React.RefObject<HTMLDivElement>;
|
tracksScrollRef: React.RefObject<HTMLDivElement | null>;
|
||||||
timelineRef: React.RefObject<HTMLDivElement>;
|
timelineRef: React.RefObject<HTMLDivElement | null>;
|
||||||
playheadRef?: React.RefObject<HTMLDivElement>;
|
playheadRef?: React.RefObject<HTMLDivElement | null>;
|
||||||
isSnappingToPlayhead?: boolean;
|
isSnappingToPlayhead?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { JSX } from "react";
|
||||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||||
import { DEFAULT_FPS } from "@/constants/project-constants";
|
import { DEFAULT_FPS } from "@/constants/project-constants";
|
||||||
import { useEditor } from "@/hooks/use-editor";
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
@@ -7,7 +8,7 @@ import { TimelineTick } from "./timeline-tick";
|
|||||||
interface TimelineRulerProps {
|
interface TimelineRulerProps {
|
||||||
zoomLevel: number;
|
zoomLevel: number;
|
||||||
dynamicTimelineWidth: number;
|
dynamicTimelineWidth: number;
|
||||||
rulerRef: React.RefObject<HTMLDivElement>;
|
rulerRef: React.Ref<HTMLDivElement>;
|
||||||
handleWheel: (e: React.WheelEvent) => void;
|
handleWheel: (e: React.WheelEvent) => void;
|
||||||
handleTimelineContentClick: (e: React.MouseEvent) => void;
|
handleTimelineContentClick: (e: React.MouseEvent) => void;
|
||||||
handleRulerTrackingMouseDown: (e: React.MouseEvent) => void;
|
handleRulerTrackingMouseDown: (e: React.MouseEvent) => void;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { formatTimeCode } from "@/lib/time";
|
import { formatTimeCode } from "@/lib/time";
|
||||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||||
|
import { sliderToZoom, zoomToSlider } from "@/lib/timeline/zoom-utils";
|
||||||
import { EditableTimecode } from "@/components/editable-timecode";
|
import { EditableTimecode } from "@/components/editable-timecode";
|
||||||
import { ScenesView } from "../scenes-view";
|
import { ScenesView } from "../scenes-view";
|
||||||
import { type TAction, invokeAction } from "@/lib/actions";
|
import { type TAction, invokeAction } from "@/lib/actions";
|
||||||
@@ -54,9 +55,9 @@ export function TimelineToolbar({
|
|||||||
direction === "in"
|
direction === "in"
|
||||||
? Math.min(
|
? Math.min(
|
||||||
TIMELINE_CONSTANTS.ZOOM_MAX,
|
TIMELINE_CONSTANTS.ZOOM_MAX,
|
||||||
zoomLevel + TIMELINE_CONSTANTS.ZOOM_STEP,
|
zoomLevel * TIMELINE_CONSTANTS.ZOOM_BUTTON_FACTOR,
|
||||||
)
|
)
|
||||||
: Math.max(minZoom, zoomLevel - TIMELINE_CONSTANTS.ZOOM_STEP);
|
: Math.max(minZoom, zoomLevel / TIMELINE_CONSTANTS.ZOOM_BUTTON_FACTOR);
|
||||||
setZoomLevel({ zoom: newZoomLevel });
|
setZoomLevel({ zoom: newZoomLevel });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -127,17 +128,13 @@ function ToolbarLeftSection() {
|
|||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
icon={<HugeiconsIcon icon={ScissorIcon} />}
|
icon={<HugeiconsIcon icon={ScissorIcon} />}
|
||||||
tooltip="Split element"
|
tooltip="Split element"
|
||||||
onClick={({ event }) =>
|
onClick={({ event }) => handleAction({ action: "split", event })}
|
||||||
handleAction({ action: "split", event })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
icon={<HugeiconsIcon icon={AlignLeftIcon} />}
|
icon={<HugeiconsIcon icon={AlignLeftIcon} />}
|
||||||
tooltip="Split left"
|
tooltip="Split left"
|
||||||
onClick={({ event }) =>
|
onClick={({ event }) => handleAction({ action: "split-left", event })}
|
||||||
handleAction({ action: "split-left", event })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
@@ -302,15 +299,17 @@ function ToolbarRightSection({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => onZoom({ direction: "out" })}
|
onClick={() => onZoom({ direction: "out" })}
|
||||||
>
|
>
|
||||||
<HugeiconsIcon icon={SearchAddIcon} />
|
<HugeiconsIcon icon={SearchMinusIcon} />
|
||||||
</Button>
|
</Button>
|
||||||
<Slider
|
<Slider
|
||||||
className="w-24"
|
className="w-28"
|
||||||
value={[zoomLevel]}
|
value={[zoomToSlider({ zoomLevel, minZoom })]}
|
||||||
onValueChange={(values) => onZoomChange(values[0])}
|
onValueChange={(values) =>
|
||||||
min={minZoom}
|
onZoomChange(sliderToZoom({ sliderPosition: values[0], minZoom }))
|
||||||
max={TIMELINE_CONSTANTS.ZOOM_MAX}
|
}
|
||||||
step={TIMELINE_CONSTANTS.ZOOM_STEP}
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.005}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="text"
|
variant="text"
|
||||||
@@ -318,7 +317,7 @@ function ToolbarRightSection({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => onZoom({ direction: "in" })}
|
onClick={() => onZoom({ direction: "in" })}
|
||||||
>
|
>
|
||||||
<HugeiconsIcon icon={SearchMinusIcon} />
|
<HugeiconsIcon icon={SearchAddIcon} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ interface TimelineTrackContentProps {
|
|||||||
track: TimelineTrack;
|
track: TimelineTrack;
|
||||||
zoomLevel: number;
|
zoomLevel: number;
|
||||||
dragState: ElementDragState;
|
dragState: ElementDragState;
|
||||||
rulerScrollRef: React.RefObject<HTMLDivElement>;
|
rulerScrollRef: React.RefObject<HTMLDivElement | null>;
|
||||||
tracksScrollRef: React.RefObject<HTMLDivElement>;
|
tracksScrollRef: React.RefObject<HTMLDivElement | null>;
|
||||||
lastMouseXRef: React.RefObject<number>;
|
lastMouseXRef: React.RefObject<number>;
|
||||||
onSnapPointChange?: (snapPoint: SnapPoint | null) => void;
|
onSnapPointChange?: (snapPoint: SnapPoint | null) => void;
|
||||||
onResizeStateChange?: (params: { isResizing: boolean }) => void;
|
onResizeStateChange?: (params: { isResizing: boolean }) => void;
|
||||||
@@ -31,6 +31,7 @@ interface TimelineTrackContentProps {
|
|||||||
track: TimelineTrack;
|
track: TimelineTrack;
|
||||||
}) => void;
|
}) => void;
|
||||||
onTrackMouseDown?: (event: React.MouseEvent) => void;
|
onTrackMouseDown?: (event: React.MouseEvent) => void;
|
||||||
|
onTrackClick?: (event: React.MouseEvent) => void;
|
||||||
shouldIgnoreClick?: () => boolean;
|
shouldIgnoreClick?: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ export function TimelineTrackContent({
|
|||||||
onElementMouseDown,
|
onElementMouseDown,
|
||||||
onElementClick,
|
onElementClick,
|
||||||
onTrackMouseDown,
|
onTrackMouseDown,
|
||||||
|
onTrackClick,
|
||||||
shouldIgnoreClick,
|
shouldIgnoreClick,
|
||||||
}: TimelineTrackContentProps) {
|
}: TimelineTrackContentProps) {
|
||||||
const editor = useEditor();
|
const editor = useEditor();
|
||||||
@@ -68,11 +70,13 @@ export function TimelineTrackContent({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={cn("size-full", hasSelectedElements && "bg-panel-accent/35")}
|
className={cn("size-full", hasSelectedElements && "bg-panel-accent/35")}
|
||||||
onClick={() => {
|
onClick={(event) => {
|
||||||
if (shouldIgnoreClick?.()) return;
|
if (shouldIgnoreClick?.()) return;
|
||||||
clearElementSelection();
|
clearElementSelection();
|
||||||
|
onTrackClick?.(event);
|
||||||
}}
|
}}
|
||||||
onMouseDown={(event) => {
|
onMouseDown={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
onTrackMouseDown?.(event);
|
onTrackMouseDown?.(event);
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useEditor } from "@/hooks/use-editor";
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
import { storageService } from "@/services/storage/storage-service";
|
import { storageService } from "@/services/storage/service";
|
||||||
|
|
||||||
interface StorageContextType {
|
interface StorageContextType {
|
||||||
isInitialized: boolean;
|
isInitialized: boolean;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ interface ColorPickerProps {
|
|||||||
value?: string;
|
value?: string;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
containerRef?: React.RefObject<HTMLDivElement>;
|
containerRef?: React.RefObject<HTMLDivElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hexToHsv = (hex: string) => {
|
const hexToHsv = (hex: string) => {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ PhoneInput.displayName = "PhoneInput";
|
|||||||
const InputComponent = React.forwardRef<
|
const InputComponent = React.forwardRef<
|
||||||
HTMLInputElement,
|
HTMLInputElement,
|
||||||
React.ComponentProps<"input">
|
React.ComponentProps<"input">
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, size: _size, ...props }, ref) => (
|
||||||
<Input
|
<Input
|
||||||
className={cn("rounded-s-none rounded-e-lg", className)}
|
className={cn("rounded-s-none rounded-e-lg", className)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ function SidebarTrigger({
|
|||||||
<Button
|
<Button
|
||||||
data-sidebar="trigger"
|
data-sidebar="trigger"
|
||||||
data-slot="sidebar-trigger"
|
data-slot="sidebar-trigger"
|
||||||
variant="ghost"
|
variant="text"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={cn("size-7", className)}
|
className={cn("size-7", className)}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const PANEL_CONFIG = {
|
|||||||
tools: 25,
|
tools: 25,
|
||||||
preview: 50,
|
preview: 50,
|
||||||
properties: 25,
|
properties: 25,
|
||||||
mainContent: 70,
|
mainContent: 50,
|
||||||
timeline: 30,
|
timeline: 50,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,12 +35,11 @@ export const TRACK_GAP = 4;
|
|||||||
export const TIMELINE_CONSTANTS = {
|
export const TIMELINE_CONSTANTS = {
|
||||||
PIXELS_PER_SECOND: 50,
|
PIXELS_PER_SECOND: 50,
|
||||||
DEFAULT_ELEMENT_DURATION: 5,
|
DEFAULT_ELEMENT_DURATION: 5,
|
||||||
PLAYHEAD_LOOKAHEAD_SECONDS: 30, // padding ahead
|
|
||||||
PADDING_TOP_PX: 0,
|
PADDING_TOP_PX: 0,
|
||||||
ZOOM_LEVELS: [0.1, 0.25, 0.5, 1, 1.5, 2, 3, 4, 6, 8, 10, 15, 20, 30, 50],
|
|
||||||
ZOOM_MIN: 0.1,
|
ZOOM_MIN: 0.1,
|
||||||
ZOOM_MAX: 100,
|
ZOOM_MAX: 100,
|
||||||
ZOOM_STEP: 0.1,
|
ZOOM_BUTTON_FACTOR: 1.7,
|
||||||
|
ZOOM_ANCHOR_PLAYHEAD_THRESHOLD: 0.15,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const DEFAULT_TIMELINE_VIEW_STATE: TTimelineViewState = {
|
export const DEFAULT_TIMELINE_VIEW_STATE: TTimelineViewState = {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { EditorCore } from "@/core";
|
import type { EditorCore } from "@/core";
|
||||||
import type { MediaAsset } from "@/types/assets";
|
import type { MediaAsset } from "@/types/assets";
|
||||||
import { storageService } from "@/services/storage/storage-service";
|
import { storageService } from "@/services/storage/service";
|
||||||
import { generateUUID } from "@/utils/id";
|
import { generateUUID } from "@/utils/id";
|
||||||
import { videoCache } from "@/services/media/video-cache";
|
import { videoCache } from "@/services/video-cache/service";
|
||||||
import { hasMediaId } from "@/lib/timeline/element-utils";
|
import { hasMediaId } from "@/lib/timeline/element-utils";
|
||||||
|
|
||||||
export class MediaManager {
|
export class MediaManager {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type {
|
|||||||
TTimelineViewState,
|
TTimelineViewState,
|
||||||
} from "@/types/project";
|
} from "@/types/project";
|
||||||
import type { ExportOptions, ExportResult } from "@/types/export";
|
import type { ExportOptions, ExportResult } from "@/types/export";
|
||||||
import { storageService } from "@/services/storage/storage-service";
|
import { storageService } from "@/services/storage/service";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { generateUUID } from "@/utils/id";
|
import { generateUUID } from "@/utils/id";
|
||||||
import { UpdateProjectSettingsCommand } from "@/lib/commands/project";
|
import { UpdateProjectSettingsCommand } from "@/lib/commands/project";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { EditorCore } from "@/core";
|
import type { EditorCore } from "@/core";
|
||||||
import type { TimelineTrack, TScene } from "@/types/timeline";
|
import type { TimelineTrack, TScene } from "@/types/timeline";
|
||||||
import { storageService } from "@/services/storage/storage-service";
|
import { storageService } from "@/services/storage/service";
|
||||||
import {
|
import {
|
||||||
getMainScene,
|
getMainScene,
|
||||||
ensureMainScene,
|
ensureMainScene,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { useEffect, useRef } from "react";
|
|||||||
interface UseEdgeAutoScrollParams {
|
interface UseEdgeAutoScrollParams {
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
getMouseClientX: () => number;
|
getMouseClientX: () => number;
|
||||||
rulerScrollRef: React.RefObject<HTMLDivElement>;
|
rulerScrollRef: React.RefObject<HTMLDivElement | null>;
|
||||||
tracksScrollRef: React.RefObject<HTMLDivElement>;
|
tracksScrollRef: React.RefObject<HTMLDivElement | null>;
|
||||||
contentWidth: number;
|
contentWidth: number;
|
||||||
edgeThreshold?: number;
|
edgeThreshold?: number;
|
||||||
maxScrollSpeed?: number;
|
maxScrollSpeed?: number;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
interface UseScrollSyncProps {
|
interface UseScrollSyncProps {
|
||||||
rulerScrollRef?: React.RefObject<HTMLDivElement>;
|
rulerScrollRef?: React.RefObject<HTMLDivElement | null>;
|
||||||
tracksScrollRef: React.RefObject<HTMLDivElement>;
|
tracksScrollRef: React.RefObject<HTMLDivElement | null>;
|
||||||
trackLabelsScrollRef?: React.RefObject<HTMLDivElement>;
|
trackLabelsScrollRef?: React.RefObject<HTMLDivElement | null>;
|
||||||
bookmarksScrollRef?: React.RefObject<HTMLDivElement>;
|
bookmarksScrollRef?: React.RefObject<HTMLDivElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useScrollSync({
|
export function useScrollSync({
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import { getCumulativeHeightBefore, getTrackHeight } from "@/lib/timeline";
|
|||||||
import { useEditor } from "../use-editor";
|
import { useEditor } from "../use-editor";
|
||||||
|
|
||||||
interface UseSelectionBoxProps {
|
interface UseSelectionBoxProps {
|
||||||
containerRef: React.RefObject<HTMLElement>;
|
containerRef: React.RefObject<HTMLElement | null>;
|
||||||
onSelectionComplete: (
|
onSelectionComplete: (
|
||||||
elements: { trackId: string; elementId: string }[],
|
elements: { trackId: string; elementId: string }[],
|
||||||
) => void;
|
) => void;
|
||||||
isEnabled?: boolean;
|
isEnabled?: boolean;
|
||||||
tracksScrollRef: React.RefObject<HTMLDivElement>;
|
tracksScrollRef: React.RefObject<HTMLDivElement | null>;
|
||||||
zoomLevel: number;
|
zoomLevel: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ interface UseSnapIndicatorPositionParams {
|
|||||||
snapPoint: { time: number } | null;
|
snapPoint: { time: number } | null;
|
||||||
zoomLevel: number;
|
zoomLevel: number;
|
||||||
tracks: TimelineTrack[];
|
tracks: TimelineTrack[];
|
||||||
timelineRef: React.RefObject<HTMLDivElement>;
|
timelineRef: React.RefObject<HTMLDivElement | null>;
|
||||||
trackLabelsRef?: React.RefObject<HTMLDivElement>;
|
trackLabelsRef?: React.RefObject<HTMLDivElement | null>;
|
||||||
tracksScrollRef: React.RefObject<HTMLDivElement>;
|
tracksScrollRef: React.RefObject<HTMLDivElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SnapIndicatorPosition {
|
interface SnapIndicatorPosition {
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
|||||||
|
|
||||||
interface UseTimelinePlayheadProps {
|
interface UseTimelinePlayheadProps {
|
||||||
zoomLevel: number;
|
zoomLevel: number;
|
||||||
rulerRef: React.RefObject<HTMLDivElement>;
|
rulerRef: React.RefObject<HTMLDivElement | null>;
|
||||||
rulerScrollRef: React.RefObject<HTMLDivElement>;
|
rulerScrollRef: React.RefObject<HTMLDivElement | null>;
|
||||||
tracksScrollRef: React.RefObject<HTMLDivElement>;
|
tracksScrollRef: React.RefObject<HTMLDivElement | null>;
|
||||||
playheadRef?: React.RefObject<HTMLDivElement>;
|
playheadRef?: React.RefObject<HTMLDivElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTimelinePlayhead({
|
export function useTimelinePlayhead({
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import { getSnappedSeekTime } from "@/lib/time";
|
|||||||
import { useEditor } from "../use-editor";
|
import { useEditor } from "../use-editor";
|
||||||
|
|
||||||
interface UseTimelineSeekProps {
|
interface UseTimelineSeekProps {
|
||||||
playheadRef: RefObject<HTMLDivElement>;
|
playheadRef: RefObject<HTMLDivElement | null>;
|
||||||
trackLabelsRef: RefObject<HTMLDivElement>;
|
trackLabelsRef: RefObject<HTMLDivElement | null>;
|
||||||
rulerScrollRef: RefObject<HTMLDivElement>;
|
rulerScrollRef: RefObject<HTMLDivElement | null>;
|
||||||
tracksScrollRef: RefObject<HTMLDivElement>;
|
tracksScrollRef: RefObject<HTMLDivElement | null>;
|
||||||
zoomLevel: number;
|
zoomLevel: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
isSelecting: boolean;
|
isSelecting: boolean;
|
||||||
|
|||||||
@@ -9,15 +9,16 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||||
import { useEditor } from "@/hooks/use-editor";
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
|
import { zoomToSlider } from "@/lib/timeline/zoom-utils";
|
||||||
|
|
||||||
interface UseTimelineZoomProps {
|
interface UseTimelineZoomProps {
|
||||||
containerRef: RefObject<HTMLDivElement>;
|
containerRef: RefObject<HTMLDivElement | null>;
|
||||||
minZoom?: number;
|
minZoom?: number;
|
||||||
initialZoom?: number;
|
initialZoom?: number;
|
||||||
initialScrollLeft?: number;
|
initialScrollLeft?: number;
|
||||||
initialPlayheadTime?: number;
|
initialPlayheadTime?: number;
|
||||||
tracksScrollRef: RefObject<HTMLDivElement>;
|
tracksScrollRef: RefObject<HTMLDivElement | null>;
|
||||||
rulerScrollRef: RefObject<HTMLDivElement>;
|
rulerScrollRef: RefObject<HTMLDivElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseTimelineZoomReturn {
|
interface UseTimelineZoomReturn {
|
||||||
@@ -122,18 +123,47 @@ export function useTimelineZoom({
|
|||||||
if (previousZoom === zoomLevel) return;
|
if (previousZoom === zoomLevel) return;
|
||||||
|
|
||||||
const scrollElement = tracksScrollRef.current;
|
const scrollElement = tracksScrollRef.current;
|
||||||
if (scrollElement) {
|
if (!scrollElement) {
|
||||||
editor.project.setTimelineViewState({
|
previousZoomRef.current = zoomLevel;
|
||||||
viewState: {
|
return;
|
||||||
zoomLevel,
|
}
|
||||||
scrollLeft: scrollElement.scrollLeft,
|
|
||||||
playheadTime: editor.playback.getCurrentTime(),
|
const currentScrollLeft = scrollElement.scrollLeft;
|
||||||
},
|
const playheadTime = editor.playback.getCurrentTime();
|
||||||
});
|
const sliderPercent = zoomToSlider({ zoomLevel, minZoom });
|
||||||
|
|
||||||
|
if (sliderPercent >= TIMELINE_CONSTANTS.ZOOM_ANCHOR_PLAYHEAD_THRESHOLD) {
|
||||||
|
const playheadPixelsBefore =
|
||||||
|
playheadTime * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * previousZoom;
|
||||||
|
const playheadPixelsAfter =
|
||||||
|
playheadTime * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
|
||||||
|
|
||||||
|
const viewportOffset = playheadPixelsBefore - currentScrollLeft;
|
||||||
|
const newScrollLeft = playheadPixelsAfter - viewportOffset;
|
||||||
|
|
||||||
|
const maxScrollLeft =
|
||||||
|
scrollElement.scrollWidth - scrollElement.clientWidth;
|
||||||
|
const clampedScrollLeft = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(maxScrollLeft, newScrollLeft),
|
||||||
|
);
|
||||||
|
|
||||||
|
scrollElement.scrollLeft = clampedScrollLeft;
|
||||||
|
if (rulerScrollRef.current) {
|
||||||
|
rulerScrollRef.current.scrollLeft = clampedScrollLeft;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
previousZoomRef.current = zoomLevel;
|
previousZoomRef.current = zoomLevel;
|
||||||
}, [zoomLevel, editor, tracksScrollRef]);
|
|
||||||
|
editor.project.setTimelineViewState({
|
||||||
|
viewState: {
|
||||||
|
zoomLevel,
|
||||||
|
scrollLeft: scrollElement.scrollLeft,
|
||||||
|
playheadTime,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [zoomLevel, editor, tracksScrollRef, rulerScrollRef, minZoom]);
|
||||||
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: tracksScrollRef is a stable ref
|
// biome-ignore lint/correctness/useExhaustiveDependencies: tracksScrollRef is a stable ref
|
||||||
const saveScrollPosition = useCallback(() => {
|
const saveScrollPosition = useCallback(() => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Command } from "@/lib/commands/base-command";
|
|||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import type { MediaAsset } from "@/types/assets";
|
import type { MediaAsset } from "@/types/assets";
|
||||||
import { generateUUID } from "@/utils/id";
|
import { generateUUID } from "@/utils/id";
|
||||||
import { storageService } from "@/services/storage/storage-service";
|
import { storageService } from "@/services/storage/service";
|
||||||
|
|
||||||
export class AddMediaAssetCommand extends Command {
|
export class AddMediaAssetCommand extends Command {
|
||||||
private assetId: string;
|
private assetId: string;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Command } from "@/lib/commands/base-command";
|
import { Command } from "@/lib/commands/base-command";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import type { MediaAsset } from "@/types/assets";
|
import type { MediaAsset } from "@/types/assets";
|
||||||
import { storageService } from "@/services/storage/storage-service";
|
import { storageService } from "@/services/storage/service";
|
||||||
import { videoCache } from "@/services/media/video-cache";
|
import { videoCache } from "@/services/video-cache/service";
|
||||||
import { hasMediaId } from "@/lib/timeline/element-utils";
|
import { hasMediaId } from "@/lib/timeline/element-utils";
|
||||||
import type { TimelineTrack } from "@/types/timeline";
|
import type { TimelineTrack } from "@/types/timeline";
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export function getProjectDurationFromScenes({
|
|||||||
scenes: TScene[];
|
scenes: TScene[];
|
||||||
}): number {
|
}): number {
|
||||||
const mainScene = getMainScene({ scenes }) ?? scenes[0] ?? null;
|
const mainScene = getMainScene({ scenes }) ?? scenes[0] ?? null;
|
||||||
if (!mainScene) {
|
if (!mainScene?.tracks || !Array.isArray(mainScene.tracks)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,3 +51,36 @@ export function getZoomPercent({
|
|||||||
}): number {
|
}): number {
|
||||||
return (zoomLevel - minZoom) / (TIMELINE_CONSTANTS.ZOOM_MAX - minZoom);
|
return (zoomLevel - minZoom) / (TIMELINE_CONSTANTS.ZOOM_MAX - minZoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert linear slider position (0-1) to exponential zoom level.
|
||||||
|
* at low slider values, zoom changes are small. at high values, changes are large.
|
||||||
|
*/
|
||||||
|
export function sliderToZoom({
|
||||||
|
sliderPosition,
|
||||||
|
minZoom,
|
||||||
|
maxZoom = TIMELINE_CONSTANTS.ZOOM_MAX,
|
||||||
|
}: {
|
||||||
|
sliderPosition: number;
|
||||||
|
minZoom: number;
|
||||||
|
maxZoom?: number;
|
||||||
|
}): number {
|
||||||
|
const clampedPosition = Math.max(0, Math.min(1, sliderPosition));
|
||||||
|
return minZoom * (maxZoom / minZoom) ** clampedPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert exponential zoom level to linear slider position (0-1).
|
||||||
|
*/
|
||||||
|
export function zoomToSlider({
|
||||||
|
zoomLevel,
|
||||||
|
minZoom,
|
||||||
|
maxZoom = TIMELINE_CONSTANTS.ZOOM_MAX,
|
||||||
|
}: {
|
||||||
|
zoomLevel: number;
|
||||||
|
minZoom: number;
|
||||||
|
maxZoom?: number;
|
||||||
|
}): number {
|
||||||
|
const clampedZoom = Math.max(minZoom, Math.min(maxZoom, zoomLevel));
|
||||||
|
return Math.log(clampedZoom / minZoom) / Math.log(maxZoom / minZoom);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { CanvasRenderer } from "../canvas-renderer";
|
import type { CanvasRenderer } from "../canvas-renderer";
|
||||||
import { BaseNode } from "./base-node";
|
import { BaseNode } from "./base-node";
|
||||||
import { videoCache } from "@/services/media/video-cache";
|
import { videoCache } from "@/services/video-cache/service";
|
||||||
|
|
||||||
const VIDEO_EPSILON = 1 / 1000;
|
const VIDEO_EPSILON = 1 / 1000;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Test fixtures for storage migrations.
|
||||||
|
* These represent real project data shapes from each version.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./v0";
|
||||||
|
export * from "./v1";
|
||||||
|
export * from "./v2";
|
||||||
|
export * from "./v3";
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
export const v0Project = {
|
||||||
|
id: "project-v0-123",
|
||||||
|
name: "My V0 Project",
|
||||||
|
createdAt: "2024-01-15T10:00:00.000Z",
|
||||||
|
updatedAt: "2024-01-15T12:00:00.000Z",
|
||||||
|
fps: 30,
|
||||||
|
canvasSize: { width: 1920, height: 1080 },
|
||||||
|
backgroundColor: "#000000",
|
||||||
|
backgroundType: "color",
|
||||||
|
tracks: [
|
||||||
|
{
|
||||||
|
id: "track-1",
|
||||||
|
type: "video",
|
||||||
|
name: "Video Track",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
id: "element-1",
|
||||||
|
type: "video",
|
||||||
|
mediaId: "media-1",
|
||||||
|
startTime: 0,
|
||||||
|
duration: 5,
|
||||||
|
trimStart: 0,
|
||||||
|
trimEnd: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bookmarks: [1.5, 3.0],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const v0ProjectWithMetadata = {
|
||||||
|
id: "project-v0-456",
|
||||||
|
metadata: {
|
||||||
|
id: "project-v0-456",
|
||||||
|
name: "V0 With Metadata",
|
||||||
|
createdAt: "2024-02-01T08:00:00.000Z",
|
||||||
|
updatedAt: "2024-02-01T09:00:00.000Z",
|
||||||
|
},
|
||||||
|
fps: 24,
|
||||||
|
canvasSize: { width: 1280, height: 720 },
|
||||||
|
backgroundType: "blur",
|
||||||
|
blurIntensity: 20,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const v0ProjectEmpty = {
|
||||||
|
id: "project-empty",
|
||||||
|
name: "Empty Project",
|
||||||
|
createdAt: "2024-03-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2024-03-01T00:00:00.000Z",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Edge cases
|
||||||
|
export const projectWithNoId = {
|
||||||
|
name: "No ID Project",
|
||||||
|
version: 1,
|
||||||
|
scenes: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const projectWithNullValues = {
|
||||||
|
id: "project-nulls",
|
||||||
|
version: 1,
|
||||||
|
name: null,
|
||||||
|
metadata: null,
|
||||||
|
scenes: null,
|
||||||
|
settings: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const projectMalformed = {
|
||||||
|
id: "project-malformed",
|
||||||
|
// Missing almost everything
|
||||||
|
};
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
export const v1Project = {
|
||||||
|
id: "project-v1-123",
|
||||||
|
version: 1,
|
||||||
|
name: "My V1 Project",
|
||||||
|
createdAt: "2024-01-15T10:00:00.000Z",
|
||||||
|
updatedAt: "2024-01-15T12:00:00.000Z",
|
||||||
|
fps: 30,
|
||||||
|
canvasSize: { width: 1920, height: 1080 },
|
||||||
|
backgroundColor: "#1a1a1a",
|
||||||
|
backgroundType: "color",
|
||||||
|
currentSceneId: "scene-main",
|
||||||
|
bookmarks: [2.0, 4.5, 7.0],
|
||||||
|
scenes: [
|
||||||
|
{
|
||||||
|
id: "scene-main",
|
||||||
|
name: "Main scene",
|
||||||
|
isMain: true,
|
||||||
|
tracks: [
|
||||||
|
{
|
||||||
|
id: "track-1",
|
||||||
|
type: "video",
|
||||||
|
name: "Video Track",
|
||||||
|
isMain: true,
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
id: "element-1",
|
||||||
|
type: "video",
|
||||||
|
mediaId: "media-1",
|
||||||
|
startTime: 0,
|
||||||
|
duration: 10,
|
||||||
|
trimStart: 0,
|
||||||
|
trimEnd: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "track-2",
|
||||||
|
type: "audio",
|
||||||
|
name: "Audio Track",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
id: "element-2",
|
||||||
|
type: "audio",
|
||||||
|
sourceType: "upload",
|
||||||
|
mediaId: "media-2",
|
||||||
|
startTime: 0,
|
||||||
|
duration: 10,
|
||||||
|
trimStart: 0,
|
||||||
|
trimEnd: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bookmarks: [],
|
||||||
|
createdAt: "2024-01-15T10:00:00.000Z",
|
||||||
|
updatedAt: "2024-01-15T12:00:00.000Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const v1ProjectWithMultipleScenes = {
|
||||||
|
id: "project-v1-multi",
|
||||||
|
version: 1,
|
||||||
|
metadata: {
|
||||||
|
id: "project-v1-multi",
|
||||||
|
name: "Multi-Scene Project",
|
||||||
|
createdAt: "2024-02-20T14:00:00.000Z",
|
||||||
|
updatedAt: "2024-02-20T16:00:00.000Z",
|
||||||
|
},
|
||||||
|
currentSceneId: "scene-1",
|
||||||
|
fps: 60,
|
||||||
|
canvasSize: { width: 3840, height: 2160 },
|
||||||
|
background: { type: "blur", blurIntensity: 15 },
|
||||||
|
scenes: [
|
||||||
|
{
|
||||||
|
id: "scene-1",
|
||||||
|
name: "Intro",
|
||||||
|
isMain: true,
|
||||||
|
tracks: [
|
||||||
|
{
|
||||||
|
id: "track-1",
|
||||||
|
type: "video",
|
||||||
|
isMain: true,
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
id: "el-1",
|
||||||
|
type: "video",
|
||||||
|
mediaId: "m1",
|
||||||
|
startTime: 0,
|
||||||
|
duration: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bookmarks: [1.0],
|
||||||
|
createdAt: "2024-02-20T14:00:00.000Z",
|
||||||
|
updatedAt: "2024-02-20T16:00:00.000Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "scene-2",
|
||||||
|
name: "Content",
|
||||||
|
isMain: false,
|
||||||
|
tracks: [
|
||||||
|
{
|
||||||
|
id: "track-2",
|
||||||
|
type: "video",
|
||||||
|
isMain: true,
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
id: "el-2",
|
||||||
|
type: "video",
|
||||||
|
mediaId: "m2",
|
||||||
|
startTime: 0,
|
||||||
|
duration: 20,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bookmarks: [],
|
||||||
|
createdAt: "2024-02-20T14:30:00.000Z",
|
||||||
|
updatedAt: "2024-02-20T16:00:00.000Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
export const v2Project = {
|
||||||
|
id: "project-v2-123",
|
||||||
|
version: 2,
|
||||||
|
metadata: {
|
||||||
|
id: "project-v2-123",
|
||||||
|
name: "My V2 Project",
|
||||||
|
thumbnail: "data:image/png;base64,abc123",
|
||||||
|
createdAt: "2024-03-01T10:00:00.000Z",
|
||||||
|
updatedAt: "2024-03-01T14:00:00.000Z",
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
fps: 30,
|
||||||
|
canvasSize: { width: 1920, height: 1080 },
|
||||||
|
background: { type: "color", color: "#000000" },
|
||||||
|
},
|
||||||
|
currentSceneId: "scene-main",
|
||||||
|
scenes: [
|
||||||
|
{
|
||||||
|
id: "scene-main",
|
||||||
|
name: "Main scene",
|
||||||
|
isMain: true,
|
||||||
|
tracks: [
|
||||||
|
{
|
||||||
|
id: "track-1",
|
||||||
|
type: "video",
|
||||||
|
name: "Video Track",
|
||||||
|
isMain: true,
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
id: "element-1",
|
||||||
|
type: "video",
|
||||||
|
mediaId: "media-1",
|
||||||
|
startTime: 0,
|
||||||
|
duration: 15.5,
|
||||||
|
trimStart: 0,
|
||||||
|
trimEnd: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "track-2",
|
||||||
|
type: "text",
|
||||||
|
name: "Text Track",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
id: "element-2",
|
||||||
|
type: "text",
|
||||||
|
content: "Hello World",
|
||||||
|
startTime: 2,
|
||||||
|
duration: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bookmarks: [5.0, 10.0],
|
||||||
|
createdAt: "2024-03-01T10:00:00.000Z",
|
||||||
|
updatedAt: "2024-03-01T14:00:00.000Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const v2ProjectWithBlurBackground = {
|
||||||
|
id: "project-v2-blur",
|
||||||
|
version: 2,
|
||||||
|
metadata: {
|
||||||
|
id: "project-v2-blur",
|
||||||
|
name: "Blur Background Project",
|
||||||
|
createdAt: "2024-03-15T08:00:00.000Z",
|
||||||
|
updatedAt: "2024-03-15T10:00:00.000Z",
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
fps: 24,
|
||||||
|
canvasSize: { width: 1080, height: 1920 },
|
||||||
|
background: { type: "blur", blurIntensity: 25 },
|
||||||
|
},
|
||||||
|
currentSceneId: "scene-1",
|
||||||
|
scenes: [
|
||||||
|
{
|
||||||
|
id: "scene-1",
|
||||||
|
name: "Main scene",
|
||||||
|
isMain: true,
|
||||||
|
tracks: [
|
||||||
|
{
|
||||||
|
id: "track-1",
|
||||||
|
type: "video",
|
||||||
|
isMain: true,
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
id: "el-1",
|
||||||
|
type: "video",
|
||||||
|
mediaId: "m1",
|
||||||
|
startTime: 0,
|
||||||
|
duration: 30,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bookmarks: [],
|
||||||
|
createdAt: "2024-03-15T08:00:00.000Z",
|
||||||
|
updatedAt: "2024-03-15T10:00:00.000Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const v2ProjectEmptyScenes = {
|
||||||
|
id: "project-v2-empty",
|
||||||
|
version: 2,
|
||||||
|
metadata: {
|
||||||
|
id: "project-v2-empty",
|
||||||
|
name: "Empty Scenes Project",
|
||||||
|
createdAt: "2024-04-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2024-04-01T00:00:00.000Z",
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
fps: 30,
|
||||||
|
canvasSize: { width: 1920, height: 1080 },
|
||||||
|
background: { type: "color", color: "#ffffff" },
|
||||||
|
},
|
||||||
|
currentSceneId: "",
|
||||||
|
scenes: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const v2ProjectSceneWithoutTracks = {
|
||||||
|
id: "project-v2-no-tracks",
|
||||||
|
version: 2,
|
||||||
|
metadata: {
|
||||||
|
id: "project-v2-no-tracks",
|
||||||
|
name: "Scene Without Tracks",
|
||||||
|
createdAt: "2024-04-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2024-04-01T00:00:00.000Z",
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
fps: 30,
|
||||||
|
canvasSize: { width: 1920, height: 1080 },
|
||||||
|
background: { type: "color", color: "#000000" },
|
||||||
|
},
|
||||||
|
currentSceneId: "scene-1",
|
||||||
|
scenes: [
|
||||||
|
{
|
||||||
|
id: "scene-1",
|
||||||
|
name: "Main Scene",
|
||||||
|
isMain: true,
|
||||||
|
createdAt: "2024-04-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2024-04-01T00:00:00.000Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
export const v3Project = {
|
||||||
|
id: "project-v3-123",
|
||||||
|
version: 3,
|
||||||
|
metadata: {
|
||||||
|
id: "project-v3-123",
|
||||||
|
name: "My V3 Project",
|
||||||
|
thumbnail: "data:image/png;base64,xyz789",
|
||||||
|
duration: 25.5,
|
||||||
|
createdAt: "2024-05-01T10:00:00.000Z",
|
||||||
|
updatedAt: "2024-05-01T14:00:00.000Z",
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
fps: 30,
|
||||||
|
canvasSize: { width: 1920, height: 1080 },
|
||||||
|
background: { type: "color", color: "#000000" },
|
||||||
|
},
|
||||||
|
currentSceneId: "scene-main",
|
||||||
|
scenes: [
|
||||||
|
{
|
||||||
|
id: "scene-main",
|
||||||
|
name: "Main scene",
|
||||||
|
isMain: true,
|
||||||
|
tracks: [
|
||||||
|
{
|
||||||
|
id: "track-1",
|
||||||
|
type: "video",
|
||||||
|
name: "Video Track",
|
||||||
|
isMain: true,
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
id: "element-1",
|
||||||
|
type: "video",
|
||||||
|
mediaId: "media-1",
|
||||||
|
startTime: 0,
|
||||||
|
duration: 25.5,
|
||||||
|
trimStart: 0,
|
||||||
|
trimEnd: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bookmarks: [],
|
||||||
|
createdAt: "2024-05-01T10:00:00.000Z",
|
||||||
|
updatedAt: "2024-05-01T14:00:00.000Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { getProjectId, transformProjectV0ToV1 } from "../transformers/v0-to-v1";
|
||||||
|
import {
|
||||||
|
projectMalformed,
|
||||||
|
projectWithNoId,
|
||||||
|
v0Project,
|
||||||
|
v0ProjectEmpty,
|
||||||
|
v0ProjectWithMetadata,
|
||||||
|
v1Project,
|
||||||
|
} from "./fixtures";
|
||||||
|
|
||||||
|
describe("V0 to V1 Migration", () => {
|
||||||
|
const fixedDate = new Date("2024-06-01T12:00:00.000Z");
|
||||||
|
|
||||||
|
describe("transformProjectV0ToV1", () => {
|
||||||
|
test("adds scenes array to v0 project", () => {
|
||||||
|
const result = transformProjectV0ToV1({
|
||||||
|
project: v0Project,
|
||||||
|
options: { now: fixedDate },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
expect(result.project.version).toBe(1);
|
||||||
|
expect(Array.isArray(result.project.scenes)).toBe(true);
|
||||||
|
expect((result.project.scenes as unknown[]).length).toBe(1);
|
||||||
|
expect(result.project.currentSceneId).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("creates main scene with correct structure", () => {
|
||||||
|
const result = transformProjectV0ToV1({
|
||||||
|
project: v0Project,
|
||||||
|
options: { now: fixedDate },
|
||||||
|
});
|
||||||
|
|
||||||
|
const scenes = result.project.scenes as Array<Record<string, unknown>>;
|
||||||
|
const mainScene = scenes[0];
|
||||||
|
|
||||||
|
expect(mainScene.isMain).toBe(true);
|
||||||
|
expect(mainScene.name).toBe("Main scene");
|
||||||
|
expect(typeof mainScene.id).toBe("string");
|
||||||
|
expect(Array.isArray(mainScene.tracks)).toBe(true);
|
||||||
|
expect(Array.isArray(mainScene.bookmarks)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("updates metadata.updatedAt when metadata exists", () => {
|
||||||
|
const result = transformProjectV0ToV1({
|
||||||
|
project: v0ProjectWithMetadata,
|
||||||
|
options: { now: fixedDate },
|
||||||
|
});
|
||||||
|
|
||||||
|
const metadata = result.project.metadata as Record<string, unknown>;
|
||||||
|
expect(metadata.updatedAt).toBe(fixedDate.toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("updates root updatedAt when no metadata", () => {
|
||||||
|
const result = transformProjectV0ToV1({
|
||||||
|
project: v0ProjectEmpty,
|
||||||
|
options: { now: fixedDate },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.project.updatedAt).toBe(fixedDate.toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("skips project that already has scenes", () => {
|
||||||
|
const result = transformProjectV0ToV1({
|
||||||
|
project: v1Project,
|
||||||
|
options: { now: fixedDate },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.skipped).toBe(true);
|
||||||
|
expect(result.reason).toBe("already has scenes");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preserves original project properties", () => {
|
||||||
|
const result = transformProjectV0ToV1({
|
||||||
|
project: v0Project,
|
||||||
|
options: { now: fixedDate },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.project.id).toBe(v0Project.id);
|
||||||
|
expect(result.project.name).toBe(v0Project.name);
|
||||||
|
expect(result.project.fps).toBe(v0Project.fps);
|
||||||
|
expect(result.project.canvasSize).toEqual(v0Project.canvasSize);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getProjectId", () => {
|
||||||
|
test("returns id from root level", () => {
|
||||||
|
const id = getProjectId({ project: v0Project });
|
||||||
|
expect(id).toBe("project-v0-123");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns id from metadata when root id missing", () => {
|
||||||
|
const project = {
|
||||||
|
metadata: { id: "from-metadata" },
|
||||||
|
};
|
||||||
|
const id = getProjectId({ project });
|
||||||
|
expect(id).toBe("from-metadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns null when no id found", () => {
|
||||||
|
const id = getProjectId({ project: projectWithNoId });
|
||||||
|
expect(id).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns null for malformed project", () => {
|
||||||
|
const id = getProjectId({ project: projectMalformed });
|
||||||
|
expect(id).toBe("project-malformed");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import {
|
||||||
|
DEFAULT_BLUR_INTENSITY,
|
||||||
|
DEFAULT_CANVAS_SIZE,
|
||||||
|
DEFAULT_COLOR,
|
||||||
|
DEFAULT_FPS,
|
||||||
|
} from "@/constants/project-constants";
|
||||||
|
import { getProjectId, transformProjectV1ToV2 } from "../transformers/v1-to-v2";
|
||||||
|
import {
|
||||||
|
projectWithNoId,
|
||||||
|
projectWithNullValues,
|
||||||
|
v1Project,
|
||||||
|
v1ProjectWithMultipleScenes,
|
||||||
|
v2Project,
|
||||||
|
} from "./fixtures";
|
||||||
|
|
||||||
|
describe("V1 to V2 Migration", () => {
|
||||||
|
describe("transformProjectV1ToV2", () => {
|
||||||
|
test("creates metadata object from flat properties", () => {
|
||||||
|
const result = transformProjectV1ToV2({ project: v1Project });
|
||||||
|
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
expect(result.project.version).toBe(2);
|
||||||
|
|
||||||
|
const metadata = result.project.metadata as Record<string, unknown>;
|
||||||
|
expect(metadata.id).toBe(v1Project.id);
|
||||||
|
expect(metadata.name).toBe(v1Project.name);
|
||||||
|
expect(typeof metadata.createdAt).toBe("string");
|
||||||
|
expect(typeof metadata.updatedAt).toBe("string");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("creates settings object from flat properties", () => {
|
||||||
|
const result = transformProjectV1ToV2({ project: v1Project });
|
||||||
|
|
||||||
|
const settings = result.project.settings as Record<string, unknown>;
|
||||||
|
expect(settings.fps).toBe(v1Project.fps);
|
||||||
|
expect(settings.canvasSize).toEqual(v1Project.canvasSize);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("converts color background correctly", () => {
|
||||||
|
const result = transformProjectV1ToV2({ project: v1Project });
|
||||||
|
|
||||||
|
const settings = result.project.settings as Record<string, unknown>;
|
||||||
|
const background = settings.background as Record<string, unknown>;
|
||||||
|
expect(background.type).toBe("color");
|
||||||
|
expect(background.color).toBe(v1Project.backgroundColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("converts blur background correctly", () => {
|
||||||
|
const projectWithBlur = {
|
||||||
|
...v1Project,
|
||||||
|
backgroundType: "blur",
|
||||||
|
blurIntensity: 30,
|
||||||
|
};
|
||||||
|
const result = transformProjectV1ToV2({ project: projectWithBlur });
|
||||||
|
|
||||||
|
const settings = result.project.settings as Record<string, unknown>;
|
||||||
|
const background = settings.background as Record<string, unknown>;
|
||||||
|
expect(background.type).toBe("blur");
|
||||||
|
expect(background.blurIntensity).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("applies legacy bookmarks to main scene", () => {
|
||||||
|
const result = transformProjectV1ToV2({ project: v1Project });
|
||||||
|
|
||||||
|
const scenes = result.project.scenes as Array<Record<string, unknown>>;
|
||||||
|
const mainScene = scenes.find((s) => s.isMain === true);
|
||||||
|
expect(mainScene?.bookmarks).toEqual(v1Project.bookmarks);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preserves existing scene bookmarks", () => {
|
||||||
|
const result = transformProjectV1ToV2({
|
||||||
|
project: v1ProjectWithMultipleScenes,
|
||||||
|
});
|
||||||
|
|
||||||
|
const scenes = result.project.scenes as Array<Record<string, unknown>>;
|
||||||
|
const introScene = scenes.find((s) => s.name === "Intro");
|
||||||
|
expect(introScene?.bookmarks).toEqual([1.0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("skips project that already has v2 structure", () => {
|
||||||
|
const result = transformProjectV1ToV2({ project: v2Project });
|
||||||
|
|
||||||
|
expect(result.skipped).toBe(true);
|
||||||
|
expect(result.reason).toBe("already v2");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("skips project with no id", () => {
|
||||||
|
const result = transformProjectV1ToV2({ project: projectWithNoId });
|
||||||
|
|
||||||
|
expect(result.skipped).toBe(true);
|
||||||
|
expect(result.reason).toBe("no project id");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles null values gracefully", () => {
|
||||||
|
const result = transformProjectV1ToV2({ project: projectWithNullValues });
|
||||||
|
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
const settings = result.project.settings as Record<string, unknown>;
|
||||||
|
expect(settings.fps).toBe(DEFAULT_FPS);
|
||||||
|
expect(settings.canvasSize).toEqual(DEFAULT_CANVAS_SIZE);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses default values for missing properties", () => {
|
||||||
|
const minimalProject = {
|
||||||
|
id: "minimal",
|
||||||
|
version: 1,
|
||||||
|
scenes: [],
|
||||||
|
};
|
||||||
|
const result = transformProjectV1ToV2({ project: minimalProject });
|
||||||
|
|
||||||
|
const settings = result.project.settings as Record<string, unknown>;
|
||||||
|
expect(settings.fps).toBe(DEFAULT_FPS);
|
||||||
|
expect(settings.canvasSize).toEqual(DEFAULT_CANVAS_SIZE);
|
||||||
|
|
||||||
|
const background = settings.background as Record<string, unknown>;
|
||||||
|
expect(background.type).toBe("color");
|
||||||
|
expect(background.color).toBe(DEFAULT_COLOR);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses default blur intensity when missing", () => {
|
||||||
|
const projectWithBlurNoIntensity = {
|
||||||
|
id: "blur-no-intensity",
|
||||||
|
version: 1,
|
||||||
|
backgroundType: "blur",
|
||||||
|
scenes: [],
|
||||||
|
};
|
||||||
|
const result = transformProjectV1ToV2({
|
||||||
|
project: projectWithBlurNoIntensity,
|
||||||
|
});
|
||||||
|
|
||||||
|
const settings = result.project.settings as Record<string, unknown>;
|
||||||
|
const background = settings.background as Record<string, unknown>;
|
||||||
|
expect(background.blurIntensity).toBe(DEFAULT_BLUR_INTENSITY);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preserves currentSceneId", () => {
|
||||||
|
const result = transformProjectV1ToV2({ project: v1Project });
|
||||||
|
expect(result.project.currentSceneId).toBe(v1Project.currentSceneId);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("finds main scene id when currentSceneId missing", () => {
|
||||||
|
const projectWithoutCurrentScene = {
|
||||||
|
...v1Project,
|
||||||
|
currentSceneId: undefined,
|
||||||
|
};
|
||||||
|
const result = transformProjectV1ToV2({
|
||||||
|
project: projectWithoutCurrentScene,
|
||||||
|
});
|
||||||
|
expect(result.project.currentSceneId).toBe("scene-main");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getProjectId", () => {
|
||||||
|
test("returns id from root level", () => {
|
||||||
|
const id = getProjectId({ project: v1Project });
|
||||||
|
expect(id).toBe("project-v1-123");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns id from metadata", () => {
|
||||||
|
const id = getProjectId({ project: v1ProjectWithMultipleScenes });
|
||||||
|
expect(id).toBe("project-v1-multi");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { getProjectId, transformProjectV2ToV3 } from "../transformers/v2-to-v3";
|
||||||
|
import {
|
||||||
|
projectWithNoId,
|
||||||
|
v2Project,
|
||||||
|
v2ProjectEmptyScenes,
|
||||||
|
v2ProjectSceneWithoutTracks,
|
||||||
|
v2ProjectWithBlurBackground,
|
||||||
|
v3Project,
|
||||||
|
} from "./fixtures";
|
||||||
|
|
||||||
|
describe("V2 to V3 Migration", () => {
|
||||||
|
describe("transformProjectV2ToV3", () => {
|
||||||
|
test("adds duration to metadata", () => {
|
||||||
|
const result = transformProjectV2ToV3({ project: v2Project });
|
||||||
|
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
expect(result.project.version).toBe(3);
|
||||||
|
|
||||||
|
const metadata = result.project.metadata as Record<string, unknown>;
|
||||||
|
expect(typeof metadata.duration).toBe("number");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("calculates duration from scene tracks", () => {
|
||||||
|
const result = transformProjectV2ToV3({ project: v2Project });
|
||||||
|
|
||||||
|
const metadata = result.project.metadata as Record<string, unknown>;
|
||||||
|
// v2Project has a video element with duration 15.5 and a text element at startTime 2 with duration 5
|
||||||
|
// Total duration should be max(15.5, 2+5) = 15.5
|
||||||
|
expect(metadata.duration).toBe(15.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles project with blur background", () => {
|
||||||
|
const result = transformProjectV2ToV3({
|
||||||
|
project: v2ProjectWithBlurBackground,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
const metadata = result.project.metadata as Record<string, unknown>;
|
||||||
|
// v2ProjectWithBlurBackground has a video with duration 30
|
||||||
|
expect(metadata.duration).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles empty scenes with zero duration", () => {
|
||||||
|
const result = transformProjectV2ToV3({ project: v2ProjectEmptyScenes });
|
||||||
|
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
const metadata = result.project.metadata as Record<string, unknown>;
|
||||||
|
expect(metadata.duration).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles scene without tracks property", () => {
|
||||||
|
const result = transformProjectV2ToV3({
|
||||||
|
project: v2ProjectSceneWithoutTracks,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
const metadata = result.project.metadata as Record<string, unknown>;
|
||||||
|
expect(metadata.duration).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("skips project that already has v3 structure", () => {
|
||||||
|
const result = transformProjectV2ToV3({ project: v3Project });
|
||||||
|
|
||||||
|
expect(result.skipped).toBe(true);
|
||||||
|
expect(result.reason).toBe("already v3");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("skips project that has duration in metadata", () => {
|
||||||
|
const projectWithDuration = {
|
||||||
|
...v2Project,
|
||||||
|
version: 2,
|
||||||
|
metadata: {
|
||||||
|
...v2Project.metadata,
|
||||||
|
duration: 10,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = transformProjectV2ToV3({ project: projectWithDuration });
|
||||||
|
|
||||||
|
expect(result.skipped).toBe(true);
|
||||||
|
expect(result.reason).toBe("already v3");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("skips project with no id", () => {
|
||||||
|
const result = transformProjectV2ToV3({ project: projectWithNoId });
|
||||||
|
|
||||||
|
expect(result.skipped).toBe(true);
|
||||||
|
expect(result.reason).toBe("no project id");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preserves existing metadata fields", () => {
|
||||||
|
const result = transformProjectV2ToV3({ project: v2Project });
|
||||||
|
|
||||||
|
const metadata = result.project.metadata as Record<string, unknown>;
|
||||||
|
expect(metadata.id).toBe(v2Project.metadata.id);
|
||||||
|
expect(metadata.name).toBe(v2Project.metadata.name);
|
||||||
|
expect(metadata.thumbnail).toBe(v2Project.metadata.thumbnail);
|
||||||
|
expect(metadata.createdAt).toBe(v2Project.metadata.createdAt);
|
||||||
|
expect(metadata.updatedAt).toBe(v2Project.metadata.updatedAt);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preserves settings object", () => {
|
||||||
|
const result = transformProjectV2ToV3({ project: v2Project });
|
||||||
|
|
||||||
|
expect(result.project.settings).toEqual(v2Project.settings);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preserves scenes array", () => {
|
||||||
|
const result = transformProjectV2ToV3({ project: v2Project });
|
||||||
|
|
||||||
|
expect(result.project.scenes).toEqual(v2Project.scenes);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles project without metadata object", () => {
|
||||||
|
const projectWithoutMetadata = {
|
||||||
|
id: "no-metadata",
|
||||||
|
version: 2,
|
||||||
|
scenes: [],
|
||||||
|
};
|
||||||
|
const result = transformProjectV2ToV3({
|
||||||
|
project: projectWithoutMetadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
const metadata = result.project.metadata as Record<string, unknown>;
|
||||||
|
expect(metadata.duration).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("calculates duration from main scene only", () => {
|
||||||
|
const multiSceneProject = {
|
||||||
|
id: "multi-scene",
|
||||||
|
version: 2,
|
||||||
|
metadata: { id: "multi-scene", name: "Multi" },
|
||||||
|
scenes: [
|
||||||
|
{
|
||||||
|
id: "scene-1",
|
||||||
|
isMain: true,
|
||||||
|
tracks: [
|
||||||
|
{
|
||||||
|
type: "video",
|
||||||
|
elements: [{ startTime: 0, duration: 10 }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "scene-2",
|
||||||
|
isMain: false,
|
||||||
|
tracks: [
|
||||||
|
{
|
||||||
|
type: "video",
|
||||||
|
elements: [{ startTime: 0, duration: 20 }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const result = transformProjectV2ToV3({ project: multiSceneProject });
|
||||||
|
|
||||||
|
const metadata = result.project.metadata as Record<string, unknown>;
|
||||||
|
// Duration is from main scene only, not sum of all scenes
|
||||||
|
expect(metadata.duration).toBe(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getProjectId", () => {
|
||||||
|
test("returns id from root level", () => {
|
||||||
|
const projectWithRootId = { id: "root-id", metadata: {} };
|
||||||
|
const id = getProjectId({ project: projectWithRootId });
|
||||||
|
expect(id).toBe("root-id");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns id from metadata when root id missing", () => {
|
||||||
|
const id = getProjectId({ project: v2Project });
|
||||||
|
expect(id).toBe("project-v2-123");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("prefers root id over metadata id", () => {
|
||||||
|
const projectWithBothIds = {
|
||||||
|
id: "root-id",
|
||||||
|
metadata: { id: "metadata-id" },
|
||||||
|
};
|
||||||
|
const id = getProjectId({ project: projectWithBothIds });
|
||||||
|
expect(id).toBe("root-id");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export { transformProjectV0ToV1 } from "./v0-to-v1";
|
||||||
|
export { transformProjectV1ToV2 } from "./v1-to-v2";
|
||||||
|
export { transformProjectV2ToV3 } from "./v2-to-v3";
|
||||||
|
export type { MigrationResult, ProjectRecord } from "./types";
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Type definitions for different project versions used in migrations.
|
||||||
|
* These types are intentionally loose (using Record) because we're dealing
|
||||||
|
* with potentially malformed data from older versions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ProjectRecord = Record<string, unknown>;
|
||||||
|
|
||||||
|
export interface MigrationResult<T> {
|
||||||
|
project: T;
|
||||||
|
skipped: boolean;
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { buildDefaultScene } from "@/lib/scenes";
|
||||||
|
import type { SerializedScene } from "@/services/storage/types";
|
||||||
|
import type { TScene } from "@/types/timeline";
|
||||||
|
import type { MigrationResult, ProjectRecord } from "./types";
|
||||||
|
|
||||||
|
export interface TransformV0ToV1Options {
|
||||||
|
now?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformProjectV0ToV1({
|
||||||
|
project,
|
||||||
|
options = {},
|
||||||
|
}: {
|
||||||
|
project: ProjectRecord;
|
||||||
|
options?: TransformV0ToV1Options;
|
||||||
|
}): MigrationResult<ProjectRecord> {
|
||||||
|
const { now = new Date() } = options;
|
||||||
|
|
||||||
|
const scenesValue = project.scenes;
|
||||||
|
if (Array.isArray(scenesValue) && scenesValue.length > 0) {
|
||||||
|
return { project, skipped: true, reason: "already has scenes" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainScene = buildDefaultScene({ isMain: true, name: "Main scene" });
|
||||||
|
const serializedScene = serializeScene({ scene: mainScene });
|
||||||
|
const updatedProject: ProjectRecord = {
|
||||||
|
...project,
|
||||||
|
scenes: [serializedScene],
|
||||||
|
currentSceneId: mainScene.id,
|
||||||
|
version: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedAt = now.toISOString();
|
||||||
|
if (isRecord(project.metadata)) {
|
||||||
|
updatedProject.metadata = {
|
||||||
|
...project.metadata,
|
||||||
|
updatedAt,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
updatedProject.updatedAt = updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { project: updatedProject, skipped: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProjectId({
|
||||||
|
project,
|
||||||
|
}: {
|
||||||
|
project: ProjectRecord;
|
||||||
|
}): string | null {
|
||||||
|
const idValue = project.id;
|
||||||
|
if (typeof idValue === "string" && idValue.length > 0) {
|
||||||
|
return idValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadataValue = project.metadata;
|
||||||
|
if (!isRecord(metadataValue)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadataId = metadataValue.id;
|
||||||
|
if (typeof metadataId === "string" && metadataId.length > 0) {
|
||||||
|
return metadataId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeScene({ scene }: { scene: TScene }): SerializedScene {
|
||||||
|
return {
|
||||||
|
id: scene.id,
|
||||||
|
name: scene.name,
|
||||||
|
isMain: scene.isMain,
|
||||||
|
tracks: scene.tracks,
|
||||||
|
bookmarks: scene.bookmarks,
|
||||||
|
createdAt: scene.createdAt.toISOString(),
|
||||||
|
updatedAt: scene.updatedAt.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is ProjectRecord {
|
||||||
|
return typeof value === "object" && value !== null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,326 @@
|
|||||||
|
import {
|
||||||
|
DEFAULT_BLUR_INTENSITY,
|
||||||
|
DEFAULT_CANVAS_SIZE,
|
||||||
|
DEFAULT_COLOR,
|
||||||
|
DEFAULT_FPS,
|
||||||
|
} from "@/constants/project-constants";
|
||||||
|
import type { MigrationResult, ProjectRecord } from "./types";
|
||||||
|
|
||||||
|
export function transformProjectV1ToV2({
|
||||||
|
project,
|
||||||
|
}: {
|
||||||
|
project: ProjectRecord;
|
||||||
|
}): MigrationResult<ProjectRecord> {
|
||||||
|
const projectId = getProjectId({ project });
|
||||||
|
if (!projectId) {
|
||||||
|
return { project, skipped: true, reason: "no project id" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isV2Project({ project })) {
|
||||||
|
return { project, skipped: true, reason: "already v2" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const migratedProject = migrateProject({ project, projectId });
|
||||||
|
return { project: migratedProject, skipped: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateProject({
|
||||||
|
project,
|
||||||
|
projectId,
|
||||||
|
}: {
|
||||||
|
project: ProjectRecord;
|
||||||
|
projectId: string;
|
||||||
|
}): ProjectRecord {
|
||||||
|
const createdAt = normalizeDateString({ value: project.createdAt });
|
||||||
|
const updatedAt = normalizeDateString({ value: project.updatedAt });
|
||||||
|
const metadataValue = project.metadata;
|
||||||
|
|
||||||
|
const metadata = isRecord(metadataValue)
|
||||||
|
? {
|
||||||
|
id: getStringValue({ value: metadataValue.id, fallback: projectId }),
|
||||||
|
name: getStringValue({ value: metadataValue.name, fallback: "" }),
|
||||||
|
thumbnail: getStringValue({ value: metadataValue.thumbnail }),
|
||||||
|
createdAt: normalizeDateString({ value: metadataValue.createdAt }),
|
||||||
|
updatedAt: normalizeDateString({ value: metadataValue.updatedAt }),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
id: projectId,
|
||||||
|
name: getStringValue({ value: project.name, fallback: "" }),
|
||||||
|
thumbnail: getStringValue({ value: project.thumbnail }),
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
const scenesValue = project.scenes;
|
||||||
|
const scenes = Array.isArray(scenesValue) ? scenesValue : [];
|
||||||
|
const legacyBookmarks = Array.isArray(project.bookmarks)
|
||||||
|
? project.bookmarks
|
||||||
|
: null;
|
||||||
|
const normalizedScenes = applyLegacyBookmarks({
|
||||||
|
scenes,
|
||||||
|
legacyBookmarks,
|
||||||
|
});
|
||||||
|
|
||||||
|
const settingsValue = project.settings;
|
||||||
|
const settings = isRecord(settingsValue)
|
||||||
|
? {
|
||||||
|
fps: getNumberValue({
|
||||||
|
value: settingsValue.fps,
|
||||||
|
fallback: DEFAULT_FPS,
|
||||||
|
}),
|
||||||
|
canvasSize: getCanvasSizeValue({
|
||||||
|
value: settingsValue.canvasSize,
|
||||||
|
fallback: DEFAULT_CANVAS_SIZE,
|
||||||
|
}),
|
||||||
|
background: getBackgroundValue({
|
||||||
|
value: settingsValue.background,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
fps: getNumberValue({ value: project.fps, fallback: DEFAULT_FPS }),
|
||||||
|
canvasSize: getCanvasSizeValue({
|
||||||
|
value: project.canvasSize,
|
||||||
|
fallback: DEFAULT_CANVAS_SIZE,
|
||||||
|
}),
|
||||||
|
background: getBackgroundValue({
|
||||||
|
value: project.background,
|
||||||
|
backgroundType: project.backgroundType,
|
||||||
|
backgroundColor: project.backgroundColor,
|
||||||
|
blurIntensity: project.blurIntensity,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentSceneId = getCurrentSceneId({
|
||||||
|
value: project.currentSceneId,
|
||||||
|
scenes: normalizedScenes,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...project,
|
||||||
|
metadata,
|
||||||
|
scenes: normalizedScenes,
|
||||||
|
currentSceneId,
|
||||||
|
settings,
|
||||||
|
version: 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProjectId({
|
||||||
|
project,
|
||||||
|
}: {
|
||||||
|
project: ProjectRecord;
|
||||||
|
}): string | null {
|
||||||
|
const idValue = project.id;
|
||||||
|
if (typeof idValue === "string" && idValue.length > 0) {
|
||||||
|
return idValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadataValue = project.metadata;
|
||||||
|
if (!isRecord(metadataValue)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadataId = metadataValue.id;
|
||||||
|
if (typeof metadataId === "string" && metadataId.length > 0) {
|
||||||
|
return metadataId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentSceneId({
|
||||||
|
value,
|
||||||
|
scenes,
|
||||||
|
}: {
|
||||||
|
value: unknown;
|
||||||
|
scenes: unknown[];
|
||||||
|
}): string {
|
||||||
|
if (typeof value === "string" && value.length > 0) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainSceneId = findMainSceneId({ scenes });
|
||||||
|
if (mainSceneId) {
|
||||||
|
return mainSceneId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function findMainSceneId({ scenes }: { scenes: unknown[] }): string | null {
|
||||||
|
for (const scene of scenes) {
|
||||||
|
if (!isRecord(scene)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scene.isMain === true && typeof scene.id === "string") {
|
||||||
|
return scene.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const scene of scenes) {
|
||||||
|
if (!isRecord(scene)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof scene.id === "string") {
|
||||||
|
return scene.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyLegacyBookmarks({
|
||||||
|
scenes,
|
||||||
|
legacyBookmarks,
|
||||||
|
}: {
|
||||||
|
scenes: unknown[];
|
||||||
|
legacyBookmarks: unknown[] | null;
|
||||||
|
}): unknown[] {
|
||||||
|
if (!legacyBookmarks || legacyBookmarks.length === 0) {
|
||||||
|
return scenes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainSceneId = findMainSceneId({ scenes });
|
||||||
|
|
||||||
|
return scenes.map((scene) => {
|
||||||
|
if (!isRecord(scene)) {
|
||||||
|
return scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mainSceneId && scene.id !== mainSceneId) {
|
||||||
|
return scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(scene.bookmarks) && scene.bookmarks.length > 0) {
|
||||||
|
return scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...scene,
|
||||||
|
bookmarks: legacyBookmarks,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBackgroundValue({
|
||||||
|
value,
|
||||||
|
backgroundType,
|
||||||
|
backgroundColor,
|
||||||
|
blurIntensity,
|
||||||
|
}: {
|
||||||
|
value?: unknown;
|
||||||
|
backgroundType?: unknown;
|
||||||
|
backgroundColor?: unknown;
|
||||||
|
blurIntensity?: unknown;
|
||||||
|
}): {
|
||||||
|
type: "color" | "blur";
|
||||||
|
color?: string;
|
||||||
|
blurIntensity?: number;
|
||||||
|
} {
|
||||||
|
if (isRecord(value)) {
|
||||||
|
const typeValue = value.type;
|
||||||
|
if (typeValue === "blur") {
|
||||||
|
return {
|
||||||
|
type: "blur",
|
||||||
|
blurIntensity: getNumberValue({
|
||||||
|
value: value.blurIntensity,
|
||||||
|
fallback: DEFAULT_BLUR_INTENSITY,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "color",
|
||||||
|
color: getStringValue({ value: value.color, fallback: DEFAULT_COLOR }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backgroundType === "blur") {
|
||||||
|
return {
|
||||||
|
type: "blur",
|
||||||
|
blurIntensity: getNumberValue({
|
||||||
|
value: blurIntensity,
|
||||||
|
fallback: DEFAULT_BLUR_INTENSITY,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "color",
|
||||||
|
color: getStringValue({ value: backgroundColor, fallback: DEFAULT_COLOR }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCanvasSizeValue({
|
||||||
|
value,
|
||||||
|
fallback,
|
||||||
|
}: {
|
||||||
|
value: unknown;
|
||||||
|
fallback: { width: number; height: number };
|
||||||
|
}): { width: number; height: number } {
|
||||||
|
if (isRecord(value)) {
|
||||||
|
const width = getNumberValue({
|
||||||
|
value: value.width,
|
||||||
|
fallback: fallback.width,
|
||||||
|
});
|
||||||
|
const height = getNumberValue({
|
||||||
|
value: value.height,
|
||||||
|
fallback: fallback.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { width, height };
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNumberValue({
|
||||||
|
value,
|
||||||
|
fallback,
|
||||||
|
}: {
|
||||||
|
value: unknown;
|
||||||
|
fallback: number;
|
||||||
|
}): number {
|
||||||
|
return typeof value === "number" ? value : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStringValue({
|
||||||
|
value,
|
||||||
|
fallback,
|
||||||
|
}: {
|
||||||
|
value: unknown;
|
||||||
|
fallback?: string;
|
||||||
|
}): string | undefined {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDateString({ value }: { value: unknown }): string {
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return value.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isV2Project({ project }: { project: ProjectRecord }): boolean {
|
||||||
|
const versionValue = project.version;
|
||||||
|
if (typeof versionValue === "number" && versionValue >= 2) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isRecord(project.metadata) && isRecord(project.settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is ProjectRecord {
|
||||||
|
return typeof value === "object" && value !== null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { getProjectDurationFromScenes } from "@/lib/scenes";
|
||||||
|
import type { TScene } from "@/types/timeline";
|
||||||
|
import type { MigrationResult, ProjectRecord } from "./types";
|
||||||
|
|
||||||
|
export function transformProjectV2ToV3({
|
||||||
|
project,
|
||||||
|
}: {
|
||||||
|
project: ProjectRecord;
|
||||||
|
}): MigrationResult<ProjectRecord> {
|
||||||
|
const projectId = getProjectId({ project });
|
||||||
|
if (!projectId) {
|
||||||
|
return { project, skipped: true, reason: "no project id" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isV3Project({ project })) {
|
||||||
|
return { project, skipped: true, reason: "already v3" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const scenes = getScenes({ project });
|
||||||
|
const duration = getProjectDurationFromScenes({ scenes });
|
||||||
|
|
||||||
|
const metadataValue = project.metadata;
|
||||||
|
const metadata = isRecord(metadataValue)
|
||||||
|
? { ...metadataValue, duration }
|
||||||
|
: { duration };
|
||||||
|
|
||||||
|
const migratedProject = {
|
||||||
|
...project,
|
||||||
|
metadata,
|
||||||
|
version: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { project: migratedProject, skipped: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProjectId({
|
||||||
|
project,
|
||||||
|
}: {
|
||||||
|
project: ProjectRecord;
|
||||||
|
}): string | null {
|
||||||
|
const idValue = project.id;
|
||||||
|
if (typeof idValue === "string" && idValue.length > 0) {
|
||||||
|
return idValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadataValue = project.metadata;
|
||||||
|
if (!isRecord(metadataValue)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadataId = metadataValue.id;
|
||||||
|
if (typeof metadataId === "string" && metadataId.length > 0) {
|
||||||
|
return metadataId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScenes({ project }: { project: ProjectRecord }): TScene[] {
|
||||||
|
const scenesValue = project.scenes;
|
||||||
|
if (!Array.isArray(scenesValue)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return scenesValue.filter(isRecord) as unknown as TScene[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isV3Project({ project }: { project: ProjectRecord }): boolean {
|
||||||
|
const versionValue = project.version;
|
||||||
|
if (typeof versionValue === "number" && versionValue >= 3) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
isRecord(project.metadata) && typeof project.metadata.duration === "number"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is ProjectRecord {
|
||||||
|
return typeof value === "object" && value !== null;
|
||||||
|
}
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter";
|
import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter";
|
||||||
import type { SerializedScene } from "@/services/storage/types";
|
|
||||||
import { buildDefaultScene } from "@/lib/scenes";
|
|
||||||
import type { TScene } from "@/types/timeline";
|
|
||||||
import { StorageMigration } from "./base";
|
import { StorageMigration } from "./base";
|
||||||
|
import { getProjectId, transformProjectV0ToV1 } from "./transformers/v0-to-v1";
|
||||||
type ProjectRecord = Record<string, unknown>;
|
|
||||||
|
|
||||||
export class V0toV1Migration extends StorageMigration {
|
export class V0toV1Migration extends StorageMigration {
|
||||||
from = 0;
|
from = 0;
|
||||||
@@ -19,75 +15,24 @@ export class V0toV1Migration extends StorageMigration {
|
|||||||
const projects = await projectsAdapter.getAll();
|
const projects = await projectsAdapter.getAll();
|
||||||
|
|
||||||
for (const project of projects) {
|
for (const project of projects) {
|
||||||
if (!isRecord(project)) {
|
if (typeof project !== "object" || project === null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const scenesValue = project.scenes;
|
const result = transformProjectV0ToV1({
|
||||||
if (Array.isArray(scenesValue) && scenesValue.length > 0) {
|
project: project as Record<string, unknown>,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.skipped) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainScene = buildDefaultScene({ isMain: true, name: "Main scene" });
|
const projectId = getProjectId({ project: result.project });
|
||||||
const serializedScene = serializeScene({ scene: mainScene });
|
|
||||||
const updatedProject: ProjectRecord = {
|
|
||||||
...project,
|
|
||||||
scenes: [serializedScene],
|
|
||||||
currentSceneId: mainScene.id,
|
|
||||||
version: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedAt = new Date().toISOString();
|
|
||||||
if (isRecord(project.metadata)) {
|
|
||||||
updatedProject.metadata = {
|
|
||||||
...project.metadata,
|
|
||||||
updatedAt,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
updatedProject.updatedAt = updatedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
const projectId = getProjectId({ project: updatedProject });
|
|
||||||
if (!projectId) {
|
if (!projectId) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await projectsAdapter.set(projectId, updatedProject);
|
await projectsAdapter.set(projectId, result.project);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializeScene({ scene }: { scene: TScene }): SerializedScene {
|
|
||||||
return {
|
|
||||||
id: scene.id,
|
|
||||||
name: scene.name,
|
|
||||||
isMain: scene.isMain,
|
|
||||||
tracks: scene.tracks,
|
|
||||||
bookmarks: scene.bookmarks,
|
|
||||||
createdAt: scene.createdAt.toISOString(),
|
|
||||||
updatedAt: scene.updatedAt.toISOString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getProjectId({ project }: { project: ProjectRecord }): string | null {
|
|
||||||
const idValue = project.id;
|
|
||||||
if (typeof idValue === "string" && idValue.length > 0) {
|
|
||||||
return idValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadataValue = project.metadata;
|
|
||||||
if (!isRecord(metadataValue)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadataId = metadataValue.id;
|
|
||||||
if (typeof metadataId === "string" && metadataId.length > 0) {
|
|
||||||
return metadataId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isRecord(value: unknown): value is ProjectRecord {
|
|
||||||
return typeof value === "object" && value !== null;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import {
|
|
||||||
DEFAULT_BLUR_INTENSITY,
|
|
||||||
DEFAULT_CANVAS_SIZE,
|
|
||||||
DEFAULT_COLOR,
|
|
||||||
DEFAULT_FPS,
|
|
||||||
} from "@/constants/project-constants";
|
|
||||||
import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter";
|
import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter";
|
||||||
import { StorageMigration } from "./base";
|
import { StorageMigration } from "./base";
|
||||||
|
import { getProjectId, transformProjectV1ToV2 } from "./transformers/v1-to-v2";
|
||||||
type ProjectRecord = Record<string, unknown>;
|
|
||||||
|
|
||||||
export class V1toV2Migration extends StorageMigration {
|
export class V1toV2Migration extends StorageMigration {
|
||||||
from = 1;
|
from = 1;
|
||||||
@@ -22,318 +15,24 @@ export class V1toV2Migration extends StorageMigration {
|
|||||||
const projects = await projectsAdapter.getAll();
|
const projects = await projectsAdapter.getAll();
|
||||||
|
|
||||||
for (const project of projects) {
|
for (const project of projects) {
|
||||||
if (!isRecord(project)) {
|
if (typeof project !== "object" || project === null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectId = getProjectId({ project });
|
const result = transformProjectV1ToV2({
|
||||||
|
project: project as Record<string, unknown>,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.skipped) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectId = getProjectId({ project: result.project });
|
||||||
if (!projectId) {
|
if (!projectId) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isV2Project({ project })) {
|
await projectsAdapter.set(projectId, result.project);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const migratedProject = migrateProject({ project, projectId });
|
|
||||||
await projectsAdapter.set(projectId, migratedProject);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrateProject({
|
|
||||||
project,
|
|
||||||
projectId,
|
|
||||||
}: {
|
|
||||||
project: ProjectRecord;
|
|
||||||
projectId: string;
|
|
||||||
}): ProjectRecord {
|
|
||||||
const createdAt = normalizeDateString({ value: project.createdAt });
|
|
||||||
const updatedAt = normalizeDateString({ value: project.updatedAt });
|
|
||||||
const metadataValue = project.metadata;
|
|
||||||
|
|
||||||
const metadata = isRecord(metadataValue)
|
|
||||||
? {
|
|
||||||
id: getStringValue({ value: metadataValue.id, fallback: projectId }),
|
|
||||||
name: getStringValue({ value: metadataValue.name, fallback: "" }),
|
|
||||||
thumbnail: getStringValue({ value: metadataValue.thumbnail }),
|
|
||||||
createdAt: normalizeDateString({ value: metadataValue.createdAt }),
|
|
||||||
updatedAt: normalizeDateString({ value: metadataValue.updatedAt }),
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
id: projectId,
|
|
||||||
name: getStringValue({ value: project.name, fallback: "" }),
|
|
||||||
thumbnail: getStringValue({ value: project.thumbnail }),
|
|
||||||
createdAt,
|
|
||||||
updatedAt,
|
|
||||||
};
|
|
||||||
|
|
||||||
const scenesValue = project.scenes;
|
|
||||||
const scenes = Array.isArray(scenesValue) ? scenesValue : [];
|
|
||||||
const legacyBookmarks = Array.isArray(project.bookmarks)
|
|
||||||
? project.bookmarks
|
|
||||||
: null;
|
|
||||||
const normalizedScenes = applyLegacyBookmarks({
|
|
||||||
scenes,
|
|
||||||
legacyBookmarks,
|
|
||||||
});
|
|
||||||
|
|
||||||
const settingsValue = project.settings;
|
|
||||||
const settings = isRecord(settingsValue)
|
|
||||||
? {
|
|
||||||
fps: getNumberValue({
|
|
||||||
value: settingsValue.fps,
|
|
||||||
fallback: DEFAULT_FPS,
|
|
||||||
}),
|
|
||||||
canvasSize: getCanvasSizeValue({
|
|
||||||
value: settingsValue.canvasSize,
|
|
||||||
fallback: DEFAULT_CANVAS_SIZE,
|
|
||||||
}),
|
|
||||||
background: getBackgroundValue({
|
|
||||||
value: settingsValue.background,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
fps: getNumberValue({ value: project.fps, fallback: DEFAULT_FPS }),
|
|
||||||
canvasSize: getCanvasSizeValue({
|
|
||||||
value: project.canvasSize,
|
|
||||||
fallback: DEFAULT_CANVAS_SIZE,
|
|
||||||
}),
|
|
||||||
background: getBackgroundValue({
|
|
||||||
value: project.background,
|
|
||||||
backgroundType: project.backgroundType,
|
|
||||||
backgroundColor: project.backgroundColor,
|
|
||||||
blurIntensity: project.blurIntensity,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentSceneId = getCurrentSceneId({
|
|
||||||
value: project.currentSceneId,
|
|
||||||
scenes: normalizedScenes,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...project,
|
|
||||||
metadata,
|
|
||||||
scenes: normalizedScenes,
|
|
||||||
currentSceneId,
|
|
||||||
settings,
|
|
||||||
version: 2,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getProjectId({ project }: { project: ProjectRecord }): string | null {
|
|
||||||
const idValue = project.id;
|
|
||||||
if (typeof idValue === "string" && idValue.length > 0) {
|
|
||||||
return idValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadataValue = project.metadata;
|
|
||||||
if (!isRecord(metadataValue)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadataId = metadataValue.id;
|
|
||||||
if (typeof metadataId === "string" && metadataId.length > 0) {
|
|
||||||
return metadataId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCurrentSceneId({
|
|
||||||
value,
|
|
||||||
scenes,
|
|
||||||
}: {
|
|
||||||
value: unknown;
|
|
||||||
scenes: unknown[];
|
|
||||||
}): string {
|
|
||||||
if (typeof value === "string" && value.length > 0) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mainSceneId = findMainSceneId({ scenes });
|
|
||||||
if (mainSceneId) {
|
|
||||||
return mainSceneId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function findMainSceneId({ scenes }: { scenes: unknown[] }): string | null {
|
|
||||||
for (const scene of scenes) {
|
|
||||||
if (!isRecord(scene)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scene.isMain === true && typeof scene.id === "string") {
|
|
||||||
return scene.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const scene of scenes) {
|
|
||||||
if (!isRecord(scene)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof scene.id === "string") {
|
|
||||||
return scene.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyLegacyBookmarks({
|
|
||||||
scenes,
|
|
||||||
legacyBookmarks,
|
|
||||||
}: {
|
|
||||||
scenes: unknown[];
|
|
||||||
legacyBookmarks: unknown[] | null;
|
|
||||||
}): unknown[] {
|
|
||||||
if (!legacyBookmarks || legacyBookmarks.length === 0) {
|
|
||||||
return scenes;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mainSceneId = findMainSceneId({ scenes });
|
|
||||||
|
|
||||||
return scenes.map((scene) => {
|
|
||||||
if (!isRecord(scene)) {
|
|
||||||
return scene;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mainSceneId && scene.id !== mainSceneId) {
|
|
||||||
return scene;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(scene.bookmarks) && scene.bookmarks.length > 0) {
|
|
||||||
return scene;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...scene,
|
|
||||||
bookmarks: legacyBookmarks,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBackgroundValue({
|
|
||||||
value,
|
|
||||||
backgroundType,
|
|
||||||
backgroundColor,
|
|
||||||
blurIntensity,
|
|
||||||
}: {
|
|
||||||
value: unknown;
|
|
||||||
backgroundType?: unknown;
|
|
||||||
backgroundColor?: unknown;
|
|
||||||
blurIntensity?: unknown;
|
|
||||||
}): {
|
|
||||||
type: "color" | "blur";
|
|
||||||
color?: string;
|
|
||||||
blurIntensity?: number;
|
|
||||||
} {
|
|
||||||
if (isRecord(value)) {
|
|
||||||
const typeValue = value.type;
|
|
||||||
if (typeValue === "blur") {
|
|
||||||
return {
|
|
||||||
type: "blur",
|
|
||||||
blurIntensity: getNumberValue({
|
|
||||||
value: value.blurIntensity,
|
|
||||||
fallback: DEFAULT_BLUR_INTENSITY,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "color",
|
|
||||||
color: getStringValue({ value: value.color, fallback: DEFAULT_COLOR }),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (backgroundType === "blur") {
|
|
||||||
return {
|
|
||||||
type: "blur",
|
|
||||||
blurIntensity: getNumberValue({
|
|
||||||
value: blurIntensity,
|
|
||||||
fallback: DEFAULT_BLUR_INTENSITY,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "color",
|
|
||||||
color: getStringValue({ value: backgroundColor, fallback: DEFAULT_COLOR }),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCanvasSizeValue({
|
|
||||||
value,
|
|
||||||
fallback,
|
|
||||||
}: {
|
|
||||||
value: unknown;
|
|
||||||
fallback: { width: number; height: number };
|
|
||||||
}): { width: number; height: number } {
|
|
||||||
if (isRecord(value)) {
|
|
||||||
const width = getNumberValue({
|
|
||||||
value: value.width,
|
|
||||||
fallback: fallback.width,
|
|
||||||
});
|
|
||||||
const height = getNumberValue({
|
|
||||||
value: value.height,
|
|
||||||
fallback: fallback.height,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { width, height };
|
|
||||||
}
|
|
||||||
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNumberValue({
|
|
||||||
value,
|
|
||||||
fallback,
|
|
||||||
}: {
|
|
||||||
value: unknown;
|
|
||||||
fallback: number;
|
|
||||||
}): number {
|
|
||||||
return typeof value === "number" ? value : fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStringValue({
|
|
||||||
value,
|
|
||||||
fallback,
|
|
||||||
}: {
|
|
||||||
value: unknown;
|
|
||||||
fallback?: string;
|
|
||||||
}): string | undefined {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeDateString({ value }: { value: unknown }): string {
|
|
||||||
if (value instanceof Date) {
|
|
||||||
return value.toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Date().toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function isV2Project({ project }: { project: ProjectRecord }): boolean {
|
|
||||||
const versionValue = project.version;
|
|
||||||
if (typeof versionValue === "number" && versionValue >= 2) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isRecord(project.metadata) && isRecord(project.settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isRecord(value: unknown): value is ProjectRecord {
|
|
||||||
return typeof value === "object" && value !== null;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { getProjectDurationFromScenes } from "@/lib/scenes";
|
|
||||||
import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter";
|
import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter";
|
||||||
import type { TScene } from "@/types/timeline";
|
|
||||||
import { StorageMigration } from "./base";
|
import { StorageMigration } from "./base";
|
||||||
|
import { getProjectId, transformProjectV2ToV3 } from "./transformers/v2-to-v3";
|
||||||
type ProjectRecord = Record<string, unknown>;
|
|
||||||
|
|
||||||
export class V2toV3Migration extends StorageMigration {
|
export class V2toV3Migration extends StorageMigration {
|
||||||
from = 2;
|
from = 2;
|
||||||
@@ -18,75 +15,24 @@ export class V2toV3Migration extends StorageMigration {
|
|||||||
const projects = await projectsAdapter.getAll();
|
const projects = await projectsAdapter.getAll();
|
||||||
|
|
||||||
for (const project of projects) {
|
for (const project of projects) {
|
||||||
if (!isRecord(project)) {
|
if (typeof project !== "object" || project === null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectId = getProjectId({ project });
|
const result = transformProjectV2ToV3({
|
||||||
|
project: project as Record<string, unknown>,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.skipped) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectId = getProjectId({ project: result.project });
|
||||||
if (!projectId) {
|
if (!projectId) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isV3Project({ project })) {
|
await projectsAdapter.set(projectId, result.project);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scenes = getScenes({ project });
|
|
||||||
const duration = getProjectDurationFromScenes({ scenes });
|
|
||||||
|
|
||||||
const metadataValue = project.metadata;
|
|
||||||
const metadata = isRecord(metadataValue)
|
|
||||||
? { ...metadataValue, duration }
|
|
||||||
: { duration };
|
|
||||||
|
|
||||||
const migratedProject = {
|
|
||||||
...project,
|
|
||||||
metadata,
|
|
||||||
version: 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
await projectsAdapter.set(projectId, migratedProject);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProjectId({ project }: { project: ProjectRecord }): string | null {
|
|
||||||
const idValue = project.id;
|
|
||||||
if (typeof idValue === "string" && idValue.length > 0) {
|
|
||||||
return idValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadataValue = project.metadata;
|
|
||||||
if (!isRecord(metadataValue)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadataId = metadataValue.id;
|
|
||||||
if (typeof metadataId === "string" && metadataId.length > 0) {
|
|
||||||
return metadataId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getScenes({ project }: { project: ProjectRecord }): TScene[] {
|
|
||||||
const scenesValue = project.scenes;
|
|
||||||
if (!Array.isArray(scenesValue)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return scenesValue.filter(isRecord) as unknown as TScene[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function isV3Project({ project }: { project: ProjectRecord }): boolean {
|
|
||||||
const versionValue = project.version;
|
|
||||||
if (typeof versionValue === "number" && versionValue >= 3) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isRecord(project.metadata) && typeof project.metadata.duration === "number";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isRecord(value: unknown): value is ProjectRecord {
|
|
||||||
return typeof value === "object" && value !== null;
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-7
@@ -333,12 +333,9 @@ class StorageService {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility methods
|
|
||||||
async clearAllData(): Promise<void> {
|
async clearAllData(): Promise<void> {
|
||||||
// Clear all projects
|
|
||||||
await this.projectsAdapter.clear();
|
await this.projectsAdapter.clear();
|
||||||
|
// project-specific media and timelines cleaned up when projects are deleted
|
||||||
// Note: Project-specific media and timelines will be cleaned up when projects are deleted
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStorageInfo(): Promise<{
|
async getStorageInfo(): Promise<{
|
||||||
@@ -392,7 +389,6 @@ class StorageService {
|
|||||||
try {
|
try {
|
||||||
const currentData = await this.loadSavedSounds();
|
const currentData = await this.loadSavedSounds();
|
||||||
|
|
||||||
// Check if sound is already saved
|
|
||||||
if (currentData.sounds.some((sound) => sound.id === soundEffect.id)) {
|
if (currentData.sounds.some((sound) => sound.id === soundEffect.id)) {
|
||||||
return; // Already saved
|
return; // Already saved
|
||||||
}
|
}
|
||||||
@@ -456,7 +452,6 @@ class StorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check browser support
|
|
||||||
isOPFSSupported(): boolean {
|
isOPFSSupported(): boolean {
|
||||||
return OPFSAdapter.isSupported();
|
return OPFSAdapter.isSupported();
|
||||||
}
|
}
|
||||||
@@ -470,6 +465,5 @@ class StorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export singleton instance
|
|
||||||
export const storageService = new StorageService();
|
export const storageService = new StorageService();
|
||||||
export { StorageService };
|
export { StorageService };
|
||||||
+3
-3
@@ -1,4 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
|
TranscriptionLanguage,
|
||||||
TranscriptionResult,
|
TranscriptionResult,
|
||||||
TranscriptionProgress,
|
TranscriptionProgress,
|
||||||
TranscriptionModelId,
|
TranscriptionModelId,
|
||||||
@@ -8,7 +9,6 @@ import {
|
|||||||
TRANSCRIPTION_MODELS,
|
TRANSCRIPTION_MODELS,
|
||||||
} from "@/constants/transcription-constants";
|
} from "@/constants/transcription-constants";
|
||||||
import type { WorkerMessage, WorkerResponse } from "./worker";
|
import type { WorkerMessage, WorkerResponse } from "./worker";
|
||||||
import type { LanguageCode } from "@/types/language";
|
|
||||||
|
|
||||||
type ProgressCallback = (progress: TranscriptionProgress) => void;
|
type ProgressCallback = (progress: TranscriptionProgress) => void;
|
||||||
|
|
||||||
@@ -20,12 +20,12 @@ class TranscriptionService {
|
|||||||
|
|
||||||
async transcribe({
|
async transcribe({
|
||||||
audioData,
|
audioData,
|
||||||
language,
|
language = "auto",
|
||||||
modelId = DEFAULT_TRANSCRIPTION_MODEL,
|
modelId = DEFAULT_TRANSCRIPTION_MODEL,
|
||||||
onProgress,
|
onProgress,
|
||||||
}: {
|
}: {
|
||||||
audioData: Float32Array;
|
audioData: Float32Array;
|
||||||
language?: LanguageCode;
|
language?: TranscriptionLanguage;
|
||||||
modelId?: TranscriptionModelId;
|
modelId?: TranscriptionModelId;
|
||||||
onProgress?: ProgressCallback;
|
onProgress?: ProgressCallback;
|
||||||
}): Promise<TranscriptionResult> {
|
}): Promise<TranscriptionResult> {
|
||||||
+2
@@ -15,6 +15,7 @@ interface VideoSinkData {
|
|||||||
prefetching: boolean;
|
prefetching: boolean;
|
||||||
prefetchPromise: Promise<void> | null;
|
prefetchPromise: Promise<void> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VideoCache {
|
export class VideoCache {
|
||||||
private sinks = new Map<string, VideoSinkData>();
|
private sinks = new Map<string, VideoSinkData>();
|
||||||
private initPromises = new Map<string, Promise<void>>();
|
private initPromises = new Map<string, Promise<void>>();
|
||||||
@@ -311,4 +312,5 @@ export class VideoCache {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const videoCache = new VideoCache();
|
export const videoCache = new VideoCache();
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import type { SoundEffect, SavedSound } from "@/types/sounds";
|
import type { SoundEffect, SavedSound } from "@/types/sounds";
|
||||||
import { storageService } from "@/services/storage/storage-service";
|
import { storageService } from "@/services/storage/service";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import { buildLibraryAudioElement } from "@/lib/timeline/element-utils";
|
import { buildLibraryAudioElement } from "@/lib/timeline/element-utils";
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "opencut",
|
"name": "opencut",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/react": "^19.2.10",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
"better-auth": "^1.4.15",
|
"better-auth": "^1.4.15",
|
||||||
"next": "^16.1.3",
|
"next": "^16.1.3",
|
||||||
},
|
},
|
||||||
@@ -55,9 +57,9 @@
|
|||||||
"pg": "^8.16.2",
|
"pg": "^8.16.2",
|
||||||
"postgres": "^3.4.5",
|
"postgres": "^3.4.5",
|
||||||
"radix-ui": "^1.4.2",
|
"radix-ui": "^1.4.2",
|
||||||
"react": "^18.2.0",
|
"react": "^19.0.0",
|
||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^8.10.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.54.0",
|
"react-hook-form": "^7.54.0",
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.4.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
@@ -85,8 +87,8 @@
|
|||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/node": "^24.2.1",
|
"@types/node": "^24.2.1",
|
||||||
"@types/pg": "^8.15.4",
|
"@types/pg": "^8.15.4",
|
||||||
"@types/react": "^18.2.48",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^19",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"drizzle-kit": "^0.31.4",
|
"drizzle-kit": "^0.31.4",
|
||||||
@@ -497,7 +499,7 @@
|
|||||||
|
|
||||||
"@tailwindcss/typography": ["@tailwindcss/typography@0.5.16", "", { "dependencies": { "lodash.castarray": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.merge": "^4.6.2", "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA=="],
|
"@tailwindcss/typography": ["@tailwindcss/typography@0.5.16", "", { "dependencies": { "lodash.castarray": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.merge": "^4.6.2", "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
"@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
|
||||||
|
|
||||||
"@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="],
|
"@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="],
|
||||||
|
|
||||||
@@ -537,11 +539,9 @@
|
|||||||
|
|
||||||
"@types/pg": ["@types/pg@8.15.4", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg=="],
|
"@types/pg": ["@types/pg@8.15.4", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg=="],
|
||||||
|
|
||||||
"@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="],
|
"@types/react": ["@types/react@19.2.10", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
|
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="],
|
|
||||||
|
|
||||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||||
|
|
||||||
@@ -575,7 +575,7 @@
|
|||||||
|
|
||||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
"bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
|
||||||
|
|
||||||
"caniuse-lite": ["caniuse-lite@1.0.30001727", "", {}, "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q=="],
|
"caniuse-lite": ["caniuse-lite@1.0.30001727", "", {}, "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q=="],
|
||||||
|
|
||||||
@@ -973,7 +973,7 @@
|
|||||||
|
|
||||||
"react-day-picker": ["react-day-picker@8.10.1", "", { "peerDependencies": { "date-fns": "^2.28.0 || ^3.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA=="],
|
"react-day-picker": ["react-day-picker@8.10.1", "", { "peerDependencies": { "date-fns": "^2.28.0 || ^3.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA=="],
|
||||||
|
|
||||||
"react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="],
|
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
|
||||||
|
|
||||||
"react-hook-form": ["react-hook-form@7.60.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A=="],
|
"react-hook-form": ["react-hook-form@7.60.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A=="],
|
||||||
|
|
||||||
@@ -1027,7 +1027,7 @@
|
|||||||
|
|
||||||
"sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="],
|
"sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="],
|
||||||
|
|
||||||
"scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||||
|
|
||||||
@@ -1159,10 +1159,6 @@
|
|||||||
|
|
||||||
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
||||||
|
|
||||||
"@opencut/web/@types/react": ["@types/react@18.3.23", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w=="],
|
|
||||||
|
|
||||||
"@opencut/web/react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
|
|
||||||
|
|
||||||
"@opencut/web/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"@opencut/web/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-accordion/@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
|
"@radix-ui/react-accordion/@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
|
||||||
@@ -1291,8 +1287,6 @@
|
|||||||
|
|
||||||
"@types/pg/@types/node": ["@types/node@22.16.5", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ=="],
|
"@types/pg/@types/node": ["@types/node@22.16.5", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ=="],
|
||||||
|
|
||||||
"@types/react-dom/@types/react": ["@types/react@18.3.23", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w=="],
|
|
||||||
|
|
||||||
"@upstash/core-analytics/@upstash/redis": ["@upstash/redis@1.35.1", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-sIMuAMU9IYbE2bkgDby8KLoQKRiBMXn0moXxqLvUmQ7VUu2CvulZLtK8O0x3WQZFvvZhU5sRC2/lOVZdGfudkA=="],
|
"@upstash/core-analytics/@upstash/redis": ["@upstash/redis@1.35.1", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-sIMuAMU9IYbE2bkgDby8KLoQKRiBMXn0moXxqLvUmQ7VUu2CvulZLtK8O0x3WQZFvvZhU5sRC2/lOVZdGfudkA=="],
|
||||||
|
|
||||||
"cmdk/@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw=="],
|
"cmdk/@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw=="],
|
||||||
@@ -1333,8 +1327,6 @@
|
|||||||
|
|
||||||
"react-day-picker/react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
|
"react-day-picker/react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
|
||||||
|
|
||||||
"react-dom/react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
|
|
||||||
|
|
||||||
"recharts/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
|
"recharts/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
|
||||||
|
|
||||||
"vaul/@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw=="],
|
"vaul/@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw=="],
|
||||||
@@ -1383,8 +1375,6 @@
|
|||||||
|
|
||||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
|
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
|
||||||
|
|
||||||
"@opencut/web/@types/react/csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-alert-dialog/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ=="],
|
"@radix-ui/react-alert-dialog/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-alert-dialog/@radix-ui/react-dialog/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="],
|
"@radix-ui/react-alert-dialog/@radix-ui/react-dialog/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="],
|
||||||
@@ -1395,8 +1385,6 @@
|
|||||||
|
|
||||||
"@types/pg/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
"@types/pg/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
"@types/react-dom/@types/react/csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
|
||||||
|
|
||||||
"cmdk/@radix-ui/react-dialog/@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
|
"cmdk/@radix-ui/react-dialog/@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
|
||||||
|
|
||||||
"cmdk/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ=="],
|
"cmdk/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ=="],
|
||||||
|
|||||||
+4
-1
@@ -13,9 +13,12 @@
|
|||||||
"start:tools": "turbo run start --filter=@opencut/tools",
|
"start:tools": "turbo run start --filter=@opencut/tools",
|
||||||
"lint:web": "biome lint apps/web/src --max-diagnostics=1000",
|
"lint:web": "biome lint apps/web/src --max-diagnostics=1000",
|
||||||
"lint:web:fix": "biome lint apps/web/src --write --max-diagnostics=1000",
|
"lint:web:fix": "biome lint apps/web/src --write --max-diagnostics=1000",
|
||||||
"format:web": "biome format apps/web/src/services/renderer --write --max-diagnostics=1000"
|
"format:web": "biome format apps/web/src/services/renderer --write --max-diagnostics=1000",
|
||||||
|
"test": "bun test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/react": "^19.2.10",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
"better-auth": "^1.4.15",
|
"better-auth": "^1.4.15",
|
||||||
"next": "^16.1.3"
|
"next": "^16.1.3"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user