@umituz/react-native-video-editor 1.1.62 → 1.1.64
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/infrastructure/services/base/base-layer-operations.service.ts +201 -0
- package/src/infrastructure/services/image-layer-operations.service.ts +36 -101
- package/src/infrastructure/services/shape-layer-operations.service.ts +38 -47
- package/src/infrastructure/services/text-layer-operations.service.ts +45 -97
- package/src/infrastructure/utils/data-clone.utils.ts +32 -51
- package/src/infrastructure/utils/video-calculations.utils.ts +7 -25
- package/src/presentation/components/CollageEditorCanvas.tsx +23 -232
- package/src/presentation/components/SubtitleListPanel.tsx +46 -252
- package/src/presentation/components/collage/CollageCanvas.tsx +94 -0
- package/src/presentation/components/collage/CollageControls.tsx +115 -0
- package/src/presentation/components/collage/CollageLayoutSelector.tsx +100 -0
- package/src/presentation/components/subtitle/SubtitleListHeader.tsx +58 -0
- package/src/presentation/components/subtitle/SubtitleListItem.tsx +96 -0
- package/src/presentation/components/subtitle/SubtitleModal.tsx +162 -0
- package/src/presentation/components/subtitle/useSubtitleForm.ts +95 -0
- package/src/presentation/hooks/generic/use-layer-form.hook.ts +124 -0
- package/src/presentation/hooks/useImageLayerForm.ts +25 -22
- package/src/presentation/hooks/useTextLayerForm.ts +35 -45
|
@@ -6,42 +6,43 @@
|
|
|
6
6
|
import type { Scene, VideoProject, Layer } from "../../domain/entities/video-project.types";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
* Uses structured clone if available, falls back to manual clone
|
|
9
|
+
* Generic deep clone using structured clone or spread operator
|
|
11
10
|
*/
|
|
12
|
-
export function
|
|
13
|
-
// Use structured clone for better performance if available
|
|
11
|
+
export function deepClone<T>(obj: T): T {
|
|
14
12
|
if (typeof structuredClone !== 'undefined') {
|
|
15
|
-
return structuredClone(
|
|
13
|
+
return structuredClone(obj);
|
|
16
14
|
}
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
if (Array.isArray(obj)) {
|
|
17
|
+
return obj.map(item => deepClone(item)) as T;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (obj !== null && typeof obj === 'object') {
|
|
21
|
+
return Object.keys(obj).reduce((acc, key) => {
|
|
22
|
+
(acc as Record<string, unknown>)[key] = deepClone((obj as Record<string, unknown>)[key]);
|
|
23
|
+
return acc;
|
|
24
|
+
}, {} as T);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return obj;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Optimized deep clone for VideoProject
|
|
32
|
+
*/
|
|
33
|
+
export function cloneVideoProject(project: VideoProject): VideoProject {
|
|
34
|
+
return deepClone(project);
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
/**
|
|
29
38
|
* Optimized deep clone for Scene
|
|
30
39
|
*/
|
|
31
40
|
export function cloneScene(scene: Scene): Scene {
|
|
32
|
-
|
|
33
|
-
return structuredClone(scene);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
...scene,
|
|
38
|
-
layers: scene.layers.map((layer: Layer) => ({ ...layer })),
|
|
39
|
-
};
|
|
41
|
+
return deepClone(scene);
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
/**
|
|
43
|
-
*
|
|
44
|
-
* Layer objects are already immutable in our architecture
|
|
45
|
+
* Shallow clone for Layer
|
|
45
46
|
*/
|
|
46
47
|
export function cloneLayer(layer: Layer): Layer {
|
|
47
48
|
return { ...layer };
|
|
@@ -51,7 +52,7 @@ export function cloneLayer(layer: Layer): Layer {
|
|
|
51
52
|
* Clone multiple layers
|
|
52
53
|
*/
|
|
53
54
|
export function cloneLayers(layers: Layer[]): Layer[] {
|
|
54
|
-
return layers.map(
|
|
55
|
+
return layers.map(layer => ({ ...layer }));
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
/**
|
|
@@ -61,7 +62,7 @@ export function cloneSceneWithNewId(scene: Scene, generateId: () => string): Sce
|
|
|
61
62
|
return {
|
|
62
63
|
...scene,
|
|
63
64
|
id: generateId(),
|
|
64
|
-
layers: scene.layers.map(
|
|
65
|
+
layers: scene.layers.map(layer => ({
|
|
65
66
|
...layer,
|
|
66
67
|
id: generateId(),
|
|
67
68
|
})),
|
|
@@ -79,36 +80,16 @@ export function cloneLayerWithNewId(layer: Layer, generateId: () => string): Lay
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
/**
|
|
82
|
-
*
|
|
83
|
-
*/
|
|
84
|
-
export function cloneAudio(audio: Scene["audio"]): Scene["audio"] {
|
|
85
|
-
if (!audio) return audio;
|
|
86
|
-
return { ...audio };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Optimized clone for Background configuration
|
|
91
|
-
*/
|
|
92
|
-
export function cloneBackground(background: Scene["background"]): Scene["background"] {
|
|
93
|
-
return { ...background };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Optimized clone for Transition configuration
|
|
98
|
-
*/
|
|
99
|
-
export function cloneTransition(transition: Scene["transition"]): Scene["transition"] {
|
|
100
|
-
return { ...transition };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Create a safe copy of an array (shallow clone)
|
|
83
|
+
* Generic shallow clone for any object
|
|
105
84
|
*/
|
|
106
|
-
export function
|
|
107
|
-
return
|
|
85
|
+
export function shallowClone<T>(obj: T): T {
|
|
86
|
+
if (!obj) return obj;
|
|
87
|
+
if (Array.isArray(obj)) return [...obj] as T;
|
|
88
|
+
return { ...obj } as T;
|
|
108
89
|
}
|
|
109
90
|
|
|
110
91
|
/**
|
|
111
|
-
* Safe array slice
|
|
92
|
+
* Safe array slice
|
|
112
93
|
*/
|
|
113
94
|
export function safeSlice<T>(
|
|
114
95
|
array: T[],
|
|
@@ -4,29 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { Scene, VideoProject } from "../../domain/entities/video-project.types";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"720p": 0.5,
|
|
13
|
-
"1080p": 1.0,
|
|
14
|
-
"4k": 3.0,
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Quality multipliers for file size estimation
|
|
19
|
-
*/
|
|
20
|
-
export const QUALITY_MULTIPLIERS: Record<string, number> = {
|
|
21
|
-
low: 0.6,
|
|
22
|
-
medium: 1.0,
|
|
23
|
-
high: 1.4,
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Base file size per second of video (in MB)
|
|
28
|
-
*/
|
|
29
|
-
export const BASE_SIZE_PER_SECOND = 2.5;
|
|
7
|
+
import {
|
|
8
|
+
RESOLUTION_MULTIPLIERS,
|
|
9
|
+
QUALITY_MULTIPLIERS,
|
|
10
|
+
BASE_SIZE_PER_SECOND,
|
|
11
|
+
} from "../constants/export.constants";
|
|
30
12
|
|
|
31
13
|
/**
|
|
32
14
|
* Calculate estimated file size for video export
|
|
@@ -37,8 +19,8 @@ export function calculateEstimatedFileSize(
|
|
|
37
19
|
quality: string,
|
|
38
20
|
): number {
|
|
39
21
|
const baseSize = durationSeconds * BASE_SIZE_PER_SECOND;
|
|
40
|
-
const resolutionMultiplier = RESOLUTION_MULTIPLIERS[resolution] || 1.0;
|
|
41
|
-
const qualityMultiplier = QUALITY_MULTIPLIERS[quality] || 1.0;
|
|
22
|
+
const resolutionMultiplier = RESOLUTION_MULTIPLIERS[resolution as keyof typeof RESOLUTION_MULTIPLIERS] || 1.0;
|
|
23
|
+
const qualityMultiplier = QUALITY_MULTIPLIERS[quality as keyof typeof QUALITY_MULTIPLIERS] || 1.0;
|
|
42
24
|
|
|
43
25
|
return baseSize * resolutionMultiplier * qualityMultiplier;
|
|
44
26
|
}
|
|
@@ -3,18 +3,13 @@
|
|
|
3
3
|
* Collage layout canvas with image cells, layout picker, spacing and border radius controls
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
ScrollView,
|
|
10
|
-
TouchableOpacity,
|
|
11
|
-
StyleSheet,
|
|
12
|
-
Dimensions,
|
|
13
|
-
} from "react-native";
|
|
14
|
-
import { Image } from "expo-image";
|
|
15
|
-
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, Dimensions } from "react-native";
|
|
8
|
+
import { AtomicText } from "@umituz/react-native-design-system/atoms";
|
|
16
9
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
17
|
-
import {
|
|
10
|
+
import { CollageCanvas } from "./collage/CollageCanvas";
|
|
11
|
+
import { CollageControls } from "./collage/CollageControls";
|
|
12
|
+
import { CollageLayoutSelector } from "./collage/CollageLayoutSelector";
|
|
18
13
|
import type { CollageLayout } from "../../infrastructure/constants/collage.constants";
|
|
19
14
|
|
|
20
15
|
const SCREEN_WIDTH = Dimensions.get("window").width;
|
|
@@ -47,184 +42,26 @@ export const CollageEditorCanvas: React.FC<CollageEditorCanvasProps> = ({
|
|
|
47
42
|
const tokens = useAppDesignTokens();
|
|
48
43
|
const size = canvasSize ?? SCREEN_WIDTH - tokens.spacing.md * 2;
|
|
49
44
|
|
|
50
|
-
const styles = useMemo(
|
|
51
|
-
() =>
|
|
52
|
-
StyleSheet.create({
|
|
53
|
-
canvas: {
|
|
54
|
-
width: size,
|
|
55
|
-
height: size,
|
|
56
|
-
alignSelf: "center",
|
|
57
|
-
position: "relative",
|
|
58
|
-
backgroundColor: tokens.colors.surface,
|
|
59
|
-
borderRadius: tokens.borders.radius.md,
|
|
60
|
-
overflow: "hidden",
|
|
61
|
-
},
|
|
62
|
-
cell: {
|
|
63
|
-
position: "absolute",
|
|
64
|
-
overflow: "hidden",
|
|
65
|
-
},
|
|
66
|
-
cellImage: {
|
|
67
|
-
width: "100%",
|
|
68
|
-
height: "100%",
|
|
69
|
-
},
|
|
70
|
-
cellEmpty: {
|
|
71
|
-
flex: 1,
|
|
72
|
-
backgroundColor: tokens.colors.surfaceVariant,
|
|
73
|
-
alignItems: "center",
|
|
74
|
-
justifyContent: "center",
|
|
75
|
-
},
|
|
76
|
-
controls: {
|
|
77
|
-
paddingHorizontal: tokens.spacing.md,
|
|
78
|
-
paddingTop: tokens.spacing.md,
|
|
79
|
-
gap: tokens.spacing.sm,
|
|
80
|
-
},
|
|
81
|
-
controlRow: {
|
|
82
|
-
flexDirection: "row",
|
|
83
|
-
alignItems: "center",
|
|
84
|
-
justifyContent: "space-between",
|
|
85
|
-
},
|
|
86
|
-
stepper: {
|
|
87
|
-
flexDirection: "row",
|
|
88
|
-
alignItems: "center",
|
|
89
|
-
gap: tokens.spacing.sm,
|
|
90
|
-
},
|
|
91
|
-
stepBtn: {
|
|
92
|
-
width: 32,
|
|
93
|
-
height: 32,
|
|
94
|
-
borderRadius: 16,
|
|
95
|
-
backgroundColor: tokens.colors.surfaceVariant,
|
|
96
|
-
alignItems: "center",
|
|
97
|
-
justifyContent: "center",
|
|
98
|
-
},
|
|
99
|
-
stepValue: {
|
|
100
|
-
minWidth: 28,
|
|
101
|
-
textAlign: "center",
|
|
102
|
-
},
|
|
103
|
-
layoutSection: {
|
|
104
|
-
paddingTop: tokens.spacing.sm,
|
|
105
|
-
},
|
|
106
|
-
layoutScroll: {
|
|
107
|
-
paddingHorizontal: tokens.spacing.md,
|
|
108
|
-
gap: tokens.spacing.sm,
|
|
109
|
-
},
|
|
110
|
-
layoutCard: {
|
|
111
|
-
width: 64,
|
|
112
|
-
alignItems: "center",
|
|
113
|
-
gap: tokens.spacing.xs,
|
|
114
|
-
},
|
|
115
|
-
layoutPreview: {
|
|
116
|
-
width: 52,
|
|
117
|
-
height: 52,
|
|
118
|
-
borderRadius: tokens.borders.radius.sm,
|
|
119
|
-
overflow: "hidden",
|
|
120
|
-
position: "relative",
|
|
121
|
-
backgroundColor: tokens.colors.surfaceVariant,
|
|
122
|
-
borderWidth: 2,
|
|
123
|
-
borderColor: "transparent",
|
|
124
|
-
},
|
|
125
|
-
layoutPreviewActive: {
|
|
126
|
-
borderColor: tokens.colors.primary,
|
|
127
|
-
},
|
|
128
|
-
}),
|
|
129
|
-
[tokens, size],
|
|
130
|
-
);
|
|
131
|
-
|
|
132
45
|
return (
|
|
133
46
|
<View>
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
{
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
height: ch * size - spacing * 2,
|
|
143
|
-
borderRadius,
|
|
144
|
-
};
|
|
145
|
-
return (
|
|
146
|
-
<TouchableOpacity
|
|
147
|
-
key={index}
|
|
148
|
-
style={[styles.cell, cellStyle]}
|
|
149
|
-
onPress={() => onCellPress(index)}
|
|
150
|
-
accessibilityLabel={`Cell ${index + 1}`}
|
|
151
|
-
accessibilityRole="button"
|
|
152
|
-
>
|
|
153
|
-
{images[index] ? (
|
|
154
|
-
<Image
|
|
155
|
-
source={{ uri: images[index]! }}
|
|
156
|
-
style={[styles.cellImage, { borderRadius }]}
|
|
157
|
-
contentFit="cover"
|
|
158
|
-
/>
|
|
159
|
-
) : (
|
|
160
|
-
<View style={styles.cellEmpty}>
|
|
161
|
-
<AtomicIcon name="add" size="md" color="textSecondary" />
|
|
162
|
-
</View>
|
|
163
|
-
)}
|
|
164
|
-
</TouchableOpacity>
|
|
165
|
-
);
|
|
166
|
-
})}
|
|
167
|
-
</View>
|
|
47
|
+
<CollageCanvas
|
|
48
|
+
layout={layout}
|
|
49
|
+
images={images}
|
|
50
|
+
spacing={spacing}
|
|
51
|
+
borderRadius={borderRadius}
|
|
52
|
+
onCellPress={onCellPress}
|
|
53
|
+
size={size}
|
|
54
|
+
/>
|
|
168
55
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
<TouchableOpacity
|
|
177
|
-
style={styles.stepBtn}
|
|
178
|
-
onPress={() => onSpacingChange(Math.max(0, spacing - 2))}
|
|
179
|
-
accessibilityLabel="Decrease spacing"
|
|
180
|
-
accessibilityRole="button"
|
|
181
|
-
>
|
|
182
|
-
<AtomicIcon name="chevron-back" size="sm" color="textSecondary" />
|
|
183
|
-
</TouchableOpacity>
|
|
184
|
-
<AtomicText fontWeight="bold" style={styles.stepValue}>
|
|
185
|
-
{spacing}
|
|
186
|
-
</AtomicText>
|
|
187
|
-
<TouchableOpacity
|
|
188
|
-
style={styles.stepBtn}
|
|
189
|
-
onPress={() => onSpacingChange(Math.min(16, spacing + 2))}
|
|
190
|
-
accessibilityLabel="Increase spacing"
|
|
191
|
-
accessibilityRole="button"
|
|
192
|
-
>
|
|
193
|
-
<AtomicIcon name="chevron-forward" size="sm" color="textSecondary" />
|
|
194
|
-
</TouchableOpacity>
|
|
195
|
-
</View>
|
|
196
|
-
</View>
|
|
56
|
+
<CollageControls
|
|
57
|
+
spacing={spacing}
|
|
58
|
+
borderRadius={borderRadius}
|
|
59
|
+
onSpacingChange={onSpacingChange}
|
|
60
|
+
onBorderRadiusChange={onBorderRadiusChange}
|
|
61
|
+
t={t}
|
|
62
|
+
/>
|
|
197
63
|
|
|
198
|
-
|
|
199
|
-
<AtomicText type="labelSmall" color="textSecondary">
|
|
200
|
-
{t("editor.collage.corners") || "Corners"}
|
|
201
|
-
</AtomicText>
|
|
202
|
-
<View style={styles.stepper}>
|
|
203
|
-
<TouchableOpacity
|
|
204
|
-
style={styles.stepBtn}
|
|
205
|
-
onPress={() => onBorderRadiusChange(Math.max(0, borderRadius - 4))}
|
|
206
|
-
accessibilityLabel="Decrease corner radius"
|
|
207
|
-
accessibilityRole="button"
|
|
208
|
-
>
|
|
209
|
-
<AtomicIcon name="chevron-back" size="sm" color="textSecondary" />
|
|
210
|
-
</TouchableOpacity>
|
|
211
|
-
<AtomicText fontWeight="bold" style={styles.stepValue}>
|
|
212
|
-
{borderRadius}
|
|
213
|
-
</AtomicText>
|
|
214
|
-
<TouchableOpacity
|
|
215
|
-
style={styles.stepBtn}
|
|
216
|
-
onPress={() => onBorderRadiusChange(Math.min(24, borderRadius + 4))}
|
|
217
|
-
accessibilityLabel="Increase corner radius"
|
|
218
|
-
accessibilityRole="button"
|
|
219
|
-
>
|
|
220
|
-
<AtomicIcon name="chevron-forward" size="sm" color="textSecondary" />
|
|
221
|
-
</TouchableOpacity>
|
|
222
|
-
</View>
|
|
223
|
-
</View>
|
|
224
|
-
</View>
|
|
225
|
-
|
|
226
|
-
{/* Layout Picker */}
|
|
227
|
-
<View style={styles.layoutSection}>
|
|
64
|
+
<View style={{ paddingTop: tokens.spacing.sm }}>
|
|
228
65
|
<AtomicText
|
|
229
66
|
type="labelSmall"
|
|
230
67
|
color="textSecondary"
|
|
@@ -232,53 +69,7 @@ export const CollageEditorCanvas: React.FC<CollageEditorCanvasProps> = ({
|
|
|
232
69
|
>
|
|
233
70
|
{t("editor.collage.layout") || "Layout"}
|
|
234
71
|
</AtomicText>
|
|
235
|
-
<
|
|
236
|
-
horizontal
|
|
237
|
-
showsHorizontalScrollIndicator={false}
|
|
238
|
-
contentContainerStyle={styles.layoutScroll}
|
|
239
|
-
>
|
|
240
|
-
{COLLAGE_LAYOUTS.map((l) => {
|
|
241
|
-
const isActive = layout.id === l.id;
|
|
242
|
-
return (
|
|
243
|
-
<TouchableOpacity
|
|
244
|
-
key={l.id}
|
|
245
|
-
style={styles.layoutCard}
|
|
246
|
-
onPress={() => onSelectLayout(l)}
|
|
247
|
-
accessibilityLabel={`Layout ${l.count} cells`}
|
|
248
|
-
accessibilityRole="button"
|
|
249
|
-
accessibilityState={{ selected: isActive }}
|
|
250
|
-
>
|
|
251
|
-
<View style={[styles.layoutPreview, isActive && styles.layoutPreviewActive]}>
|
|
252
|
-
{l.grid.map((cell, i) => {
|
|
253
|
-
const [lx, ly, lw, lh] = cell;
|
|
254
|
-
return (
|
|
255
|
-
<View
|
|
256
|
-
key={i}
|
|
257
|
-
style={{
|
|
258
|
-
position: "absolute",
|
|
259
|
-
left: lx * 52 + 2,
|
|
260
|
-
top: ly * 52 + 2,
|
|
261
|
-
width: lw * 52 - 4,
|
|
262
|
-
height: lh * 52 - 4,
|
|
263
|
-
backgroundColor: isActive
|
|
264
|
-
? tokens.colors.primary
|
|
265
|
-
: tokens.colors.surfaceVariant,
|
|
266
|
-
borderRadius: 2,
|
|
267
|
-
}}
|
|
268
|
-
/>
|
|
269
|
-
);
|
|
270
|
-
})}
|
|
271
|
-
</View>
|
|
272
|
-
<AtomicText
|
|
273
|
-
type="labelSmall"
|
|
274
|
-
color={isActive ? "primary" : "textSecondary"}
|
|
275
|
-
>
|
|
276
|
-
{l.count}
|
|
277
|
-
</AtomicText>
|
|
278
|
-
</TouchableOpacity>
|
|
279
|
-
);
|
|
280
|
-
})}
|
|
281
|
-
</ScrollView>
|
|
72
|
+
<CollageLayoutSelector selected={layout} onSelect={onSelectLayout} />
|
|
282
73
|
</View>
|
|
283
74
|
</View>
|
|
284
75
|
);
|