mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
refactor: restructure files to their domains, new preview overlay system, and dep graph
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { drawCssBackground } from "@/lib/gradients";
|
||||
import { masksRegistry } from "@/lib/masks";
|
||||
import { drawCssBackground } from "@/gradients";
|
||||
import { masksRegistry } from "@/masks";
|
||||
import type { AnyBaseNode } from "../nodes/base-node";
|
||||
import type { CanvasRenderer } from "../canvas-renderer";
|
||||
import { createOffscreenCanvas } from "../canvas-utils";
|
||||
@@ -19,7 +19,7 @@ import type {
|
||||
LayerMaskDescriptor,
|
||||
QuadTransformDescriptor,
|
||||
} from "./types";
|
||||
import { DEFAULT_GRAPHIC_SOURCE_SIZE } from "@/lib/graphics";
|
||||
import { DEFAULT_GRAPHIC_SOURCE_SIZE } from "@/graphics";
|
||||
|
||||
export type TextureUploadDescriptor = {
|
||||
id: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { BlendMode } from "@/lib/rendering";
|
||||
import type { EffectPass } from "@/lib/effects/types";
|
||||
import type { BlendMode } from "@/rendering";
|
||||
import type { EffectPass } from "@/effects/types";
|
||||
|
||||
export type FrameDescriptor = {
|
||||
width: number;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createOffscreenCanvas } from "./canvas-utils";
|
||||
import { effectsRegistry, resolveEffectPasses } from "@/lib/effects";
|
||||
import { buildDefaultParamValues } from "@/lib/registry";
|
||||
import type { ParamValues } from "@/lib/params";
|
||||
import { effectsRegistry, resolveEffectPasses } from "@/effects";
|
||||
import { buildDefaultParamValues } from "@/params/registry";
|
||||
import type { ParamValues } from "@/params";
|
||||
import { gpuRenderer } from "./gpu-renderer";
|
||||
|
||||
const PREVIEW_SIZE = 160;
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
applyMaskFeather as applyMaskFeatherWasm,
|
||||
initializeGpu,
|
||||
} from "opencut-wasm";
|
||||
import type { EffectPass, EffectUniformValue } from "@/lib/effects/types";
|
||||
import type { EffectPass, EffectUniformValue } from "@/effects/types";
|
||||
|
||||
let gpuAvailable = false;
|
||||
let initPromise: Promise<void> | null = null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { EffectPass } from "@/lib/effects/types";
|
||||
import type { RetimeConfig } from "@/lib/timeline";
|
||||
import type { EffectPass } from "@/effects/types";
|
||||
import type { RetimeConfig } from "@/timeline";
|
||||
import { BaseNode } from "./base-node";
|
||||
|
||||
export type BlurBackgroundNodeParams = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { EffectPass } from "@/lib/effects/types";
|
||||
import type { ParamValues } from "@/lib/params";
|
||||
import type { EffectPass } from "@/effects/types";
|
||||
import type { ParamValues } from "@/params";
|
||||
import { BaseNode } from "./base-node";
|
||||
|
||||
export type EffectLayerNodeParams = {
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
DEFAULT_GRAPHIC_SOURCE_SIZE,
|
||||
getGraphicDefinition,
|
||||
registerDefaultGraphics,
|
||||
} from "@/lib/graphics";
|
||||
import type { ParamValues } from "@/lib/params";
|
||||
} from "@/graphics";
|
||||
import type { ParamValues } from "@/params";
|
||||
import {
|
||||
VisualNode,
|
||||
type ResolvedVisualNodeState,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { resolveStickerId } from "@/lib/stickers";
|
||||
import { resolveStickerId } from "@/stickers";
|
||||
import {
|
||||
VisualNode,
|
||||
type ResolvedVisualSourceNodeState,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { BaseNode } from "./base-node";
|
||||
import type { TextElement } from "@/lib/timeline";
|
||||
import type { EffectPass } from "@/lib/effects/types";
|
||||
import type { Transform } from "@/lib/rendering";
|
||||
import type { TextElement } from "@/timeline";
|
||||
import type { EffectPass } from "@/effects/types";
|
||||
import type { Transform } from "@/rendering";
|
||||
import {
|
||||
drawMeasuredTextLayout,
|
||||
} from "@/lib/text/primitives";
|
||||
import type { MeasuredTextElement } from "@/lib/text/measure-element";
|
||||
} from "@/text/primitives";
|
||||
import type { MeasuredTextElement } from "@/text/measure-element";
|
||||
|
||||
export type TextNodeParams = TextElement & {
|
||||
canvasCenter: { x: number; y: number };
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { BaseNode } from "./base-node";
|
||||
import type { Effect, EffectPass } from "@/lib/effects/types";
|
||||
import type { Mask } from "@/lib/masks/types";
|
||||
import type { BlendMode, Transform } from "@/lib/rendering";
|
||||
import type { RetimeConfig, VisualElement } from "@/lib/timeline";
|
||||
import type { Effect, EffectPass } from "@/effects/types";
|
||||
import type { Mask } from "@/masks/types";
|
||||
import type { BlendMode, Transform } from "@/rendering";
|
||||
import type { RetimeConfig, VisualElement } from "@/timeline";
|
||||
|
||||
export interface VisualNodeParams {
|
||||
duration: number;
|
||||
|
||||
@@ -5,20 +5,20 @@ import {
|
||||
resolveGraphicParamsAtTime,
|
||||
resolveOpacityAtTime,
|
||||
resolveTransformAtTime,
|
||||
} from "@/lib/animation";
|
||||
import { resolveEffectParamsAtTime } from "@/lib/animation/effect-param-channel";
|
||||
} from "@/animation";
|
||||
import { resolveEffectParamsAtTime } from "@/animation/effect-param-channel";
|
||||
import {
|
||||
buildGaussianBlurPasses,
|
||||
intensityToSigma,
|
||||
} from "@/lib/effects/definitions/blur";
|
||||
import { effectsRegistry, resolveEffectPasses } from "@/lib/effects";
|
||||
import type { Effect, EffectPass } from "@/lib/effects/types";
|
||||
import { getSourceTimeAtClipTime } from "@/lib/retime";
|
||||
import { DEFAULT_GRAPHIC_SOURCE_SIZE } from "@/lib/graphics";
|
||||
} from "@/effects/definitions/blur";
|
||||
import { effectsRegistry, resolveEffectPasses } from "@/effects";
|
||||
import type { Effect, EffectPass } from "@/effects/types";
|
||||
import { getSourceTimeAtClipTime } from "@/retime";
|
||||
import { DEFAULT_GRAPHIC_SOURCE_SIZE } from "@/graphics";
|
||||
import {
|
||||
getTextMeasurementContext,
|
||||
measureTextElement,
|
||||
} from "@/lib/text/measure-element";
|
||||
} from "@/text/measure-element";
|
||||
import { videoCache } from "@/services/video-cache/service";
|
||||
import type { CanvasRenderer } from "./canvas-renderer";
|
||||
import type { AnyBaseNode } from "./nodes/base-node";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SceneTracks, TimelineTrack } from "@/lib/timeline";
|
||||
import type { MediaAsset } from "@/lib/media/types";
|
||||
import type { SceneTracks, TimelineTrack } from "@/timeline";
|
||||
import type { MediaAsset } from "@/media/types";
|
||||
import { RootNode } from "./nodes/root-node";
|
||||
import { VideoNode } from "./nodes/video-node";
|
||||
import { ImageNode } from "./nodes/image-node";
|
||||
@@ -10,8 +10,8 @@ import { ColorNode } from "./nodes/color-node";
|
||||
import { BlurBackgroundNode } from "./nodes/blur-background-node";
|
||||
import { EffectLayerNode } from "./nodes/effect-layer-node";
|
||||
import type { AnyBaseNode } from "./nodes/base-node";
|
||||
import type { TBackground, TCanvasSize } from "@/lib/project/types";
|
||||
import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/lib/background/blur";
|
||||
import type { TBackground, TCanvasSize } from "@/project/types";
|
||||
import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/background/blur";
|
||||
|
||||
const PREVIEW_MAX_IMAGE_SIZE = 2048;
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@ import {
|
||||
} from "mediabunny";
|
||||
import type { FrameRate } from "opencut-wasm";
|
||||
import { mediaTimeToSeconds } from "opencut-wasm";
|
||||
import { TICKS_PER_SECOND } from "@/lib/wasm";
|
||||
import { frameRateToFloat } from "@/lib/fps/utils";
|
||||
import { TICKS_PER_SECOND } from "@/wasm";
|
||||
import { frameRateToFloat } from "@/fps/utils";
|
||||
import type { RootNode } from "./nodes/root-node";
|
||||
import type { ExportFormat, ExportQuality } from "@/lib/export";
|
||||
import type { ExportFormat, ExportQuality } from "@/export";
|
||||
import { CanvasRenderer } from "./canvas-renderer";
|
||||
|
||||
type ExportParams = {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useStoragePersistence } from "@/services/storage/use-storage-persistence";
|
||||
|
||||
export function StoragePersistenceDialog() {
|
||||
const { showDialog, onConfirm, onDismiss } = useStoragePersistence();
|
||||
|
||||
return (
|
||||
<Dialog open={showDialog} onOpenChange={(open) => !open && onDismiss()}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Don't lose your projects</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogBody>
|
||||
<p className="text-base text-muted-foreground">
|
||||
Your browser can automatically delete your projects when storage
|
||||
runs low.
|
||||
</p>
|
||||
<p className="text-base text-muted-foreground">
|
||||
Allow OpenCut to protect them?
|
||||
</p>
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onDismiss}>
|
||||
Not now
|
||||
</Button>
|
||||
<Button onClick={onConfirm}>Allow</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/lib/background/blur";
|
||||
import { DEFAULT_BACKGROUND_COLOR } from "@/lib/background/color";
|
||||
import { DEFAULT_CANVAS_SIZE } from "@/lib/canvas/sizes";
|
||||
import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/background/blur";
|
||||
import { DEFAULT_BACKGROUND_COLOR } from "@/background/color";
|
||||
import { DEFAULT_CANVAS_SIZE } from "@/canvas/sizes";
|
||||
const DEFAULT_FPS = 30;
|
||||
import type { MediaAssetData } from "@/services/storage/types";
|
||||
import { getProjectId, transformProjectV1ToV2 } from "../transformers/v1-to-v2";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/lib/background/blur";
|
||||
import { DEFAULT_BACKGROUND_COLOR } from "@/lib/background/color";
|
||||
import { DEFAULT_CANVAS_SIZE } from "@/lib/canvas/sizes";
|
||||
import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/background/blur";
|
||||
import { DEFAULT_BACKGROUND_COLOR } from "@/background/color";
|
||||
import { DEFAULT_CANVAS_SIZE } from "@/canvas/sizes";
|
||||
const DEFAULT_FPS = 30;
|
||||
import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter";
|
||||
import type { MediaAssetData } from "@/services/storage/types";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { STICKER_INTRINSIC_SIZE_FALLBACK } from "@/lib/stickers/intrinsic-size";
|
||||
import { STICKER_INTRINSIC_SIZE_FALLBACK } from "@/stickers/intrinsic-size";
|
||||
import type { MigrationResult, ProjectRecord } from "./types";
|
||||
import { getProjectId, isRecord } from "./utils";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MigrationResult, ProjectRecord } from "./types";
|
||||
import { VOLUME_DB_MIN } from "@/lib/timeline/audio-constants";
|
||||
import { clampDb } from "@/lib/timeline/audio-state";
|
||||
import { VOLUME_DB_MIN } from "@/timeline/audio-constants";
|
||||
import { clampDb } from "@/timeline/audio-state";
|
||||
import { getProjectId, isRecord } from "./utils";
|
||||
|
||||
export function transformProjectV17ToV18({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { INTENSITY_TO_SIGMA_DIVISOR } from "@/lib/effects/definitions/blur";
|
||||
import { INTENSITY_TO_SIGMA_DIVISOR } from "@/effects/definitions/blur";
|
||||
import type { MigrationResult, ProjectRecord } from "./types";
|
||||
import { getProjectId, isRecord } from "./utils";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { MigrationResult, ProjectRecord } from "./types";
|
||||
import { getProjectId, isRecord } from "./utils";
|
||||
|
||||
import { TICKS_PER_SECOND } from "@/lib/wasm";
|
||||
import { TICKS_PER_SECOND } from "@/wasm";
|
||||
const ARBITRARY_FPS_DENOMINATOR = 1_000_000;
|
||||
const STANDARD_FRAME_RATES = [
|
||||
{ value: 24_000 / 1_001, numerator: 24_000, denominator: 1_001 },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parseCustomMaskPath } from "@/lib/masks/custom-path";
|
||||
import { parseCustomMaskPath } from "@/masks/custom-path";
|
||||
import type { MigrationResult, ProjectRecord } from "./types";
|
||||
import { getProjectId, isRecord } from "./utils";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { TProject, TProjectMetadata } from "@/lib/project/types";
|
||||
import { getProjectDurationFromScenes } from "@/lib/scenes";
|
||||
import type { MediaAsset } from "@/lib/media/types";
|
||||
import type { TProject, TProjectMetadata } from "@/project/types";
|
||||
import { getProjectDurationFromScenes } from "@/timeline/scenes";
|
||||
import type { MediaAsset } from "@/media/types";
|
||||
import { IndexedDBAdapter } from "./indexeddb-adapter";
|
||||
import { OPFSAdapter } from "./opfs-adapter";
|
||||
import {
|
||||
@@ -16,12 +16,12 @@ import type {
|
||||
SerializedProject,
|
||||
SerializedScene,
|
||||
} from "./types";
|
||||
import type { SavedSoundsData, SavedSound, SoundEffect } from "@/lib/sounds/types";
|
||||
import type { SavedSoundsData, SavedSound, SoundEffect } from "@/sounds/types";
|
||||
import {
|
||||
migrations,
|
||||
runStorageMigrations,
|
||||
} from "@/services/storage/migrations";
|
||||
import type { Bookmark, SceneTracks, TScene } from "@/lib/timeline";
|
||||
import type { Bookmark, SceneTracks, TScene } from "@/timeline";
|
||||
|
||||
function normalizeBookmarks({ raw }: { raw: unknown }): Bookmark[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { MediaType } from "@/lib/media/types";
|
||||
import type { MediaType } from "@/media/types";
|
||||
import type {
|
||||
TProject,
|
||||
TProjectMetadata,
|
||||
TTimelineViewState,
|
||||
} from "@/lib/project/types";
|
||||
import type { TScene } from "@/lib/timeline";
|
||||
} from "@/project/types";
|
||||
import type { TScene } from "@/timeline";
|
||||
|
||||
export interface StorageAdapter<T> {
|
||||
get(key: string): Promise<T | null>;
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
|
||||
export function useLocalStorage<T>({
|
||||
key,
|
||||
defaultValue,
|
||||
}: {
|
||||
key: string;
|
||||
defaultValue: T;
|
||||
}): [
|
||||
T,
|
||||
({ value }: { value: T | ((previousValue: T) => T) }) => void,
|
||||
boolean,
|
||||
] {
|
||||
const [value, setValue] = useState<T>(defaultValue);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const valueRef = useRef(defaultValue);
|
||||
|
||||
// avoid hydration mismatch by reading after mount
|
||||
useEffect(() => {
|
||||
try {
|
||||
const storedValue = localStorage.getItem(key);
|
||||
if (storedValue !== null) {
|
||||
const parsedValue = JSON.parse(storedValue) as T;
|
||||
valueRef.current = parsedValue;
|
||||
setValue(parsedValue);
|
||||
}
|
||||
} catch {
|
||||
// localstorage might be unavailable
|
||||
}
|
||||
setIsReady(true);
|
||||
}, [key]);
|
||||
|
||||
// sync to localstorage after hydration
|
||||
useEffect(() => {
|
||||
if (!isReady) return;
|
||||
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
} catch {
|
||||
// localstorage might be full or disabled
|
||||
}
|
||||
}, [key, value, isReady]);
|
||||
|
||||
const setValueWithCallback = useCallback(
|
||||
({ value: nextValue }: { value: T | ((previousValue: T) => T) }) => {
|
||||
const resolvedValue =
|
||||
typeof nextValue === "function"
|
||||
? (nextValue as (previousValue: T) => T)(valueRef.current)
|
||||
: nextValue;
|
||||
|
||||
valueRef.current = resolvedValue;
|
||||
setValue(resolvedValue);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return [value, setValueWithCallback, isReady];
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const DISMISSED_KEY = "opencut-storage-persist-dismissed";
|
||||
|
||||
function isFirefox(): boolean {
|
||||
return navigator.userAgent.toLowerCase().includes("firefox");
|
||||
}
|
||||
|
||||
export function useStoragePersistence() {
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!navigator.storage?.persist) return;
|
||||
|
||||
const run = async () => {
|
||||
const alreadyPersisted = await navigator.storage.persisted();
|
||||
if (alreadyPersisted) return;
|
||||
|
||||
const dismissed = localStorage.getItem(DISMISSED_KEY) === "true";
|
||||
if (dismissed) return;
|
||||
|
||||
if (isFirefox()) {
|
||||
setShowDialog(true);
|
||||
} else {
|
||||
await navigator.storage.persist();
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
}, []);
|
||||
|
||||
const onConfirm = async () => {
|
||||
setShowDialog(false);
|
||||
await navigator.storage.persist();
|
||||
};
|
||||
|
||||
const onDismiss = () => {
|
||||
setShowDialog(false);
|
||||
localStorage.setItem(DISMISSED_KEY, "true");
|
||||
};
|
||||
|
||||
return { showDialog, onConfirm, onDismiss };
|
||||
}
|
||||
@@ -3,11 +3,11 @@ import type {
|
||||
TranscriptionResult,
|
||||
TranscriptionProgress,
|
||||
TranscriptionModelId,
|
||||
} from "@/lib/transcription/types";
|
||||
} from "@/transcription/types";
|
||||
import {
|
||||
DEFAULT_TRANSCRIPTION_MODEL,
|
||||
TRANSCRIPTION_MODELS,
|
||||
} from "@/lib/transcription/models";
|
||||
} from "@/transcription/models";
|
||||
import type { WorkerMessage, WorkerResponse } from "./worker";
|
||||
|
||||
type ProgressCallback = (progress: TranscriptionProgress) => void;
|
||||
|
||||
@@ -3,11 +3,11 @@ import {
|
||||
type AutomaticSpeechRecognitionPipeline,
|
||||
type AutomaticSpeechRecognitionOutput,
|
||||
} from "@huggingface/transformers";
|
||||
import type { TranscriptionSegment } from "@/lib/transcription/types";
|
||||
import type { TranscriptionSegment } from "@/transcription/types";
|
||||
import {
|
||||
DEFAULT_CHUNK_LENGTH_SECONDS,
|
||||
DEFAULT_STRIDE_SECONDS,
|
||||
} from "@/lib/transcription/audio";
|
||||
} from "@/transcription/audio";
|
||||
|
||||
export type WorkerMessage =
|
||||
| { type: "init"; modelId: string }
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { createAudioContext } from "@/lib/media/audio";
|
||||
import { createAudioContext } from "@/media/audio";
|
||||
import {
|
||||
buildSourceWaveformSummary,
|
||||
type SourceWaveformSummary,
|
||||
} from "@/lib/media/waveform-summary";
|
||||
} from "@/media/waveform-summary";
|
||||
|
||||
interface GetSourceWaveformSummaryArgs {
|
||||
sourceKey: string;
|
||||
|
||||
Reference in New Issue
Block a user