@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,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export Orchestrator Service
|
|
3
|
+
* Handles export business logic and notifications
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as Device from "expo-device";
|
|
7
|
+
import { Alert } from "react-native";
|
|
8
|
+
import { notificationService } from "@umituz/react-native-notifications";
|
|
9
|
+
import {
|
|
10
|
+
videoExportService,
|
|
11
|
+
ExportProgress,
|
|
12
|
+
} from "@domains/video/infrastructure/services/video-export.service";
|
|
13
|
+
import type { ExportSettings, VideoProject } from "@domains/video";
|
|
14
|
+
|
|
15
|
+
export interface ExportResult {
|
|
16
|
+
success: boolean;
|
|
17
|
+
uri?: string;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ExportOrchestratorService {
|
|
22
|
+
exportVideo: (
|
|
23
|
+
project: VideoProject,
|
|
24
|
+
settings: ExportSettings,
|
|
25
|
+
onProgress: (progress: ExportProgress) => void,
|
|
26
|
+
) => Promise<ExportResult>;
|
|
27
|
+
requestNotificationPermissions: () => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class ExportOrchestratorServiceImpl implements ExportOrchestratorService {
|
|
31
|
+
async requestNotificationPermissions(): Promise<void> {
|
|
32
|
+
if (Device.isDevice) {
|
|
33
|
+
await notificationService.requestPermissions();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async exportVideo(
|
|
38
|
+
project: VideoProject,
|
|
39
|
+
settings: ExportSettings,
|
|
40
|
+
onProgress: (progress: ExportProgress) => void,
|
|
41
|
+
): Promise<ExportResult> {
|
|
42
|
+
const projectToExport: VideoProject = {
|
|
43
|
+
...project,
|
|
44
|
+
exportSettings: settings,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const result = await videoExportService.exportVideo(
|
|
49
|
+
projectToExport,
|
|
50
|
+
onProgress,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (result.success) {
|
|
54
|
+
await this.showSuccessNotification(project.title, result.uri);
|
|
55
|
+
this.showSuccessAlert();
|
|
56
|
+
return { success: true, uri: result.uri };
|
|
57
|
+
} else {
|
|
58
|
+
await this.showFailureNotification(project.title, result.error);
|
|
59
|
+
this.showFailureAlert(result.error);
|
|
60
|
+
return { success: false, error: result.error };
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
await this.showFailureNotification(
|
|
64
|
+
project.title,
|
|
65
|
+
"An unexpected error occurred",
|
|
66
|
+
);
|
|
67
|
+
this.showFailureAlert("An unexpected error occurred");
|
|
68
|
+
return {
|
|
69
|
+
success: false,
|
|
70
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private async showSuccessNotification(
|
|
76
|
+
projectTitle: string,
|
|
77
|
+
uri?: string,
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
if (!Device.isDevice) return;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
await notificationService.notifications.scheduleNotification({
|
|
83
|
+
title: "Export Complete",
|
|
84
|
+
body: `${projectTitle} has been exported successfully!`,
|
|
85
|
+
trigger: { type: "date", date: new Date() },
|
|
86
|
+
data: { uri: uri || "" },
|
|
87
|
+
});
|
|
88
|
+
} catch (error) {
|
|
89
|
+
// Silent failure - notification is optional
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private async showFailureNotification(
|
|
94
|
+
projectTitle: string,
|
|
95
|
+
error?: string,
|
|
96
|
+
): Promise<void> {
|
|
97
|
+
if (!Device.isDevice) return;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
await notificationService.notifications.scheduleNotification({
|
|
101
|
+
title: "Export Failed",
|
|
102
|
+
body: `Failed to export ${projectTitle}: ${error || "Unknown error"}`,
|
|
103
|
+
trigger: { type: "date", date: new Date() },
|
|
104
|
+
});
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// Silent failure - notification is optional
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private showSuccessAlert(): void {
|
|
111
|
+
Alert.alert(
|
|
112
|
+
"Export Complete",
|
|
113
|
+
"Your video has been exported successfully!",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private showFailureAlert(error?: string): void {
|
|
118
|
+
Alert.alert("Export Failed", error || "An error occurred during export");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const exportOrchestratorService = new ExportOrchestratorServiceImpl();
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Layer Operations Service
|
|
3
|
+
* Single Responsibility: Image layer business logic
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { generateUUID } from "@umituz/react-native-uuid";
|
|
7
|
+
import type { Scene, ImageLayer } from "@domains/video";
|
|
8
|
+
import type { LayerOperationResult, AddImageLayerData } from "../../types";
|
|
9
|
+
|
|
10
|
+
class ImageLayerOperationsService {
|
|
11
|
+
/**
|
|
12
|
+
* Add image layer to scene
|
|
13
|
+
*/
|
|
14
|
+
addImageLayer(
|
|
15
|
+
scenes: Scene[],
|
|
16
|
+
sceneIndex: number,
|
|
17
|
+
layerData: AddImageLayerData,
|
|
18
|
+
): LayerOperationResult {
|
|
19
|
+
try {
|
|
20
|
+
if (sceneIndex < 0 || sceneIndex >= scenes.length) {
|
|
21
|
+
return {
|
|
22
|
+
success: false,
|
|
23
|
+
updatedScenes: scenes,
|
|
24
|
+
error: "Invalid scene index",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const newLayer: ImageLayer = {
|
|
29
|
+
id: generateUUID(),
|
|
30
|
+
type: "image",
|
|
31
|
+
uri: layerData.uri || "",
|
|
32
|
+
position: { x: 15, y: 30 },
|
|
33
|
+
size: { width: 70, height: 40 },
|
|
34
|
+
rotation: 0,
|
|
35
|
+
opacity: layerData.opacity || 1,
|
|
36
|
+
animation: {
|
|
37
|
+
type: "fade",
|
|
38
|
+
duration: 500,
|
|
39
|
+
easing: "ease-in-out",
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const updatedScenes = [...scenes];
|
|
44
|
+
updatedScenes[sceneIndex] = {
|
|
45
|
+
...updatedScenes[sceneIndex],
|
|
46
|
+
layers: [...updatedScenes[sceneIndex].layers, newLayer],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return { success: true, updatedScenes };
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return {
|
|
52
|
+
success: false,
|
|
53
|
+
updatedScenes: scenes,
|
|
54
|
+
error:
|
|
55
|
+
error instanceof Error ? error.message : "Failed to add image layer",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Edit image layer
|
|
62
|
+
*/
|
|
63
|
+
editImageLayer(
|
|
64
|
+
scenes: Scene[],
|
|
65
|
+
sceneIndex: number,
|
|
66
|
+
layerId: string,
|
|
67
|
+
layerData: Partial<ImageLayer>,
|
|
68
|
+
): LayerOperationResult {
|
|
69
|
+
try {
|
|
70
|
+
if (sceneIndex < 0 || sceneIndex >= scenes.length) {
|
|
71
|
+
return {
|
|
72
|
+
success: false,
|
|
73
|
+
updatedScenes: scenes,
|
|
74
|
+
error: "Invalid scene index",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const updatedScenes = [...scenes];
|
|
79
|
+
const layerIndex = updatedScenes[sceneIndex].layers.findIndex(
|
|
80
|
+
(l) => l.id === layerId,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (layerIndex === -1) {
|
|
84
|
+
return {
|
|
85
|
+
success: false,
|
|
86
|
+
updatedScenes: scenes,
|
|
87
|
+
error: "Layer not found",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
updatedScenes[sceneIndex].layers[layerIndex] = {
|
|
92
|
+
...updatedScenes[sceneIndex].layers[layerIndex],
|
|
93
|
+
...layerData,
|
|
94
|
+
} as ImageLayer;
|
|
95
|
+
|
|
96
|
+
return { success: true, updatedScenes };
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return {
|
|
99
|
+
success: false,
|
|
100
|
+
updatedScenes: scenes,
|
|
101
|
+
error:
|
|
102
|
+
error instanceof Error ? error.message : "Failed to edit image layer",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const imageLayerOperationsService = new ImageLayerOperationsService();
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer Manipulation Service
|
|
3
|
+
* Orchestrator service that delegates to specialized layer operation services
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Scene, Animation } from "@domains/video";
|
|
7
|
+
import type { LayerOperationResult, LayerOrderAction } from "../../types";
|
|
8
|
+
import {
|
|
9
|
+
layerDeleteService,
|
|
10
|
+
layerOrderService,
|
|
11
|
+
layerDuplicateService,
|
|
12
|
+
layerTransformService,
|
|
13
|
+
} from "./layer-operations";
|
|
14
|
+
|
|
15
|
+
class LayerManipulationService {
|
|
16
|
+
deleteLayer(
|
|
17
|
+
scenes: Scene[],
|
|
18
|
+
sceneIndex: number,
|
|
19
|
+
layerId: string,
|
|
20
|
+
): LayerOperationResult {
|
|
21
|
+
return layerDeleteService.deleteLayer(scenes, sceneIndex, layerId);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
changeLayerOrder(
|
|
25
|
+
scenes: Scene[],
|
|
26
|
+
sceneIndex: number,
|
|
27
|
+
layerId: string,
|
|
28
|
+
action: LayerOrderAction,
|
|
29
|
+
): LayerOperationResult {
|
|
30
|
+
return layerOrderService.changeLayerOrder(
|
|
31
|
+
scenes,
|
|
32
|
+
sceneIndex,
|
|
33
|
+
layerId,
|
|
34
|
+
action,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
duplicateLayer(
|
|
39
|
+
scenes: Scene[],
|
|
40
|
+
sceneIndex: number,
|
|
41
|
+
layerId: string,
|
|
42
|
+
): LayerOperationResult {
|
|
43
|
+
return layerDuplicateService.duplicateLayer(scenes, sceneIndex, layerId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
updateLayerPosition(
|
|
47
|
+
scenes: Scene[],
|
|
48
|
+
sceneIndex: number,
|
|
49
|
+
layerId: string,
|
|
50
|
+
x: number,
|
|
51
|
+
y: number,
|
|
52
|
+
): LayerOperationResult {
|
|
53
|
+
return layerTransformService.updateLayerPosition(
|
|
54
|
+
scenes,
|
|
55
|
+
sceneIndex,
|
|
56
|
+
layerId,
|
|
57
|
+
x,
|
|
58
|
+
y,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
updateLayerSize(
|
|
63
|
+
scenes: Scene[],
|
|
64
|
+
sceneIndex: number,
|
|
65
|
+
layerId: string,
|
|
66
|
+
width: number,
|
|
67
|
+
height: number,
|
|
68
|
+
): LayerOperationResult {
|
|
69
|
+
return layerTransformService.updateLayerSize(
|
|
70
|
+
scenes,
|
|
71
|
+
sceneIndex,
|
|
72
|
+
layerId,
|
|
73
|
+
width,
|
|
74
|
+
height,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
updateLayerAnimation(
|
|
79
|
+
scenes: Scene[],
|
|
80
|
+
sceneIndex: number,
|
|
81
|
+
layerId: string,
|
|
82
|
+
animation: Animation | undefined,
|
|
83
|
+
): LayerOperationResult {
|
|
84
|
+
return layerTransformService.updateLayerAnimation(
|
|
85
|
+
scenes,
|
|
86
|
+
sceneIndex,
|
|
87
|
+
layerId,
|
|
88
|
+
animation,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const layerManipulationService = new LayerManipulationService();
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer Delete Service
|
|
3
|
+
* Single Responsibility: Handle layer deletion operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Scene } from "@domains/video";
|
|
7
|
+
import type { LayerOperationResult } from "../../../types";
|
|
8
|
+
|
|
9
|
+
class LayerDeleteService {
|
|
10
|
+
/**
|
|
11
|
+
* Delete layer from scene
|
|
12
|
+
*/
|
|
13
|
+
deleteLayer(
|
|
14
|
+
scenes: Scene[],
|
|
15
|
+
sceneIndex: number,
|
|
16
|
+
layerId: string,
|
|
17
|
+
): LayerOperationResult {
|
|
18
|
+
try {
|
|
19
|
+
if (sceneIndex < 0 || sceneIndex >= scenes.length) {
|
|
20
|
+
return {
|
|
21
|
+
success: false,
|
|
22
|
+
updatedScenes: scenes,
|
|
23
|
+
error: "Invalid scene index",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const updatedScenes = [...scenes];
|
|
28
|
+
updatedScenes[sceneIndex] = {
|
|
29
|
+
...updatedScenes[sceneIndex],
|
|
30
|
+
layers: updatedScenes[sceneIndex].layers.filter(
|
|
31
|
+
(l) => l.id !== layerId,
|
|
32
|
+
),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return { success: true, updatedScenes };
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return {
|
|
38
|
+
success: false,
|
|
39
|
+
updatedScenes: scenes,
|
|
40
|
+
error:
|
|
41
|
+
error instanceof Error ? error.message : "Failed to delete layer",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const layerDeleteService = new LayerDeleteService();
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer Duplicate Service
|
|
3
|
+
* Single Responsibility: Handle layer duplication operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { generateUUID } from "@umituz/react-native-uuid";
|
|
7
|
+
import type { Scene } from "@domains/video";
|
|
8
|
+
import type { LayerOperationResult } from "../../../types";
|
|
9
|
+
|
|
10
|
+
class LayerDuplicateService {
|
|
11
|
+
/**
|
|
12
|
+
* Duplicate layer
|
|
13
|
+
*/
|
|
14
|
+
duplicateLayer(
|
|
15
|
+
scenes: Scene[],
|
|
16
|
+
sceneIndex: number,
|
|
17
|
+
layerId: string,
|
|
18
|
+
): LayerOperationResult {
|
|
19
|
+
try {
|
|
20
|
+
if (sceneIndex < 0 || sceneIndex >= scenes.length) {
|
|
21
|
+
return {
|
|
22
|
+
success: false,
|
|
23
|
+
updatedScenes: scenes,
|
|
24
|
+
error: "Invalid scene index",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const updatedScenes = [...scenes];
|
|
29
|
+
const layers = updatedScenes[sceneIndex].layers;
|
|
30
|
+
const layerToDuplicate = layers.find((l) => l.id === layerId);
|
|
31
|
+
|
|
32
|
+
if (!layerToDuplicate) {
|
|
33
|
+
return {
|
|
34
|
+
success: false,
|
|
35
|
+
updatedScenes: scenes,
|
|
36
|
+
error: "Layer not found",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const duplicatedLayer = {
|
|
41
|
+
...JSON.parse(JSON.stringify(layerToDuplicate)),
|
|
42
|
+
id: generateUUID(),
|
|
43
|
+
position: {
|
|
44
|
+
x: layerToDuplicate.position.x + 5,
|
|
45
|
+
y: layerToDuplicate.position.y + 5,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
updatedScenes[sceneIndex] = {
|
|
50
|
+
...updatedScenes[sceneIndex],
|
|
51
|
+
layers: [...layers, duplicatedLayer],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return { success: true, updatedScenes };
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return {
|
|
57
|
+
success: false,
|
|
58
|
+
updatedScenes: scenes,
|
|
59
|
+
error:
|
|
60
|
+
error instanceof Error ? error.message : "Failed to duplicate layer",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const layerDuplicateService = new LayerDuplicateService();
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer Order Service
|
|
3
|
+
* Single Responsibility: Handle layer ordering operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Scene } from "@domains/video";
|
|
7
|
+
import type { LayerOperationResult, LayerOrderAction } from "../../../types";
|
|
8
|
+
|
|
9
|
+
class LayerOrderService {
|
|
10
|
+
/**
|
|
11
|
+
* Change layer order
|
|
12
|
+
*/
|
|
13
|
+
changeLayerOrder(
|
|
14
|
+
scenes: Scene[],
|
|
15
|
+
sceneIndex: number,
|
|
16
|
+
layerId: string,
|
|
17
|
+
action: LayerOrderAction,
|
|
18
|
+
): LayerOperationResult {
|
|
19
|
+
try {
|
|
20
|
+
if (sceneIndex < 0 || sceneIndex >= scenes.length) {
|
|
21
|
+
return {
|
|
22
|
+
success: false,
|
|
23
|
+
updatedScenes: scenes,
|
|
24
|
+
error: "Invalid scene index",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const updatedScenes = [...scenes];
|
|
29
|
+
const scene = updatedScenes[sceneIndex];
|
|
30
|
+
const layerIndex = scene.layers.findIndex((l) => l.id === layerId);
|
|
31
|
+
|
|
32
|
+
if (layerIndex === -1) {
|
|
33
|
+
return {
|
|
34
|
+
success: false,
|
|
35
|
+
updatedScenes: scenes,
|
|
36
|
+
error: "Layer not found",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const updatedLayers = [...scene.layers];
|
|
41
|
+
let newIndex = layerIndex;
|
|
42
|
+
|
|
43
|
+
switch (action) {
|
|
44
|
+
case "front":
|
|
45
|
+
newIndex = updatedLayers.length - 1;
|
|
46
|
+
break;
|
|
47
|
+
case "back":
|
|
48
|
+
newIndex = 0;
|
|
49
|
+
break;
|
|
50
|
+
case "up":
|
|
51
|
+
newIndex = Math.min(layerIndex + 1, updatedLayers.length - 1);
|
|
52
|
+
break;
|
|
53
|
+
case "down":
|
|
54
|
+
newIndex = Math.max(layerIndex - 1, 0);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (newIndex !== layerIndex) {
|
|
59
|
+
const [layer] = updatedLayers.splice(layerIndex, 1);
|
|
60
|
+
updatedLayers.splice(newIndex, 0, layer);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
updatedScenes[sceneIndex] = {
|
|
64
|
+
...scene,
|
|
65
|
+
layers: updatedLayers,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return { success: true, updatedScenes };
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
updatedScenes: scenes,
|
|
73
|
+
error:
|
|
74
|
+
error instanceof Error
|
|
75
|
+
? error.message
|
|
76
|
+
: "Failed to change layer order",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const layerOrderService = new LayerOrderService();
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer Transform Service
|
|
3
|
+
* Single Responsibility: Handle layer position, size, and animation updates
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Scene, Animation, Layer } from "@domains/video";
|
|
7
|
+
import type { LayerOperationResult } from "../../../types";
|
|
8
|
+
|
|
9
|
+
class LayerTransformService {
|
|
10
|
+
/**
|
|
11
|
+
* Update layer position
|
|
12
|
+
*/
|
|
13
|
+
updateLayerPosition(
|
|
14
|
+
scenes: Scene[],
|
|
15
|
+
sceneIndex: number,
|
|
16
|
+
layerId: string,
|
|
17
|
+
x: number,
|
|
18
|
+
y: number,
|
|
19
|
+
): LayerOperationResult {
|
|
20
|
+
try {
|
|
21
|
+
if (sceneIndex < 0 || sceneIndex >= scenes.length) {
|
|
22
|
+
return {
|
|
23
|
+
success: false,
|
|
24
|
+
updatedScenes: scenes,
|
|
25
|
+
error: "Invalid scene index",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const updatedScenes = [...scenes];
|
|
30
|
+
const layerIndex = updatedScenes[sceneIndex].layers.findIndex(
|
|
31
|
+
(l) => l.id === layerId,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (layerIndex === -1) {
|
|
35
|
+
return {
|
|
36
|
+
success: false,
|
|
37
|
+
updatedScenes: scenes,
|
|
38
|
+
error: "Layer not found",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
updatedScenes[sceneIndex].layers[layerIndex] = {
|
|
43
|
+
...updatedScenes[sceneIndex].layers[layerIndex],
|
|
44
|
+
position: { x, y },
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return { success: true, updatedScenes };
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return {
|
|
50
|
+
success: false,
|
|
51
|
+
updatedScenes: scenes,
|
|
52
|
+
error:
|
|
53
|
+
error instanceof Error
|
|
54
|
+
? error.message
|
|
55
|
+
: "Failed to update layer position",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Update layer size
|
|
62
|
+
*/
|
|
63
|
+
updateLayerSize(
|
|
64
|
+
scenes: Scene[],
|
|
65
|
+
sceneIndex: number,
|
|
66
|
+
layerId: string,
|
|
67
|
+
width: number,
|
|
68
|
+
height: number,
|
|
69
|
+
): LayerOperationResult {
|
|
70
|
+
try {
|
|
71
|
+
if (sceneIndex < 0 || sceneIndex >= scenes.length) {
|
|
72
|
+
return {
|
|
73
|
+
success: false,
|
|
74
|
+
updatedScenes: scenes,
|
|
75
|
+
error: "Invalid scene index",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const updatedScenes = [...scenes];
|
|
80
|
+
const layerIndex = updatedScenes[sceneIndex].layers.findIndex(
|
|
81
|
+
(l) => l.id === layerId,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (layerIndex === -1) {
|
|
85
|
+
return {
|
|
86
|
+
success: false,
|
|
87
|
+
updatedScenes: scenes,
|
|
88
|
+
error: "Layer not found",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
updatedScenes[sceneIndex].layers[layerIndex] = {
|
|
93
|
+
...updatedScenes[sceneIndex].layers[layerIndex],
|
|
94
|
+
size: { width, height },
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return { success: true, updatedScenes };
|
|
98
|
+
} catch (error) {
|
|
99
|
+
return {
|
|
100
|
+
success: false,
|
|
101
|
+
updatedScenes: scenes,
|
|
102
|
+
error:
|
|
103
|
+
error instanceof Error
|
|
104
|
+
? error.message
|
|
105
|
+
: "Failed to update layer size",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Update layer animation
|
|
112
|
+
*/
|
|
113
|
+
updateLayerAnimation(
|
|
114
|
+
scenes: Scene[],
|
|
115
|
+
sceneIndex: number,
|
|
116
|
+
layerId: string,
|
|
117
|
+
animation: Animation | undefined,
|
|
118
|
+
): LayerOperationResult {
|
|
119
|
+
try {
|
|
120
|
+
if (sceneIndex < 0 || sceneIndex >= scenes.length) {
|
|
121
|
+
return {
|
|
122
|
+
success: false,
|
|
123
|
+
updatedScenes: scenes,
|
|
124
|
+
error: "Invalid scene index",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const updatedScenes = [...scenes];
|
|
129
|
+
const layerIndex = updatedScenes[sceneIndex].layers.findIndex(
|
|
130
|
+
(l) => l.id === layerId,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (layerIndex === -1) {
|
|
134
|
+
return {
|
|
135
|
+
success: false,
|
|
136
|
+
updatedScenes: scenes,
|
|
137
|
+
error: "Layer not found",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
updatedScenes[sceneIndex].layers[layerIndex] = {
|
|
142
|
+
...updatedScenes[sceneIndex].layers[layerIndex],
|
|
143
|
+
animation,
|
|
144
|
+
} as Layer;
|
|
145
|
+
|
|
146
|
+
return { success: true, updatedScenes };
|
|
147
|
+
} catch (error) {
|
|
148
|
+
return {
|
|
149
|
+
success: false,
|
|
150
|
+
updatedScenes: scenes,
|
|
151
|
+
error:
|
|
152
|
+
error instanceof Error
|
|
153
|
+
? error.message
|
|
154
|
+
: "Failed to update layer animation",
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const layerTransformService = new LayerTransformService();
|