@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.
@@ -6,42 +6,43 @@
6
6
  import type { Scene, VideoProject, Layer } from "../../domain/entities/video-project.types";
7
7
 
8
8
  /**
9
- * Optimized deep clone for VideoProject
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 cloneVideoProject(project: VideoProject): VideoProject {
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(project);
13
+ return structuredClone(obj);
16
14
  }
17
15
 
18
- // Manual clone fallback - only clone what's necessary
19
- return {
20
- ...project,
21
- scenes: project.scenes.map((scene: Scene) => ({
22
- ...scene,
23
- layers: scene.layers.map((layer: Layer) => ({ ...layer })),
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
- if (typeof structuredClone !== 'undefined') {
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
- * Optimized shallow clone for Layer
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((layer: Layer) => ({ ...layer }));
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((layer: Layer) => ({
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
- * Optimized clone for Audio configuration
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 cloneArray<T>(array: T[]): T[] {
107
- return [...array];
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 that handles edge cases
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
- * Resolution multipliers for file size estimation
10
- */
11
- export const RESOLUTION_MULTIPLIERS: Record<string, number> = {
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, { useMemo } from "react";
7
- import {
8
- View,
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 { COLLAGE_LAYOUTS } from "../../infrastructure/constants/collage.constants";
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
- {/* Canvas */}
135
- <View style={styles.canvas}>
136
- {layout.grid.map((cell, index) => {
137
- const [cx, cy, cw, ch] = cell;
138
- const cellStyle = {
139
- left: cx * size + spacing,
140
- top: cy * size + spacing,
141
- width: cw * size - spacing * 2,
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
- {/* Spacing + Border Radius */}
170
- <View style={styles.controls}>
171
- <View style={styles.controlRow}>
172
- <AtomicText type="labelSmall" color="textSecondary">
173
- {t("editor.collage.spacing") || "Spacing"}
174
- </AtomicText>
175
- <View style={styles.stepper}>
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
- <View style={styles.controlRow}>
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
- <ScrollView
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
  );