@umituz/react-native-video-editor 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -0
- package/package.json +48 -0
- package/src/domain/entities/index.ts +50 -0
- package/src/domain/entities/video-project.types.ts +153 -0
- package/src/index.ts +100 -0
- package/src/infrastructure/constants/animation-layer.constants.ts +32 -0
- package/src/infrastructure/constants/audio-layer.constants.ts +14 -0
- package/src/infrastructure/constants/export.constants.ts +28 -0
- package/src/infrastructure/constants/image-layer.constants.ts +12 -0
- package/src/infrastructure/constants/index.ts +11 -0
- package/src/infrastructure/constants/shape-layer.constants.ts +29 -0
- package/src/infrastructure/constants/text-layer.constants.ts +40 -0
- package/src/infrastructure/services/export-orchestrator.service.ts +122 -0
- package/src/infrastructure/services/image-layer-operations.service.ts +108 -0
- package/src/infrastructure/services/layer-manipulation.service.ts +93 -0
- package/src/infrastructure/services/layer-operations/index.ts +9 -0
- package/src/infrastructure/services/layer-operations/layer-delete.service.ts +47 -0
- package/src/infrastructure/services/layer-operations/layer-duplicate.service.ts +66 -0
- package/src/infrastructure/services/layer-operations/layer-order.service.ts +82 -0
- package/src/infrastructure/services/layer-operations/layer-transform.service.ts +160 -0
- package/src/infrastructure/services/layer-operations.service.ts +198 -0
- package/src/infrastructure/services/scene-operations.service.ts +166 -0
- package/src/infrastructure/services/shape-layer-operations.service.ts +65 -0
- package/src/infrastructure/services/text-layer-operations.service.ts +114 -0
- package/src/presentation/components/AnimationEditor.tsx +103 -0
- package/src/presentation/components/AudioEditor.tsx +144 -0
- package/src/presentation/components/DraggableLayer.tsx +110 -0
- package/src/presentation/components/EditorHeader.tsx +107 -0
- package/src/presentation/components/EditorPreviewArea.tsx +221 -0
- package/src/presentation/components/EditorTimeline.tsx +136 -0
- package/src/presentation/components/EditorToolPanel.tsx +180 -0
- package/src/presentation/components/ExportDialog.tsx +135 -0
- package/src/presentation/components/ImageLayerEditor.tsx +95 -0
- package/src/presentation/components/LayerActionsMenu.tsx +197 -0
- package/src/presentation/components/SceneActionsMenu.tsx +69 -0
- package/src/presentation/components/ShapeLayerEditor.tsx +108 -0
- package/src/presentation/components/TextLayerEditor.tsx +104 -0
- package/src/presentation/components/animation-layer/AnimationEditorActions.tsx +104 -0
- package/src/presentation/components/animation-layer/AnimationInfoBanner.tsx +43 -0
- package/src/presentation/components/animation-layer/AnimationTypeSelector.tsx +105 -0
- package/src/presentation/components/animation-layer/index.ts +8 -0
- package/src/presentation/components/audio-layer/AudioEditorActions.tsx +115 -0
- package/src/presentation/components/audio-layer/AudioFileSelector.tsx +126 -0
- package/src/presentation/components/audio-layer/FadeEffectsSelector.tsx +151 -0
- package/src/presentation/components/audio-layer/InfoBanner.tsx +43 -0
- package/src/presentation/components/audio-layer/VolumeSelector.tsx +98 -0
- package/src/presentation/components/audio-layer/index.ts +10 -0
- package/src/presentation/components/draggable-layer/LayerContent.tsx +106 -0
- package/src/presentation/components/draggable-layer/ResizeHandles.tsx +97 -0
- package/src/presentation/components/draggable-layer/index.ts +7 -0
- package/src/presentation/components/export/ExportActions.tsx +101 -0
- package/src/presentation/components/export/ExportInfoBanner.tsx +44 -0
- package/src/presentation/components/export/ExportProgress.tsx +114 -0
- package/src/presentation/components/export/OptionSelectorRow.tsx +101 -0
- package/src/presentation/components/export/ProjectInfoBox.tsx +61 -0
- package/src/presentation/components/export/WatermarkToggle.tsx +87 -0
- package/src/presentation/components/export/index.ts +11 -0
- package/src/presentation/components/image-layer/ImagePreview.tsx +70 -0
- package/src/presentation/components/image-layer/ImageSelectionButtons.tsx +82 -0
- package/src/presentation/components/image-layer/OpacitySelector.tsx +91 -0
- package/src/presentation/components/image-layer/index.ts +8 -0
- package/src/presentation/components/index.ts +17 -0
- package/src/presentation/components/shape-layer/ColorPickerHorizontal.tsx +92 -0
- package/src/presentation/components/shape-layer/ShapePreview.tsx +57 -0
- package/src/presentation/components/shape-layer/ShapeTypeSelector.tsx +102 -0
- package/src/presentation/components/shape-layer/ValueSelector.tsx +106 -0
- package/src/presentation/components/shape-layer/index.ts +9 -0
- package/src/presentation/components/text-layer/ColorPicker.tsx +91 -0
- package/src/presentation/components/text-layer/EditorActions.tsx +95 -0
- package/src/presentation/components/text-layer/FontSizeSelector.tsx +86 -0
- package/src/presentation/components/text-layer/OptionSelector.tsx +98 -0
- package/src/presentation/components/text-layer/TextAlignSelector.tsx +87 -0
- package/src/presentation/components/text-layer/TextInputSection.tsx +70 -0
- package/src/presentation/components/text-layer/TextPreview.tsx +71 -0
- package/src/presentation/components/text-layer/index.ts +12 -0
- package/src/presentation/hooks/useAnimationLayerForm.ts +72 -0
- package/src/presentation/hooks/useAudioLayerForm.ts +76 -0
- package/src/presentation/hooks/useDraggableLayerGestures.ts +166 -0
- package/src/presentation/hooks/useEditorActions.tsx +93 -0
- package/src/presentation/hooks/useEditorBottomSheet.ts +43 -0
- package/src/presentation/hooks/useEditorHistory.ts +80 -0
- package/src/presentation/hooks/useEditorLayers.ts +97 -0
- package/src/presentation/hooks/useEditorPlayback.ts +90 -0
- package/src/presentation/hooks/useEditorScenes.ts +106 -0
- package/src/presentation/hooks/useExport.ts +67 -0
- package/src/presentation/hooks/useExportActions.tsx +51 -0
- package/src/presentation/hooks/useExportForm.ts +96 -0
- package/src/presentation/hooks/useImageLayerForm.ts +57 -0
- package/src/presentation/hooks/useImageLayerOperations.ts +71 -0
- package/src/presentation/hooks/useLayerActions.tsx +162 -0
- package/src/presentation/hooks/useLayerManipulation.ts +178 -0
- package/src/presentation/hooks/useMenuActions.tsx +92 -0
- package/src/presentation/hooks/useSceneActions.tsx +81 -0
- package/src/presentation/hooks/useShapeLayerForm.ts +84 -0
- package/src/presentation/hooks/useShapeLayerOperations.ts +52 -0
- package/src/presentation/hooks/useTextLayerForm.ts +100 -0
- package/src/presentation/hooks/useTextLayerOperations.ts +74 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useExportForm Hook
|
|
3
|
+
* Manages form state for export dialog
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback, useMemo } from "react";
|
|
7
|
+
import type { ExportSettings, VideoProject } from "@domains/video";
|
|
8
|
+
import type {
|
|
9
|
+
Resolution,
|
|
10
|
+
Quality,
|
|
11
|
+
Format,
|
|
12
|
+
} from "../constants/export.constants";
|
|
13
|
+
import {
|
|
14
|
+
BASE_SIZE_PER_SECOND,
|
|
15
|
+
RESOLUTION_MULTIPLIERS,
|
|
16
|
+
QUALITY_MULTIPLIERS,
|
|
17
|
+
} from "../constants/export.constants";
|
|
18
|
+
|
|
19
|
+
export interface ExportFormState {
|
|
20
|
+
resolution: Resolution;
|
|
21
|
+
quality: Quality;
|
|
22
|
+
format: Format;
|
|
23
|
+
includeWatermark: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface UseExportFormReturn {
|
|
27
|
+
formState: ExportFormState;
|
|
28
|
+
setResolution: (resolution: Resolution) => void;
|
|
29
|
+
setQuality: (quality: Quality) => void;
|
|
30
|
+
setFormat: (format: Format) => void;
|
|
31
|
+
setIncludeWatermark: (include: boolean) => void;
|
|
32
|
+
buildExportSettings: () => ExportSettings;
|
|
33
|
+
estimatedSize: string;
|
|
34
|
+
projectDuration: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Hook for managing export form state
|
|
39
|
+
*/
|
|
40
|
+
export function useExportForm(project: VideoProject): UseExportFormReturn {
|
|
41
|
+
const [formState, setFormState] = useState<ExportFormState>({
|
|
42
|
+
resolution: "1080p",
|
|
43
|
+
quality: "high",
|
|
44
|
+
format: "mp4",
|
|
45
|
+
includeWatermark: false,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const projectDuration = useMemo(() => {
|
|
49
|
+
return (
|
|
50
|
+
project.scenes.reduce((acc, scene) => acc + scene.duration, 0) / 1000
|
|
51
|
+
);
|
|
52
|
+
}, [project.scenes]);
|
|
53
|
+
|
|
54
|
+
const estimatedSize = useMemo(() => {
|
|
55
|
+
const baseSize = projectDuration * BASE_SIZE_PER_SECOND;
|
|
56
|
+
const resolutionMultiplier = RESOLUTION_MULTIPLIERS[formState.resolution];
|
|
57
|
+
const qualityMultiplier = QUALITY_MULTIPLIERS[formState.quality];
|
|
58
|
+
return (baseSize * resolutionMultiplier * qualityMultiplier).toFixed(1);
|
|
59
|
+
}, [projectDuration, formState.resolution, formState.quality]);
|
|
60
|
+
|
|
61
|
+
const setResolution = useCallback((resolution: Resolution) => {
|
|
62
|
+
setFormState((prev) => ({ ...prev, resolution }));
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
const setQuality = useCallback((quality: Quality) => {
|
|
66
|
+
setFormState((prev) => ({ ...prev, quality }));
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
const setFormat = useCallback((format: Format) => {
|
|
70
|
+
setFormState((prev) => ({ ...prev, format }));
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
const setIncludeWatermark = useCallback((include: boolean) => {
|
|
74
|
+
setFormState((prev) => ({ ...prev, includeWatermark: include }));
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
const buildExportSettings = useCallback((): ExportSettings => {
|
|
78
|
+
return {
|
|
79
|
+
resolution: formState.resolution,
|
|
80
|
+
quality: formState.quality,
|
|
81
|
+
format: formState.format,
|
|
82
|
+
includeWatermark: formState.includeWatermark,
|
|
83
|
+
};
|
|
84
|
+
}, [formState]);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
formState,
|
|
88
|
+
setResolution,
|
|
89
|
+
setQuality,
|
|
90
|
+
setFormat,
|
|
91
|
+
setIncludeWatermark,
|
|
92
|
+
buildExportSettings,
|
|
93
|
+
estimatedSize,
|
|
94
|
+
projectDuration,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useImageLayerForm Hook
|
|
3
|
+
* Manages form state for image layer editor
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback } from "react";
|
|
7
|
+
import type { ImageLayer } from "@domains/video";
|
|
8
|
+
|
|
9
|
+
export interface ImageLayerFormState {
|
|
10
|
+
imageUri: string;
|
|
11
|
+
opacity: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface UseImageLayerFormReturn {
|
|
15
|
+
formState: ImageLayerFormState;
|
|
16
|
+
setImageUri: (uri: string) => void;
|
|
17
|
+
setOpacity: (opacity: number) => void;
|
|
18
|
+
buildLayerData: () => Partial<ImageLayer>;
|
|
19
|
+
isValid: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Hook for managing image layer form state
|
|
24
|
+
*/
|
|
25
|
+
export function useImageLayerForm(
|
|
26
|
+
initialLayer?: ImageLayer,
|
|
27
|
+
): UseImageLayerFormReturn {
|
|
28
|
+
const [formState, setFormState] = useState<ImageLayerFormState>({
|
|
29
|
+
imageUri: initialLayer?.uri || "",
|
|
30
|
+
opacity: initialLayer?.opacity || 1,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const setImageUri = useCallback((uri: string) => {
|
|
34
|
+
setFormState((prev) => ({ ...prev, imageUri: uri }));
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
const setOpacity = useCallback((opacity: number) => {
|
|
38
|
+
setFormState((prev) => ({ ...prev, opacity }));
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
const buildLayerData = useCallback((): Partial<ImageLayer> => {
|
|
42
|
+
return {
|
|
43
|
+
uri: formState.imageUri,
|
|
44
|
+
opacity: formState.opacity,
|
|
45
|
+
};
|
|
46
|
+
}, [formState]);
|
|
47
|
+
|
|
48
|
+
const isValid = formState.imageUri.length > 0;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
formState,
|
|
52
|
+
setImageUri,
|
|
53
|
+
setOpacity,
|
|
54
|
+
buildLayerData,
|
|
55
|
+
isValid,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useImageLayerOperations Hook
|
|
3
|
+
* Single Responsibility: Image layer operations (add, edit)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback } from "react";
|
|
7
|
+
import { Alert } from "react-native";
|
|
8
|
+
import { layerOperationsService } from "../infrastructure/services/layer-operations.service";
|
|
9
|
+
import type { AddImageLayerData } from "../types";
|
|
10
|
+
import type { ImageLayer } from "@domains/video";
|
|
11
|
+
|
|
12
|
+
export interface UseImageLayerOperationsParams {
|
|
13
|
+
scenes: any[];
|
|
14
|
+
sceneIndex: number;
|
|
15
|
+
onUpdateScenes: (scenes: any[]) => void;
|
|
16
|
+
onCloseBottomSheet: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface UseImageLayerOperationsReturn {
|
|
20
|
+
addImageLayer: (data: AddImageLayerData) => void;
|
|
21
|
+
editImageLayer: (layerId: string, data: Partial<ImageLayer>) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useImageLayerOperations({
|
|
25
|
+
scenes,
|
|
26
|
+
sceneIndex,
|
|
27
|
+
onUpdateScenes,
|
|
28
|
+
onCloseBottomSheet,
|
|
29
|
+
}: UseImageLayerOperationsParams): UseImageLayerOperationsReturn {
|
|
30
|
+
const addImageLayer = useCallback(
|
|
31
|
+
(data: AddImageLayerData) => {
|
|
32
|
+
const result = layerOperationsService.addImageLayer(
|
|
33
|
+
scenes,
|
|
34
|
+
sceneIndex,
|
|
35
|
+
data,
|
|
36
|
+
);
|
|
37
|
+
if (result.success) {
|
|
38
|
+
onUpdateScenes(result.updatedScenes);
|
|
39
|
+
onCloseBottomSheet();
|
|
40
|
+
Alert.alert("Success", "Image layer added!");
|
|
41
|
+
} else {
|
|
42
|
+
Alert.alert("Error", result.error || "Failed to add image layer");
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
[scenes, sceneIndex, onUpdateScenes, onCloseBottomSheet],
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const editImageLayer = useCallback(
|
|
49
|
+
(layerId: string, data: Partial<ImageLayer>) => {
|
|
50
|
+
const result = layerOperationsService.editImageLayer(
|
|
51
|
+
scenes,
|
|
52
|
+
sceneIndex,
|
|
53
|
+
layerId,
|
|
54
|
+
data,
|
|
55
|
+
);
|
|
56
|
+
if (result.success) {
|
|
57
|
+
onUpdateScenes(result.updatedScenes);
|
|
58
|
+
onCloseBottomSheet();
|
|
59
|
+
Alert.alert("Success", "Image layer updated!");
|
|
60
|
+
} else {
|
|
61
|
+
Alert.alert("Error", result.error || "Failed to update image layer");
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
[scenes, sceneIndex, onUpdateScenes, onCloseBottomSheet],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
addImageLayer,
|
|
69
|
+
editImageLayer,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useLayerActions Hook
|
|
3
|
+
* Single Responsibility: Layer action handlers (add, edit, animate)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback } from "react";
|
|
7
|
+
import { TextLayerEditor } from "../presentation/components/TextLayerEditor";
|
|
8
|
+
import { ImageLayerEditor } from "../presentation/components/ImageLayerEditor";
|
|
9
|
+
import { ShapeLayerEditor } from "../presentation/components/ShapeLayerEditor";
|
|
10
|
+
import { AnimationEditor } from "../presentation/components/AnimationEditor";
|
|
11
|
+
import type { ImageLayer } from "@domains/video";
|
|
12
|
+
import type { UseEditorLayersReturn } from "./useEditorLayers";
|
|
13
|
+
import type { UseEditorBottomSheetReturn } from "./useEditorBottomSheet";
|
|
14
|
+
|
|
15
|
+
export interface UseLayerActionsParams {
|
|
16
|
+
selectedLayerId: string | null;
|
|
17
|
+
currentScene: any;
|
|
18
|
+
layers: UseEditorLayersReturn;
|
|
19
|
+
bottomSheet: UseEditorBottomSheetReturn;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UseLayerActionsReturn {
|
|
23
|
+
handleAddText: () => void;
|
|
24
|
+
handleEditLayer: () => void;
|
|
25
|
+
handleAddImage: () => void;
|
|
26
|
+
handleEditImageLayer: (layerId: string) => void;
|
|
27
|
+
handleAddShape: () => void;
|
|
28
|
+
handleAnimate: (layerId: string) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function useLayerActions({
|
|
32
|
+
selectedLayerId,
|
|
33
|
+
currentScene,
|
|
34
|
+
layers,
|
|
35
|
+
bottomSheet,
|
|
36
|
+
}: UseLayerActionsParams): UseLayerActionsReturn {
|
|
37
|
+
const { openBottomSheet, closeBottomSheet } = bottomSheet;
|
|
38
|
+
|
|
39
|
+
const handleAddText = useCallback(() => {
|
|
40
|
+
openBottomSheet({
|
|
41
|
+
title: "Add Text Layer",
|
|
42
|
+
children: (
|
|
43
|
+
<TextLayerEditor
|
|
44
|
+
onSave={layers.addTextLayer}
|
|
45
|
+
onCancel={closeBottomSheet}
|
|
46
|
+
/>
|
|
47
|
+
),
|
|
48
|
+
});
|
|
49
|
+
}, [layers.addTextLayer, openBottomSheet, closeBottomSheet]);
|
|
50
|
+
|
|
51
|
+
const handleEditLayer = useCallback(() => {
|
|
52
|
+
if (!selectedLayerId || !currentScene) return;
|
|
53
|
+
const layer = currentScene.layers.find(
|
|
54
|
+
(l: any) => l.id === selectedLayerId,
|
|
55
|
+
);
|
|
56
|
+
if (!layer || layer.type !== "text") return;
|
|
57
|
+
|
|
58
|
+
openBottomSheet({
|
|
59
|
+
title: "Edit Text Layer",
|
|
60
|
+
children: (
|
|
61
|
+
<TextLayerEditor
|
|
62
|
+
layer={layer}
|
|
63
|
+
onSave={(data) => layers.editTextLayer(selectedLayerId, data)}
|
|
64
|
+
onCancel={closeBottomSheet}
|
|
65
|
+
/>
|
|
66
|
+
),
|
|
67
|
+
});
|
|
68
|
+
}, [
|
|
69
|
+
selectedLayerId,
|
|
70
|
+
currentScene,
|
|
71
|
+
layers.editTextLayer,
|
|
72
|
+
openBottomSheet,
|
|
73
|
+
closeBottomSheet,
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
const handleAddImage = useCallback(() => {
|
|
77
|
+
openBottomSheet({
|
|
78
|
+
title: "Add Image Layer",
|
|
79
|
+
children: (
|
|
80
|
+
<ImageLayerEditor
|
|
81
|
+
onSave={layers.addImageLayer}
|
|
82
|
+
onCancel={closeBottomSheet}
|
|
83
|
+
/>
|
|
84
|
+
),
|
|
85
|
+
});
|
|
86
|
+
}, [layers.addImageLayer, openBottomSheet, closeBottomSheet]);
|
|
87
|
+
|
|
88
|
+
const handleEditImageLayer = useCallback(
|
|
89
|
+
(layerId: string) => {
|
|
90
|
+
if (!currentScene) return;
|
|
91
|
+
const layer = currentScene.layers.find((l: any) => l.id === layerId) as
|
|
92
|
+
| ImageLayer
|
|
93
|
+
| undefined;
|
|
94
|
+
if (!layer) return;
|
|
95
|
+
|
|
96
|
+
openBottomSheet({
|
|
97
|
+
title: "Edit Image Layer",
|
|
98
|
+
children: (
|
|
99
|
+
<ImageLayerEditor
|
|
100
|
+
layer={layer}
|
|
101
|
+
onSave={(data) => layers.editImageLayer(layerId, data)}
|
|
102
|
+
onCancel={closeBottomSheet}
|
|
103
|
+
/>
|
|
104
|
+
),
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
[currentScene, layers.editImageLayer, openBottomSheet, closeBottomSheet],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const handleAddShape = useCallback(() => {
|
|
111
|
+
openBottomSheet({
|
|
112
|
+
title: "Add Shape Layer",
|
|
113
|
+
children: (
|
|
114
|
+
<ShapeLayerEditor
|
|
115
|
+
onSave={layers.addShapeLayer}
|
|
116
|
+
onCancel={closeBottomSheet}
|
|
117
|
+
/>
|
|
118
|
+
),
|
|
119
|
+
});
|
|
120
|
+
}, [layers.addShapeLayer, openBottomSheet, closeBottomSheet]);
|
|
121
|
+
|
|
122
|
+
const handleAnimate = useCallback(
|
|
123
|
+
(layerId: string) => {
|
|
124
|
+
if (!currentScene) return;
|
|
125
|
+
const layer = currentScene.layers.find((l: any) => l.id === layerId);
|
|
126
|
+
if (!layer) return;
|
|
127
|
+
|
|
128
|
+
openBottomSheet({
|
|
129
|
+
title: layer.animation ? "Edit Animation" : "Add Animation",
|
|
130
|
+
children: (
|
|
131
|
+
<AnimationEditor
|
|
132
|
+
animation={layer.animation}
|
|
133
|
+
onSave={(animation) =>
|
|
134
|
+
layers.updateLayerAnimation(layerId, animation)
|
|
135
|
+
}
|
|
136
|
+
onRemove={
|
|
137
|
+
layer.animation
|
|
138
|
+
? () => layers.updateLayerAnimation(layerId, undefined)
|
|
139
|
+
: undefined
|
|
140
|
+
}
|
|
141
|
+
onCancel={closeBottomSheet}
|
|
142
|
+
/>
|
|
143
|
+
),
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
[
|
|
147
|
+
currentScene,
|
|
148
|
+
layers.updateLayerAnimation,
|
|
149
|
+
openBottomSheet,
|
|
150
|
+
closeBottomSheet,
|
|
151
|
+
],
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
handleAddText,
|
|
156
|
+
handleEditLayer,
|
|
157
|
+
handleAddImage,
|
|
158
|
+
handleEditImageLayer,
|
|
159
|
+
handleAddShape,
|
|
160
|
+
handleAnimate,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useLayerManipulation Hook
|
|
3
|
+
* Single Responsibility: Layer manipulation operations (delete, order, duplicate, position, size, animation)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback } from "react";
|
|
7
|
+
import { Alert } from "react-native";
|
|
8
|
+
import { layerOperationsService } from "../infrastructure/services/layer-operations.service";
|
|
9
|
+
import type { LayerOrderAction } from "../types";
|
|
10
|
+
import type { Animation } from "@domains/video";
|
|
11
|
+
|
|
12
|
+
export interface UseLayerManipulationParams {
|
|
13
|
+
scenes: any[];
|
|
14
|
+
sceneIndex: number;
|
|
15
|
+
onUpdateScenes: (scenes: any[]) => void;
|
|
16
|
+
onCloseBottomSheet: () => void;
|
|
17
|
+
onLayerDeleted?: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface UseLayerManipulationReturn {
|
|
21
|
+
deleteLayer: (layerId: string) => void;
|
|
22
|
+
changeLayerOrder: (layerId: string, action: LayerOrderAction) => void;
|
|
23
|
+
duplicateLayer: (layerId: string) => void;
|
|
24
|
+
updateLayerPosition: (layerId: string, x: number, y: number) => void;
|
|
25
|
+
updateLayerSize: (layerId: string, width: number, height: number) => void;
|
|
26
|
+
updateLayerAnimation: (
|
|
27
|
+
layerId: string,
|
|
28
|
+
animation: Animation | undefined,
|
|
29
|
+
) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function useLayerManipulation({
|
|
33
|
+
scenes,
|
|
34
|
+
sceneIndex,
|
|
35
|
+
onUpdateScenes,
|
|
36
|
+
onCloseBottomSheet,
|
|
37
|
+
onLayerDeleted,
|
|
38
|
+
}: UseLayerManipulationParams): UseLayerManipulationReturn {
|
|
39
|
+
const deleteLayer = useCallback(
|
|
40
|
+
(layerId: string) => {
|
|
41
|
+
Alert.alert(
|
|
42
|
+
"Delete Layer",
|
|
43
|
+
"Are you sure you want to delete this layer?",
|
|
44
|
+
[
|
|
45
|
+
{ text: "Cancel", style: "cancel" },
|
|
46
|
+
{
|
|
47
|
+
text: "Delete",
|
|
48
|
+
style: "destructive",
|
|
49
|
+
onPress: () => {
|
|
50
|
+
const result = layerOperationsService.deleteLayer(
|
|
51
|
+
scenes,
|
|
52
|
+
sceneIndex,
|
|
53
|
+
layerId,
|
|
54
|
+
);
|
|
55
|
+
if (result.success) {
|
|
56
|
+
onUpdateScenes(result.updatedScenes);
|
|
57
|
+
onLayerDeleted?.();
|
|
58
|
+
Alert.alert("Success", "Layer deleted");
|
|
59
|
+
} else {
|
|
60
|
+
Alert.alert("Error", result.error || "Failed to delete layer");
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
[scenes, sceneIndex, onUpdateScenes, onLayerDeleted],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const changeLayerOrder = useCallback(
|
|
71
|
+
(layerId: string, action: LayerOrderAction) => {
|
|
72
|
+
const result = layerOperationsService.changeLayerOrder(
|
|
73
|
+
scenes,
|
|
74
|
+
sceneIndex,
|
|
75
|
+
layerId,
|
|
76
|
+
action,
|
|
77
|
+
);
|
|
78
|
+
if (result.success) {
|
|
79
|
+
onUpdateScenes(result.updatedScenes);
|
|
80
|
+
const actionNames = {
|
|
81
|
+
front: "Layer moved to front",
|
|
82
|
+
back: "Layer moved to back",
|
|
83
|
+
up: "Layer moved up",
|
|
84
|
+
down: "Layer moved down",
|
|
85
|
+
};
|
|
86
|
+
Alert.alert("Success", actionNames[action]);
|
|
87
|
+
} else {
|
|
88
|
+
Alert.alert("Error", result.error || "Failed to change layer order");
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
[scenes, sceneIndex, onUpdateScenes],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const duplicateLayer = useCallback(
|
|
95
|
+
(layerId: string) => {
|
|
96
|
+
const result = layerOperationsService.duplicateLayer(
|
|
97
|
+
scenes,
|
|
98
|
+
sceneIndex,
|
|
99
|
+
layerId,
|
|
100
|
+
);
|
|
101
|
+
if (result.success) {
|
|
102
|
+
onUpdateScenes(result.updatedScenes);
|
|
103
|
+
Alert.alert("Success", "Layer duplicated!");
|
|
104
|
+
} else {
|
|
105
|
+
Alert.alert("Error", result.error || "Failed to duplicate layer");
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
[scenes, sceneIndex, onUpdateScenes],
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const updateLayerPosition = useCallback(
|
|
112
|
+
(layerId: string, x: number, y: number) => {
|
|
113
|
+
const result = layerOperationsService.updateLayerPosition(
|
|
114
|
+
scenes,
|
|
115
|
+
sceneIndex,
|
|
116
|
+
layerId,
|
|
117
|
+
x,
|
|
118
|
+
y,
|
|
119
|
+
);
|
|
120
|
+
if (result.success) {
|
|
121
|
+
onUpdateScenes(result.updatedScenes);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
[scenes, sceneIndex, onUpdateScenes],
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const updateLayerSize = useCallback(
|
|
128
|
+
(layerId: string, width: number, height: number) => {
|
|
129
|
+
const result = layerOperationsService.updateLayerSize(
|
|
130
|
+
scenes,
|
|
131
|
+
sceneIndex,
|
|
132
|
+
layerId,
|
|
133
|
+
width,
|
|
134
|
+
height,
|
|
135
|
+
);
|
|
136
|
+
if (result.success) {
|
|
137
|
+
onUpdateScenes(result.updatedScenes);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
[scenes, sceneIndex, onUpdateScenes],
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const updateLayerAnimation = useCallback(
|
|
144
|
+
(layerId: string, animation: Animation | undefined) => {
|
|
145
|
+
const result = layerOperationsService.updateLayerAnimation(
|
|
146
|
+
scenes,
|
|
147
|
+
sceneIndex,
|
|
148
|
+
layerId,
|
|
149
|
+
animation,
|
|
150
|
+
);
|
|
151
|
+
if (result.success) {
|
|
152
|
+
onUpdateScenes(result.updatedScenes);
|
|
153
|
+
onCloseBottomSheet();
|
|
154
|
+
Alert.alert(
|
|
155
|
+
"Success",
|
|
156
|
+
animation
|
|
157
|
+
? "Animation applied to layer!"
|
|
158
|
+
: "Animation removed from layer",
|
|
159
|
+
);
|
|
160
|
+
} else {
|
|
161
|
+
Alert.alert(
|
|
162
|
+
"Error",
|
|
163
|
+
result.error || "Failed to update layer animation",
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
[scenes, sceneIndex, onUpdateScenes, onCloseBottomSheet],
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
deleteLayer,
|
|
172
|
+
changeLayerOrder,
|
|
173
|
+
duplicateLayer,
|
|
174
|
+
updateLayerPosition,
|
|
175
|
+
updateLayerSize,
|
|
176
|
+
updateLayerAnimation,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useMenuActions Hook
|
|
3
|
+
* Single Responsibility: Menu action handlers (layer actions menu)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback } from "react";
|
|
7
|
+
import { LayerActionsMenu } from "../presentation/components/LayerActionsMenu";
|
|
8
|
+
import type { UseEditorLayersReturn } from "./useEditorLayers";
|
|
9
|
+
import type { UseEditorBottomSheetReturn } from "./useEditorBottomSheet";
|
|
10
|
+
|
|
11
|
+
export interface UseMenuActionsParams {
|
|
12
|
+
layers: UseEditorLayersReturn;
|
|
13
|
+
bottomSheet: UseEditorBottomSheetReturn;
|
|
14
|
+
handleEditLayer: () => void;
|
|
15
|
+
handleEditImageLayer: (layerId: string) => void;
|
|
16
|
+
handleAnimate: (layerId: string) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface UseMenuActionsReturn {
|
|
20
|
+
handleLayerActionsPress: (layer: any) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useMenuActions({
|
|
24
|
+
layers,
|
|
25
|
+
bottomSheet,
|
|
26
|
+
handleEditLayer,
|
|
27
|
+
handleEditImageLayer,
|
|
28
|
+
handleAnimate,
|
|
29
|
+
}: UseMenuActionsParams): UseMenuActionsReturn {
|
|
30
|
+
const { openBottomSheet, closeBottomSheet } = bottomSheet;
|
|
31
|
+
|
|
32
|
+
const handleLayerActionsPress = useCallback(
|
|
33
|
+
(layer: any) => {
|
|
34
|
+
openBottomSheet({
|
|
35
|
+
title: "Layer Actions",
|
|
36
|
+
children: (
|
|
37
|
+
<LayerActionsMenu
|
|
38
|
+
layer={layer}
|
|
39
|
+
onEditText={() => {
|
|
40
|
+
closeBottomSheet();
|
|
41
|
+
setTimeout(() => handleEditLayer(), 300);
|
|
42
|
+
}}
|
|
43
|
+
onEditImage={() => {
|
|
44
|
+
closeBottomSheet();
|
|
45
|
+
setTimeout(() => handleEditImageLayer(layer.id), 300);
|
|
46
|
+
}}
|
|
47
|
+
onAnimate={() => {
|
|
48
|
+
closeBottomSheet();
|
|
49
|
+
setTimeout(() => handleAnimate(layer.id), 300);
|
|
50
|
+
}}
|
|
51
|
+
onDuplicate={() => {
|
|
52
|
+
closeBottomSheet();
|
|
53
|
+
layers.duplicateLayer(layer.id);
|
|
54
|
+
}}
|
|
55
|
+
onMoveFront={() => {
|
|
56
|
+
closeBottomSheet();
|
|
57
|
+
layers.changeLayerOrder(layer.id, "front");
|
|
58
|
+
}}
|
|
59
|
+
onMoveUp={() => {
|
|
60
|
+
closeBottomSheet();
|
|
61
|
+
layers.changeLayerOrder(layer.id, "up");
|
|
62
|
+
}}
|
|
63
|
+
onMoveDown={() => {
|
|
64
|
+
closeBottomSheet();
|
|
65
|
+
layers.changeLayerOrder(layer.id, "down");
|
|
66
|
+
}}
|
|
67
|
+
onMoveBack={() => {
|
|
68
|
+
closeBottomSheet();
|
|
69
|
+
layers.changeLayerOrder(layer.id, "back");
|
|
70
|
+
}}
|
|
71
|
+
onDelete={() => {
|
|
72
|
+
closeBottomSheet();
|
|
73
|
+
layers.deleteLayer(layer.id);
|
|
74
|
+
}}
|
|
75
|
+
/>
|
|
76
|
+
),
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
[
|
|
80
|
+
layers,
|
|
81
|
+
handleEditLayer,
|
|
82
|
+
handleEditImageLayer,
|
|
83
|
+
handleAnimate,
|
|
84
|
+
openBottomSheet,
|
|
85
|
+
closeBottomSheet,
|
|
86
|
+
],
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
handleLayerActionsPress,
|
|
91
|
+
};
|
|
92
|
+
}
|