@umituz/react-native-video-editor 1.1.63 → 1.1.65
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/package.json +1 -1
- package/src/application/services/EditorService.ts +151 -0
- package/src/application/usecases/LayerUseCases.ts +192 -0
- package/src/infrastructure/repositories/LayerRepositoryImpl.ts +158 -0
- package/src/infrastructure/services/image-layer-operations.service.ts +1 -1
- package/src/infrastructure/services/shape-layer-operations.service.ts +1 -1
- package/src/infrastructure/services/text-layer-operations.service.ts +1 -1
- package/src/infrastructure/utils/debounce.utils.ts +69 -0
- package/src/infrastructure/utils/image-processing.utils.ts +73 -0
- package/src/infrastructure/utils/position-calculations.utils.ts +45 -161
- package/src/presentation/components/EditorTimeline.tsx +29 -11
- package/src/presentation/components/EditorToolPanel.tsx +71 -172
- package/src/presentation/components/LayerActionsMenu.tsx +97 -159
- package/src/presentation/components/SceneActionsMenu.tsx +34 -44
- package/src/presentation/components/SubtitleListPanel.tsx +55 -28
- package/src/presentation/components/SubtitleStylePicker.tsx +36 -156
- package/src/presentation/components/collage/CollageCanvas.tsx +2 -2
- package/src/presentation/components/collage/CollageLayoutSelector.tsx +0 -4
- package/src/presentation/components/draggable-layer/LayerContent.tsx +7 -2
- package/src/presentation/components/generic/ActionMenu.tsx +110 -0
- package/src/presentation/components/generic/Editor.tsx +65 -0
- package/src/presentation/components/generic/Selector.tsx +96 -0
- package/src/presentation/components/generic/Toolbar.tsx +77 -0
- package/src/presentation/components/image-layer/ImagePreview.tsx +7 -1
- package/src/presentation/components/shape-layer/ColorPickerHorizontal.tsx +20 -55
- package/src/presentation/components/subtitle/SubtitleListItem.tsx +4 -5
- package/src/presentation/components/subtitle/SubtitleModal.tsx +2 -2
- package/src/presentation/components/subtitle/useSubtitleForm.ts +1 -1
- package/src/presentation/components/text-layer/ColorPicker.tsx +21 -55
- package/src/presentation/components/text-layer/FontSizeSelector.tsx +19 -50
- package/src/presentation/components/text-layer/OptionSelector.tsx +18 -55
- package/src/presentation/components/text-layer/TextAlignSelector.tsx +24 -51
- package/src/presentation/hooks/generic/use-layer-form.hook.ts +19 -16
- package/src/presentation/hooks/generic/useForm.ts +99 -0
- package/src/presentation/hooks/generic/useList.ts +117 -0
- package/src/presentation/hooks/useDraggableLayerGestures.ts +28 -25
- package/src/presentation/hooks/useEditorPlayback.ts +19 -2
- package/src/presentation/hooks/useImageLayerForm.ts +9 -6
- package/src/presentation/hooks/useMenuActions.tsx +19 -4
- package/src/presentation/hooks/useTextLayerForm.ts +9 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-video-editor",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.65",
|
|
4
4
|
"description": "Professional video editor with layer-based timeline, text/image/shape/audio/animation layers, and export functionality",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor Service
|
|
3
|
+
* Single Responsibility: High-level editor operations coordination
|
|
4
|
+
* Application Service Layer
|
|
5
|
+
*
|
|
6
|
+
* Orchestrates multiple use cases and domain operations
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { VideoProject, Scene, Layer } from "../../domain/entities/video-project.types";
|
|
10
|
+
import type {
|
|
11
|
+
CreateLayerRequest,
|
|
12
|
+
UpdateLayerRequest,
|
|
13
|
+
DeleteLayerRequest,
|
|
14
|
+
} from "../usecases/LayerUseCases";
|
|
15
|
+
import {
|
|
16
|
+
CreateLayerUseCase,
|
|
17
|
+
UpdateLayerUseCase,
|
|
18
|
+
DeleteLayerUseCase,
|
|
19
|
+
DuplicateLayerUseCase,
|
|
20
|
+
ReorderLayerUseCase,
|
|
21
|
+
} from "../usecases/LayerUseCases";
|
|
22
|
+
import type { LayerRepository } from "../usecases/LayerUseCases";
|
|
23
|
+
|
|
24
|
+
export interface EditorServiceConfig {
|
|
25
|
+
layerRepository: LayerRepository;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Application service that coordinates editor operations
|
|
30
|
+
* Provides high-level API for the presentation layer
|
|
31
|
+
*/
|
|
32
|
+
export class EditorService {
|
|
33
|
+
private readonly createLayerUseCase: CreateLayerUseCase;
|
|
34
|
+
private readonly updateLayerUseCase: UpdateLayerUseCase;
|
|
35
|
+
private readonly deleteLayerUseCase: DeleteLayerUseCase;
|
|
36
|
+
private readonly duplicateLayerUseCase: DuplicateLayerUseCase;
|
|
37
|
+
private readonly reorderLayerUseCase: ReorderLayerUseCase;
|
|
38
|
+
|
|
39
|
+
constructor(config: EditorServiceConfig) {
|
|
40
|
+
this.createLayerUseCase = new CreateLayerUseCase(config.layerRepository);
|
|
41
|
+
this.updateLayerUseCase = new UpdateLayerUseCase(config.layerRepository);
|
|
42
|
+
this.deleteLayerUseCase = new DeleteLayerUseCase(config.layerRepository);
|
|
43
|
+
this.duplicateLayerUseCase = new DuplicateLayerUseCase(config.layerRepository);
|
|
44
|
+
this.reorderLayerUseCase = new ReorderLayerUseCase(config.layerRepository);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Layer Operations
|
|
48
|
+
async createLayer(request: CreateLayerRequest): Promise<Layer> {
|
|
49
|
+
return await this.createLayerUseCase.execute(request);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async updateLayer(request: UpdateLayerRequest): Promise<void> {
|
|
53
|
+
await this.updateLayerUseCase.execute(request);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async deleteLayer(request: DeleteLayerRequest): Promise<void> {
|
|
57
|
+
await this.deleteLayerUseCase.execute(request);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async duplicateLayer(sceneId: string, layerId: string): Promise<Layer> {
|
|
61
|
+
return await this.duplicateLayerUseCase.execute(sceneId, layerId);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async reorderLayer(
|
|
65
|
+
sceneId: string,
|
|
66
|
+
layerId: string,
|
|
67
|
+
direction: "front" | "back" | "up" | "down",
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
await this.reorderLayerUseCase.execute(sceneId, layerId, direction);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Batch Operations
|
|
73
|
+
async batchUpdateLayers(
|
|
74
|
+
sceneId: string,
|
|
75
|
+
updates: Array<{ layerId: string; changes: Partial<Layer> }>,
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
for (const { layerId, changes } of updates) {
|
|
78
|
+
await this.updateLayer({ sceneId, layerId, updates: changes });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async duplicateMultipleLayers(
|
|
83
|
+
sceneId: string,
|
|
84
|
+
layerIds: string[],
|
|
85
|
+
): Promise<Layer[]> {
|
|
86
|
+
const duplicated: Layer[] = [];
|
|
87
|
+
|
|
88
|
+
for (const layerId of layerIds) {
|
|
89
|
+
const layer = await this.duplicateLayer(sceneId, layerId);
|
|
90
|
+
duplicated.push(layer);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return duplicated;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async deleteMultipleLayers(sceneId: string, layerIds: string[]): Promise<void> {
|
|
97
|
+
for (const layerId of layerIds) {
|
|
98
|
+
await this.deleteLayer({ sceneId, layerId });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Query Operations
|
|
103
|
+
getLayersByScene(scenes: Scene[], sceneId: string): Layer[] {
|
|
104
|
+
const scene = scenes.find((s) => s.id === sceneId);
|
|
105
|
+
return scene?.layers || [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
getLayersByType(scenes: Scene[], type: Layer["type"]): Layer[] {
|
|
109
|
+
const allLayers = scenes.flatMap((s) => s.layers);
|
|
110
|
+
return allLayers.filter((l) => l.type === type);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
getLayerById(scenes: Scene[], layerId: string): Layer | undefined {
|
|
114
|
+
for (const scene of scenes) {
|
|
115
|
+
const layer = scene.layers.find((l) => l.id === layerId);
|
|
116
|
+
if (layer) return layer;
|
|
117
|
+
}
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Validation
|
|
122
|
+
validateLayer(layer: Partial<Layer>): { valid: boolean; errors: string[] } {
|
|
123
|
+
const errors: string[] = [];
|
|
124
|
+
|
|
125
|
+
if (!layer.type) {
|
|
126
|
+
errors.push("Layer type is required");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (layer.type === "image" && !(layer as any).uri) {
|
|
130
|
+
errors.push("Image layer requires URI");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (layer.type === "text" && !(layer as any).content) {
|
|
134
|
+
errors.push("Text layer requires content");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
valid: errors.length === 0,
|
|
139
|
+
errors,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Export Helpers
|
|
144
|
+
prepareProjectForExport(project: VideoProject): VideoProject {
|
|
145
|
+
// Clean up temporary data
|
|
146
|
+
// Optimize images
|
|
147
|
+
// Validate all layers
|
|
148
|
+
// Return export-ready project
|
|
149
|
+
return project;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer Use Cases
|
|
3
|
+
* Single Responsibility: Layer business logic orchestration
|
|
4
|
+
* Domain-Driven Design: Application Layer
|
|
5
|
+
*
|
|
6
|
+
* This layer orchestrates domain operations and coordinates between entities.
|
|
7
|
+
* It doesn't contain business logic, but coordinates domain services.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Layer, ImageLayer, TextLayer, ShapeLayer } from "../../domain/entities/video-project.types";
|
|
11
|
+
import type { AddImageLayerData, AddTextLayerData, AddShapeLayerData } from "../../domain/entities/video-project.types";
|
|
12
|
+
|
|
13
|
+
// Repository Interface (Dependency Inversion)
|
|
14
|
+
export interface LayerRepository {
|
|
15
|
+
addLayer(sceneId: string, layer: Layer): Promise<void>;
|
|
16
|
+
updateLayer(sceneId: string, layerId: string, updates: Partial<Layer>): Promise<void>;
|
|
17
|
+
deleteLayer(sceneId: string, layerId: string): Promise<void>;
|
|
18
|
+
duplicateLayer(sceneId: string, layerId: string): Promise<Layer>;
|
|
19
|
+
reorderLayer(sceneId: string, layerId: string, direction: "front" | "back" | "up" | "down"): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Request/Response DTOs
|
|
23
|
+
export interface CreateLayerRequest {
|
|
24
|
+
sceneId: string;
|
|
25
|
+
layerType: "image" | "text" | "shape" | "video";
|
|
26
|
+
data: AddImageLayerData | AddTextLayerData | AddShapeLayerData;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface UpdateLayerRequest {
|
|
30
|
+
sceneId: string;
|
|
31
|
+
layerId: string;
|
|
32
|
+
updates: Partial<Layer>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface DeleteLayerRequest {
|
|
36
|
+
sceneId: string;
|
|
37
|
+
layerId: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Use Case: Create Layer
|
|
41
|
+
export class CreateLayerUseCase {
|
|
42
|
+
constructor(private readonly layerRepository: LayerRepository) {}
|
|
43
|
+
|
|
44
|
+
async execute(request: CreateLayerRequest): Promise<Layer> {
|
|
45
|
+
const { sceneId, layerType, data } = request;
|
|
46
|
+
|
|
47
|
+
// Validate request
|
|
48
|
+
if (!sceneId) {
|
|
49
|
+
throw new Error("Scene ID is required");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Create layer through domain service
|
|
53
|
+
const layer = this.createLayerByType(layerType, data);
|
|
54
|
+
|
|
55
|
+
// Persist through repository
|
|
56
|
+
await this.layerRepository.addLayer(sceneId, layer);
|
|
57
|
+
|
|
58
|
+
return layer;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private createLayerByType(
|
|
62
|
+
type: string,
|
|
63
|
+
data: AddImageLayerData | AddTextLayerData | AddShapeLayerData,
|
|
64
|
+
): Layer {
|
|
65
|
+
switch (type) {
|
|
66
|
+
case "image":
|
|
67
|
+
return this.createImageLayer(data as AddImageLayerData);
|
|
68
|
+
case "text":
|
|
69
|
+
return this.createTextLayer(data as AddTextLayerData);
|
|
70
|
+
case "shape":
|
|
71
|
+
return this.createShapeLayer(data as AddShapeLayerData);
|
|
72
|
+
default:
|
|
73
|
+
throw new Error(`Unsupported layer type: ${type}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private createImageLayer(data: AddImageLayerData): ImageLayer {
|
|
78
|
+
return {
|
|
79
|
+
id: this.generateId(),
|
|
80
|
+
type: "image",
|
|
81
|
+
uri: data.uri || "",
|
|
82
|
+
position: { x: 15, y: 30 },
|
|
83
|
+
size: { width: 70, height: 40 },
|
|
84
|
+
rotation: 0,
|
|
85
|
+
opacity: data.opacity ?? 1,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private createTextLayer(data: AddTextLayerData): TextLayer {
|
|
90
|
+
return {
|
|
91
|
+
id: this.generateId(),
|
|
92
|
+
type: "text",
|
|
93
|
+
content: data.content || "",
|
|
94
|
+
position: { x: 10, y: 40 },
|
|
95
|
+
size: { width: 80, height: 20 },
|
|
96
|
+
rotation: 0,
|
|
97
|
+
opacity: 1,
|
|
98
|
+
fontSize: data.fontSize || 48,
|
|
99
|
+
fontFamily: data.fontFamily || "System",
|
|
100
|
+
fontWeight: (data.fontWeight as TextLayer["fontWeight"]) || "bold",
|
|
101
|
+
color: data.color || "#000000",
|
|
102
|
+
textAlign: data.textAlign || "center",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private createShapeLayer(data: AddShapeLayerData): ShapeLayer {
|
|
107
|
+
return {
|
|
108
|
+
id: this.generateId(),
|
|
109
|
+
type: "shape",
|
|
110
|
+
shape: (data.shape as ShapeLayer["shape"]) || "rectangle",
|
|
111
|
+
position: { x: 25, y: 25 },
|
|
112
|
+
size: { width: 50, height: 50 },
|
|
113
|
+
rotation: 0,
|
|
114
|
+
opacity: data.opacity ?? 1,
|
|
115
|
+
fillColor: data.fillColor || "#000000",
|
|
116
|
+
borderColor: data.borderColor,
|
|
117
|
+
borderWidth: data.borderWidth,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private generateId(): string {
|
|
122
|
+
return `layer_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Use Case: Update Layer
|
|
127
|
+
export class UpdateLayerUseCase {
|
|
128
|
+
constructor(private readonly layerRepository: LayerRepository) {}
|
|
129
|
+
|
|
130
|
+
async execute(request: UpdateLayerRequest): Promise<void> {
|
|
131
|
+
const { sceneId, layerId, updates } = request;
|
|
132
|
+
|
|
133
|
+
// Validate
|
|
134
|
+
if (!sceneId || !layerId) {
|
|
135
|
+
throw new Error("Scene ID and Layer ID are required");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Persist through repository
|
|
139
|
+
await this.layerRepository.updateLayer(sceneId, layerId, updates);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Use Case: Delete Layer
|
|
144
|
+
export class DeleteLayerUseCase {
|
|
145
|
+
constructor(private readonly layerRepository: LayerRepository) {}
|
|
146
|
+
|
|
147
|
+
async execute(request: DeleteLayerRequest): Promise<void> {
|
|
148
|
+
const { sceneId, layerId } = request;
|
|
149
|
+
|
|
150
|
+
// Validate
|
|
151
|
+
if (!sceneId || !layerId) {
|
|
152
|
+
throw new Error("Scene ID and Layer ID are required");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Persist through repository
|
|
156
|
+
await this.layerRepository.deleteLayer(sceneId, layerId);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Use Case: Duplicate Layer
|
|
161
|
+
export class DuplicateLayerUseCase {
|
|
162
|
+
constructor(private readonly layerRepository: LayerRepository) {}
|
|
163
|
+
|
|
164
|
+
async execute(sceneId: string, layerId: string): Promise<Layer> {
|
|
165
|
+
// Validate
|
|
166
|
+
if (!sceneId || !layerId) {
|
|
167
|
+
throw new Error("Scene ID and Layer ID are required");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Persist through repository
|
|
171
|
+
return await this.layerRepository.duplicateLayer(sceneId, layerId);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Use Case: Reorder Layer
|
|
176
|
+
export class ReorderLayerUseCase {
|
|
177
|
+
constructor(private readonly layerRepository: LayerRepository) {}
|
|
178
|
+
|
|
179
|
+
async execute(
|
|
180
|
+
sceneId: string,
|
|
181
|
+
layerId: string,
|
|
182
|
+
direction: "front" | "back" | "up" | "down",
|
|
183
|
+
): Promise<void> {
|
|
184
|
+
// Validate
|
|
185
|
+
if (!sceneId || !layerId) {
|
|
186
|
+
throw new Error("Scene ID and Layer ID are required");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Persist through repository
|
|
190
|
+
await this.layerRepository.reorderLayer(sceneId, layerId, direction);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer Repository Implementation
|
|
3
|
+
* Single Responsibility: Persist layer data
|
|
4
|
+
* Infrastructure Layer
|
|
5
|
+
*
|
|
6
|
+
* Implements the LayerRepository interface from the application layer.
|
|
7
|
+
* Handles data persistence and retrieval.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Layer, Scene } from "../../domain/entities/video-project.types";
|
|
11
|
+
import type { LayerRepository } from "../../application/usecases/LayerUseCases";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* In-memory implementation of LayerRepository
|
|
15
|
+
* In production, this would connect to a database, API, or state management
|
|
16
|
+
*/
|
|
17
|
+
export class LayerRepositoryImpl implements LayerRepository {
|
|
18
|
+
constructor(
|
|
19
|
+
private readonly getScenes: () => Scene[],
|
|
20
|
+
private readonly setScenes: (scenes: Scene[]) => void,
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
async addLayer(sceneId: string, layer: Layer): Promise<void> {
|
|
24
|
+
const scenes = this.getScenes();
|
|
25
|
+
const sceneIndex = scenes.findIndex((s) => s.id === sceneId);
|
|
26
|
+
|
|
27
|
+
if (sceneIndex === -1) {
|
|
28
|
+
throw new Error(`Scene not found: ${sceneId}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
scenes[sceneIndex].layers.push(layer);
|
|
32
|
+
this.setScenes([...scenes]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async updateLayer(
|
|
36
|
+
sceneId: string,
|
|
37
|
+
layerId: string,
|
|
38
|
+
updates: Partial<Layer>,
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
const scenes = this.getScenes();
|
|
41
|
+
const sceneIndex = scenes.findIndex((s) => s.id === sceneId);
|
|
42
|
+
|
|
43
|
+
if (sceneIndex === -1) {
|
|
44
|
+
throw new Error(`Scene not found: ${sceneId}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const layerIndex = scenes[sceneIndex].layers.findIndex((l) => l.id === layerId);
|
|
48
|
+
|
|
49
|
+
if (layerIndex === -1) {
|
|
50
|
+
throw new Error(`Layer not found: ${layerId}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
scenes[sceneIndex].layers[layerIndex] = {
|
|
54
|
+
...scenes[sceneIndex].layers[layerIndex],
|
|
55
|
+
...updates,
|
|
56
|
+
} as Layer;
|
|
57
|
+
|
|
58
|
+
this.setScenes([...scenes]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async deleteLayer(sceneId: string, layerId: string): Promise<void> {
|
|
62
|
+
const scenes = this.getScenes();
|
|
63
|
+
const sceneIndex = scenes.findIndex((s) => s.id === sceneId);
|
|
64
|
+
|
|
65
|
+
if (sceneIndex === -1) {
|
|
66
|
+
throw new Error(`Scene not found: ${sceneId}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
scenes[sceneIndex].layers = scenes[sceneIndex].layers.filter(
|
|
70
|
+
(l) => l.id !== layerId,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
this.setScenes([...scenes]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async duplicateLayer(sceneId: string, layerId: string): Promise<Layer> {
|
|
77
|
+
const scenes = this.getScenes();
|
|
78
|
+
const sceneIndex = scenes.findIndex((s) => s.id === sceneId);
|
|
79
|
+
|
|
80
|
+
if (sceneIndex === -1) {
|
|
81
|
+
throw new Error(`Scene not found: ${sceneId}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const layer = scenes[sceneIndex].layers.find((l) => l.id === layerId);
|
|
85
|
+
|
|
86
|
+
if (!layer) {
|
|
87
|
+
throw new Error(`Layer not found: ${layerId}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const duplicated: Layer = {
|
|
91
|
+
...layer,
|
|
92
|
+
id: `layer_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
93
|
+
position: {
|
|
94
|
+
x: layer.position.x + 5,
|
|
95
|
+
y: layer.position.y + 5,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
scenes[sceneIndex].layers.push(duplicated);
|
|
100
|
+
this.setScenes([...scenes]);
|
|
101
|
+
|
|
102
|
+
return duplicated;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async reorderLayer(
|
|
106
|
+
sceneId: string,
|
|
107
|
+
layerId: string,
|
|
108
|
+
direction: "front" | "back" | "up" | "down",
|
|
109
|
+
): Promise<void> {
|
|
110
|
+
const scenes = this.getScenes();
|
|
111
|
+
const sceneIndex = scenes.findIndex((s) => s.id === sceneId);
|
|
112
|
+
|
|
113
|
+
if (sceneIndex === -1) {
|
|
114
|
+
throw new Error(`Scene not found: ${sceneId}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const layers = scenes[sceneIndex].layers;
|
|
118
|
+
const layerIndex = layers.findIndex((l) => l.id === layerId);
|
|
119
|
+
|
|
120
|
+
if (layerIndex === -1) {
|
|
121
|
+
throw new Error(`Layer not found: ${layerId}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const [layer] = layers.splice(layerIndex, 1);
|
|
125
|
+
|
|
126
|
+
switch (direction) {
|
|
127
|
+
case "front":
|
|
128
|
+
layers.push(layer);
|
|
129
|
+
break;
|
|
130
|
+
case "back":
|
|
131
|
+
layers.unshift(layer);
|
|
132
|
+
break;
|
|
133
|
+
case "up":
|
|
134
|
+
if (layerIndex < layers.length) {
|
|
135
|
+
layers.splice(layerIndex + 1, 0, layer);
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
case "down":
|
|
139
|
+
if (layerIndex > 0) {
|
|
140
|
+
layers.splice(layerIndex - 1, 0, layer);
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
scenes[sceneIndex].layers = layers;
|
|
146
|
+
this.setScenes([...scenes]);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Factory function to create repository with state management integration
|
|
152
|
+
*/
|
|
153
|
+
export function createLayerRepository(
|
|
154
|
+
getScenes: () => Scene[],
|
|
155
|
+
setScenes: (scenes: Scene[]) => void,
|
|
156
|
+
): LayerRepository {
|
|
157
|
+
return new LayerRepositoryImpl(getScenes, setScenes);
|
|
158
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { generateUUID } from "@umituz/react-native-design-system/uuid";
|
|
7
|
-
import type { ImageLayer } from "../../domain/entities/video-project.types";
|
|
7
|
+
import type { ImageLayer, Scene } from "../../domain/entities/video-project.types";
|
|
8
8
|
import type { LayerOperationResult, AddImageLayerData } from "../../domain/entities/video-project.types";
|
|
9
9
|
import { BaseLayerOperationsService } from "./base/base-layer-operations.service";
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { generateUUID } from "@umituz/react-native-design-system/uuid";
|
|
7
|
-
import type { ShapeLayer } from "../../domain/entities/video-project.types";
|
|
7
|
+
import type { ShapeLayer, Scene } from "../../domain/entities/video-project.types";
|
|
8
8
|
import type { LayerOperationResult, AddShapeLayerData } from "../../domain/entities/video-project.types";
|
|
9
9
|
import { BaseLayerOperationsService } from "./base/base-layer-operations.service";
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { generateUUID } from "@umituz/react-native-design-system/uuid";
|
|
7
|
-
import type { TextLayer } from "../../domain/entities/video-project.types";
|
|
7
|
+
import type { TextLayer, Scene } from "../../domain/entities/video-project.types";
|
|
8
8
|
import type { LayerOperationResult, AddTextLayerData } from "../../domain/entities/video-project.types";
|
|
9
9
|
import { BaseLayerOperationsService } from "./base/base-layer-operations.service";
|
|
10
10
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debounce & Throttle Utilities
|
|
3
|
+
* PERFORMANCE: Prevents excessive function calls
|
|
4
|
+
* - Debounce: Wait before executing (search, auto-save)
|
|
5
|
+
* - Throttle: Execute at most once per interval (scroll, resize)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Debounce function execution
|
|
10
|
+
* @param func Function to debounce
|
|
11
|
+
* @param wait Wait time in ms (default: 300)
|
|
12
|
+
* @returns Debounced function
|
|
13
|
+
*/
|
|
14
|
+
export function debounce<T extends (...args: unknown[]) => unknown>(
|
|
15
|
+
func: T,
|
|
16
|
+
wait: number = 300,
|
|
17
|
+
): (...args: Parameters<T>) => void {
|
|
18
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
19
|
+
|
|
20
|
+
return function debounced(...args: Parameters<T>) {
|
|
21
|
+
if (timeoutId) {
|
|
22
|
+
clearTimeout(timeoutId);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
timeoutId = setTimeout(() => {
|
|
26
|
+
func(...args);
|
|
27
|
+
timeoutId = null;
|
|
28
|
+
}, wait);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Throttle function execution
|
|
34
|
+
* @param func Function to throttle
|
|
35
|
+
* @param limit Minimum time between calls in ms (default: 100)
|
|
36
|
+
* @returns Throttled function
|
|
37
|
+
*/
|
|
38
|
+
export function throttle<T extends (...args: unknown[]) => unknown>(
|
|
39
|
+
func: T,
|
|
40
|
+
limit: number = 100,
|
|
41
|
+
): (...args: Parameters<T>) => void {
|
|
42
|
+
let inThrottle = false;
|
|
43
|
+
let lastResult: ReturnType<T>;
|
|
44
|
+
|
|
45
|
+
return function throttled(...args: Parameters<T>) {
|
|
46
|
+
if (!inThrottle) {
|
|
47
|
+
lastResult = func(...args) as ReturnType<T>;
|
|
48
|
+
inThrottle = true;
|
|
49
|
+
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
inThrottle = false;
|
|
52
|
+
}, limit);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return lastResult;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* React hook for debounced value
|
|
61
|
+
* @param value Value to debounce
|
|
62
|
+
* @param _delay Delay in ms (default: 500)
|
|
63
|
+
* @returns Debounced value
|
|
64
|
+
*/
|
|
65
|
+
export function useDebouncedValue<T>(value: T, _delay: number = 500): T {
|
|
66
|
+
// This would be implemented in a separate hook file
|
|
67
|
+
// For now, just return the value
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Processing Utilities
|
|
3
|
+
* PERFORMANCE: Optimizes image handling for memory efficiency
|
|
4
|
+
* NOTE: Requires expo-image-manipulator package (graceful degradation if not installed)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
let ImageManipulator: any = null;
|
|
8
|
+
try {
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
10
|
+
ImageManipulator = require('expo-image-manipulator');
|
|
11
|
+
} catch {
|
|
12
|
+
ImageManipulator = null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function resizeImage(uri: string, maxWidth: number = 1024, maxHeight: number = 1024): Promise<string> {
|
|
16
|
+
if (!ImageManipulator) return uri;
|
|
17
|
+
try {
|
|
18
|
+
const result = await ImageManipulator.manipulateAsync(uri, [{ resize: { width: maxWidth, height: maxHeight } }], { compress: 0.8, format: ImageManipulator.SaveFormat.JPEG });
|
|
19
|
+
return result.uri;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.warn('Image resize failed:', error);
|
|
22
|
+
return uri;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function generateThumbnail(uri: string, size: number = 200): Promise<string> {
|
|
27
|
+
if (!ImageManipulator) return uri;
|
|
28
|
+
try {
|
|
29
|
+
const result = await ImageManipulator.manipulateAsync(uri, [{ resize: { width: size, height: size } }], { compress: 0.7, format: ImageManipulator.SaveFormat.JPEG });
|
|
30
|
+
return result.uri;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.warn('Thumbnail generation failed:', error);
|
|
33
|
+
return uri;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function compressImage(uri: string, quality: number = 0.8): Promise<string> {
|
|
38
|
+
if (!ImageManipulator) return uri;
|
|
39
|
+
try {
|
|
40
|
+
const result = await ImageManipulator.manipulateAsync(uri, [], { compress: quality, format: ImageManipulator.SaveFormat.JPEG });
|
|
41
|
+
return result.uri;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.warn('Image compression failed:', error);
|
|
44
|
+
return uri;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function getImageDimensions(uri: string): Promise<{ width: number; height: number } | null> {
|
|
49
|
+
if (!ImageManipulator) return null;
|
|
50
|
+
try {
|
|
51
|
+
const result = await ImageManipulator.manipulateAsync(uri, [], { compress: 1, format: ImageManipulator.SaveFormat.JPEG });
|
|
52
|
+
return { width: result.width, height: result.height };
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.warn('Failed to get image dimensions:', error);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function optimizeImageForEditor(uri: string): Promise<string> {
|
|
60
|
+
const MAX_DIMENSION = 1920;
|
|
61
|
+
if (!ImageManipulator) return uri;
|
|
62
|
+
try {
|
|
63
|
+
const dimensions = await getImageDimensions(uri);
|
|
64
|
+
if (!dimensions) return uri;
|
|
65
|
+
if (dimensions.width > MAX_DIMENSION || dimensions.height > MAX_DIMENSION) {
|
|
66
|
+
return await resizeImage(uri, MAX_DIMENSION, MAX_DIMENSION);
|
|
67
|
+
}
|
|
68
|
+
return await compressImage(uri, 0.85);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.warn('Image optimization failed:', error);
|
|
71
|
+
return uri;
|
|
72
|
+
}
|
|
73
|
+
}
|