@umituz/react-native-video-editor 1.1.53 → 1.1.55

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.
@@ -3,7 +3,7 @@
3
3
  * Draggable and resizable layer component
4
4
  */
5
5
 
6
- import React from "react";
6
+ import React, { useMemo, useCallback } from "react";
7
7
  import { View, StyleSheet } from "react-native";
8
8
  import { GestureDetector } from "react-native-gesture-handler";
9
9
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
@@ -11,6 +11,7 @@ import type { Layer } from "../../domain/entities/video-project.types";
11
11
  import { useDraggableLayerGestures } from "../hooks/useDraggableLayerGestures";
12
12
  import { LayerContent } from "./draggable-layer/LayerContent";
13
13
  import { ResizeHandles } from "./draggable-layer/ResizeHandles";
14
+ import { percentageToPixels } from "../../infrastructure/utils/position-calculations.utils";
14
15
 
15
16
  interface DraggableLayerProps {
16
17
  layer: Layer;
@@ -22,7 +23,7 @@ interface DraggableLayerProps {
22
23
  onSizeChange: (width: number, height: number) => void;
23
24
  }
24
25
 
25
- export const DraggableLayer: React.FC<DraggableLayerProps> = ({
26
+ export const DraggableLayer: React.FC<DraggableLayerProps> = React.memo(({
26
27
  layer,
27
28
  canvasWidth,
28
29
  canvasHeight,
@@ -36,10 +37,23 @@ export const DraggableLayer: React.FC<DraggableLayerProps> = ({
36
37
  const safeCanvasWidth = canvasWidth > 0 ? canvasWidth : 1;
37
38
  const safeCanvasHeight = canvasHeight > 0 ? canvasHeight : 1;
38
39
 
39
- const initialX = (layer.position.x / 100) * safeCanvasWidth;
40
- const initialY = (layer.position.y / 100) * safeCanvasHeight;
41
- const initialWidth = (layer.size.width / 100) * safeCanvasWidth;
42
- const initialHeight = (layer.size.height / 100) * safeCanvasHeight;
40
+ const initialX = percentageToPixels(layer.position.x, safeCanvasWidth);
41
+ const initialY = percentageToPixels(layer.position.y, safeCanvasHeight);
42
+ const initialWidth = percentageToPixels(layer.size.width, safeCanvasWidth);
43
+ const initialHeight = percentageToPixels(layer.size.height, safeCanvasHeight);
44
+
45
+ // Stable callbacks to prevent gesture re-creation
46
+ const handlePositionChange = useCallback((x: number, y: number) => {
47
+ onPositionChange(x, y);
48
+ }, [onPositionChange]);
49
+
50
+ const handleSizeChange = useCallback((width: number, height: number) => {
51
+ onSizeChange(width, height);
52
+ }, [onSizeChange]);
53
+
54
+ const handleSelect = useCallback(() => {
55
+ onSelect();
56
+ }, [onSelect]);
43
57
 
44
58
  const {
45
59
  state,
@@ -55,12 +69,13 @@ export const DraggableLayer: React.FC<DraggableLayerProps> = ({
55
69
  initialHeight,
56
70
  canvasWidth: safeCanvasWidth,
57
71
  canvasHeight: safeCanvasHeight,
58
- onSelect,
59
- onPositionChange,
60
- onSizeChange,
72
+ onSelect: handleSelect,
73
+ onPositionChange: handlePositionChange,
74
+ onSizeChange: handleSizeChange,
61
75
  });
62
76
 
63
- const layerStyle = {
77
+ // Memoize layer style to prevent new object on every render
78
+ const layerStyle = useMemo(() => ({
64
79
  transform: [
65
80
  { translateX: state.x },
66
81
  { translateY: state.y },
@@ -69,17 +84,20 @@ export const DraggableLayer: React.FC<DraggableLayerProps> = ({
69
84
  opacity: layer.opacity,
70
85
  width: state.width,
71
86
  height: state.height,
72
- };
87
+ }), [state.x, state.y, state.width, state.height, layer.rotation, layer.opacity]);
88
+
89
+ // Memoize border style
90
+ const borderStyle = useMemo(() => ({
91
+ borderColor: isSelected ? tokens.colors.primary : "transparent",
92
+ borderWidth: isSelected ? 2 : 0,
93
+ }), [isSelected, tokens.colors.primary]);
73
94
 
74
95
  return (
75
96
  <GestureDetector gesture={composedGesture}>
76
97
  <View
77
98
  style={[
78
99
  styles.layer,
79
- {
80
- borderColor: isSelected ? tokens.colors.primary : "transparent",
81
- borderWidth: isSelected ? 2 : 0,
82
- },
100
+ borderStyle,
83
101
  layerStyle,
84
102
  ]}
85
103
  >
@@ -96,7 +114,9 @@ export const DraggableLayer: React.FC<DraggableLayerProps> = ({
96
114
  </View>
97
115
  </GestureDetector>
98
116
  );
99
- };
117
+ });
118
+
119
+ DraggableLayer.displayName = 'DraggableLayer';
100
120
 
101
121
  const styles = StyleSheet.create({
102
122
  layer: {
@@ -1,15 +1,17 @@
1
1
  /**
2
2
  * Editor Preview Area Component
3
3
  * Single Responsibility: Display video preview canvas and playback controls
4
+ * Optimized to prevent unnecessary re-renders
4
5
  */
5
6
 
6
- import React, { useMemo } from "react";
7
+ import React, { useMemo, useCallback } from "react";
7
8
  import { View, TouchableOpacity, Dimensions } from "react-native";
8
9
  import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
9
10
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
10
11
  import { DraggableLayer } from "./DraggableLayer";
11
12
  import type { Scene, Layer } from "../../domain/entities/video-project.types";
12
13
  import { createPreviewStyles } from "./EditorPreviewArea.styles";
14
+ import { formatTimeDisplay, calculateProgressPercent } from "../../infrastructure/utils/time-calculations.utils";
13
15
 
14
16
  const { width } = Dimensions.get("window");
15
17
  const PREVIEW_ASPECT_RATIO = 16 / 9;
@@ -28,7 +30,7 @@ interface EditorPreviewAreaProps {
28
30
  onReset: () => void;
29
31
  }
30
32
 
31
- export const EditorPreviewArea: React.FC<EditorPreviewAreaProps> = ({
33
+ export const EditorPreviewArea: React.FC<EditorPreviewAreaProps> = React.memo(({
32
34
  scene,
33
35
  selectedLayerId,
34
36
  isPlaying,
@@ -43,26 +45,88 @@ export const EditorPreviewArea: React.FC<EditorPreviewAreaProps> = ({
43
45
  const tokens = useAppDesignTokens();
44
46
  const styles = useMemo(() => createPreviewStyles(tokens), [tokens]);
45
47
 
48
+ // Memoize canvas style
49
+ const canvasStyle = useMemo(() => [
50
+ styles.previewCanvas,
51
+ {
52
+ backgroundColor:
53
+ scene.background.type === "color"
54
+ ? scene.background.value
55
+ : tokens.colors.surfaceSecondary,
56
+ height: PREVIEW_HEIGHT,
57
+ },
58
+ ], [styles.previewCanvas, scene.background.type, scene.background.value, tokens.colors.surfaceSecondary]);
59
+
60
+ // Memoize empty preview text style
61
+ const emptyTextStyle = useMemo(() => ({
62
+ color: tokens.colors.textSecondary,
63
+ marginTop: 12,
64
+ }), [tokens.colors.textSecondary]);
65
+
66
+ // Memoize layer actions button style
67
+ const layerActionsButtonStyle = useMemo(() => [
68
+ styles.layerActionsButton,
69
+ { backgroundColor: tokens.colors.primary },
70
+ ], [styles.layerActionsButton, tokens.colors.primary]);
71
+
72
+ // Stable callbacks for layer operations
73
+ const handleLayerSelect = useCallback((layerId: string) => {
74
+ onLayerSelect(layerId);
75
+ }, [onLayerSelect]);
76
+
77
+ const createLayerPositionHandler = useCallback((layerId: string) => {
78
+ return (x: number, y: number) => {
79
+ onLayerPositionChange(layerId, x, y);
80
+ };
81
+ }, [onLayerPositionChange]);
82
+
83
+ const createLayerSizeHandler = useCallback((layerId: string) => {
84
+ return (width: number, height: number) => {
85
+ onLayerSizeChange(layerId, width, height);
86
+ };
87
+ }, [onLayerSizeChange]);
88
+
89
+ // Stable callback for layer actions button
90
+ const handleLayerActionsPress = useCallback(() => {
91
+ const layer = scene.layers.find((l) => l.id === selectedLayerId);
92
+ if (layer) {
93
+ onLayerActionsPress(layer);
94
+ }
95
+ }, [scene.layers, selectedLayerId, onLayerActionsPress]);
96
+
97
+ // Memoize playback controls style
98
+ const playbackControlsStyle = useMemo(() => [
99
+ styles.playbackControls,
100
+ { backgroundColor: tokens.colors.surface },
101
+ ], [styles.playbackControls, tokens.colors.surface]);
102
+
103
+ // Memoize progress bar container style
104
+ const progressBarContainerStyle = useMemo(() => [
105
+ styles.progressBarContainer,
106
+ { backgroundColor: tokens.colors.borderLight },
107
+ ], [styles.progressBarContainer, tokens.colors.borderLight]);
108
+
109
+ // Memoize progress bar style
110
+ const progressBarStyle = useMemo(() => ({
111
+ ...styles.progressBar,
112
+ width: `${calculateProgressPercent(currentTime, scene.duration)}%` as any,
113
+ backgroundColor: tokens.colors.primary,
114
+ }), [styles.progressBar, currentTime, scene.duration, tokens.colors.primary]);
115
+
116
+ // Memoize time text style
117
+ const timeTextStyle = useMemo(() => ({
118
+ color: tokens.colors.textSecondary,
119
+ }), [tokens.colors.textSecondary]);
120
+
46
121
  return (
47
122
  <View style={styles.previewSection}>
48
- <View
49
- style={[
50
- styles.previewCanvas,
51
- {
52
- backgroundColor:
53
- scene.background.type === "color"
54
- ? scene.background.value
55
- : tokens.colors.surfaceSecondary,
56
- height: PREVIEW_HEIGHT,
57
- },
58
- ]}
59
- >
123
+ <View style={canvasStyle}>
60
124
  {scene.layers.length === 0 ? (
61
125
  <View style={styles.emptyPreview}>
62
126
  <AtomicIcon name="film-outline" size="xl" color="secondary" />
63
127
  <AtomicText
64
128
  type="bodyMedium"
65
- style={{ color: tokens.colors.textSecondary, marginTop: 12 }}
129
+ style={emptyTextStyle}
66
130
  >
67
131
  Canvas is empty. Add layers to get started.
68
132
  </AtomicText>
@@ -76,28 +140,16 @@ export const EditorPreviewArea: React.FC<EditorPreviewAreaProps> = ({
76
140
  canvasWidth={width}
77
141
  canvasHeight={PREVIEW_HEIGHT}
78
142
  isSelected={selectedLayerId === layer.id}
79
- onSelect={() => onLayerSelect(layer.id)}
80
- onPositionChange={(x, y) =>
81
- onLayerPositionChange(layer.id, x, y)
82
- }
83
- onSizeChange={(w, h) => onLayerSizeChange(layer.id, w, h)}
143
+ onSelect={() => handleLayerSelect(layer.id)}
144
+ onPositionChange={createLayerPositionHandler(layer.id)}
145
+ onSizeChange={createLayerSizeHandler(layer.id)}
84
146
  />
85
147
  ))}
86
148
 
87
149
  {selectedLayerId && (
88
150
  <TouchableOpacity
89
- style={[
90
- styles.layerActionsButton,
91
- { backgroundColor: tokens.colors.primary },
92
- ]}
93
- onPress={() => {
94
- const layer = scene.layers.find(
95
- (l) => l.id === selectedLayerId,
96
- );
97
- if (layer) {
98
- onLayerActionsPress(layer);
99
- }
100
- }}
151
+ style={layerActionsButtonStyle}
152
+ onPress={handleLayerActionsPress}
101
153
  >
102
154
  <AtomicIcon
103
155
  name="ellipsis-vertical"
@@ -110,12 +162,7 @@ export const EditorPreviewArea: React.FC<EditorPreviewAreaProps> = ({
110
162
  )}
111
163
  </View>
112
164
 
113
- <View
114
- style={[
115
- styles.playbackControls,
116
- { backgroundColor: tokens.colors.surface },
117
- ]}
118
- >
165
+ <View style={playbackControlsStyle}>
119
166
  <View style={styles.playbackRow}>
120
167
  <TouchableOpacity onPress={onPlayPause} style={styles.playButton}>
121
168
  <AtomicIcon
@@ -128,10 +175,9 @@ export const EditorPreviewArea: React.FC<EditorPreviewAreaProps> = ({
128
175
  <View style={styles.timeDisplay}>
129
176
  <AtomicText
130
177
  type="labelSmall"
131
- style={{ color: tokens.colors.textSecondary }}
178
+ style={timeTextStyle}
132
179
  >
133
- {Math.floor(currentTime / 1000)}s /{" "}
134
- {Math.floor(scene.duration / 1000)}s
180
+ {formatTimeDisplay(currentTime, true)} / {formatTimeDisplay(scene.duration, true)}
135
181
  </AtomicText>
136
182
  </View>
137
183
 
@@ -140,23 +186,12 @@ export const EditorPreviewArea: React.FC<EditorPreviewAreaProps> = ({
140
186
  </TouchableOpacity>
141
187
  </View>
142
188
 
143
- <View
144
- style={[
145
- styles.progressBarContainer,
146
- { backgroundColor: tokens.colors.borderLight },
147
- ]}
148
- >
149
- <View
150
- style={[
151
- styles.progressBar,
152
- {
153
- width: `${(currentTime / scene.duration) * 100}%`,
154
- backgroundColor: tokens.colors.primary,
155
- },
156
- ]}
157
- />
189
+ <View style={progressBarContainerStyle}>
190
+ <View style={progressBarStyle} />
158
191
  </View>
159
192
  </View>
160
193
  </View>
161
194
  );
162
- };
195
+ });
196
+
197
+ EditorPreviewArea.displayName = 'EditorPreviewArea';
@@ -3,7 +3,7 @@
3
3
  * Single Responsibility: Display scene timeline
4
4
  */
5
5
 
6
- import React from "react";
6
+ import React, { useMemo } from "react";
7
7
  import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
8
8
  import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
9
9
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
@@ -18,6 +18,84 @@ interface EditorTimelineProps {
18
18
  onAddScene: () => void;
19
19
  }
20
20
 
21
+ interface SceneCardProps {
22
+ scene: Scene;
23
+ index: number;
24
+ isSelected: boolean;
25
+ onSelect: (index: number) => void;
26
+ onLongPress: (index: number) => void;
27
+ primaryColor: string;
28
+ backgroundColor: string;
29
+ borderLightColor: string;
30
+ surfaceSecondaryColor: string;
31
+ onPrimaryColor: string;
32
+ textPrimaryColor: string;
33
+ }
34
+
35
+ /**
36
+ * Memoized SceneCard component to prevent unnecessary re-renders
37
+ * Only re-renders when its own props change, not when other scenes change
38
+ */
39
+ const SceneCard = React.memo<SceneCardProps>(({
40
+ scene,
41
+ index,
42
+ isSelected,
43
+ onSelect,
44
+ onLongPress,
45
+ primaryColor,
46
+ backgroundColor,
47
+ borderLightColor,
48
+ surfaceSecondaryColor,
49
+ onPrimaryColor,
50
+ textPrimaryColor,
51
+ }) => {
52
+ const cardStyle = useMemo(() => [
53
+ styles.sceneCard,
54
+ {
55
+ backgroundColor: isSelected ? `${primaryColor}20` : backgroundColor,
56
+ borderColor: isSelected ? primaryColor : borderLightColor,
57
+ },
58
+ ], [isSelected, primaryColor, backgroundColor, borderLightColor]);
59
+
60
+ const thumbnailStyle = useMemo(() => [
61
+ styles.sceneThumbnail,
62
+ {
63
+ backgroundColor: scene.background.value || surfaceSecondaryColor,
64
+ },
65
+ ], [scene.background.value, surfaceSecondaryColor]);
66
+
67
+ const textStyle = useMemo(() => ({
68
+ color: textPrimaryColor,
69
+ marginTop: 4,
70
+ fontWeight: isSelected ? "600" as const : "400" as const,
71
+ }), [isSelected, textPrimaryColor]);
72
+
73
+ return (
74
+ <TouchableOpacity
75
+ style={cardStyle}
76
+ onPress={() => onSelect(index)}
77
+ onLongPress={() => onLongPress(index)}
78
+ >
79
+ <View style={thumbnailStyle}>
80
+ <AtomicText
81
+ type="labelSmall"
82
+ style={{ color: onPrimaryColor, fontWeight: "600" }}
83
+ >
84
+ {index + 1}
85
+ </AtomicText>
86
+ </View>
87
+ <AtomicText
88
+ type="labelSmall"
89
+ style={textStyle}
90
+ >
91
+ {scene.duration / 1000}s
92
+ </AtomicText>
93
+ </TouchableOpacity>
94
+ );
95
+ });
96
+
97
+ SceneCard.displayName = 'SceneCard';
98
+
21
99
  export const EditorTimeline: React.FC<EditorTimelineProps> = ({
22
100
  project,
23
101
  currentSceneIndex,
@@ -28,12 +106,23 @@ export const EditorTimeline: React.FC<EditorTimelineProps> = ({
28
106
  const { t } = useLocalization();
29
107
  const tokens = useAppDesignTokens();
30
108
 
109
+ // Memoize styles that don't change
110
+ const containerStyle = useMemo(() => [
111
+ styles.timeline,
112
+ { backgroundColor: tokens.colors.surface }
113
+ ], [tokens.colors.surface]);
114
+
115
+ const headerTextStyle = useMemo(() => ({
116
+ color: tokens.colors.textPrimary,
117
+ fontWeight: "600" as const,
118
+ }), [tokens.colors.textPrimary]);
119
+
31
120
  return (
32
- <View style={[styles.timeline, { backgroundColor: tokens.colors.surface }]}>
121
+ <View style={containerStyle}>
33
122
  <View style={styles.timelineHeader}>
34
123
  <AtomicText
35
124
  type="bodyMedium"
36
- style={{ color: tokens.colors.textPrimary, fontWeight: "600" }}
125
+ style={headerTextStyle}
37
126
  >
38
127
  {t("editor.timeline.title")}
39
128
  </AtomicText>
@@ -48,51 +137,20 @@ export const EditorTimeline: React.FC<EditorTimelineProps> = ({
48
137
  style={styles.scenesScroll}
49
138
  >
50
139
  {project.scenes.map((scene: Scene, index: number) => (
51
- <TouchableOpacity
140
+ <SceneCard
52
141
  key={scene.id}
53
- style={[
54
- styles.sceneCard,
55
- {
56
- backgroundColor:
57
- currentSceneIndex === index
58
- ? tokens.colors.primary + "20"
59
- : tokens.colors.backgroundPrimary,
60
- borderColor:
61
- currentSceneIndex === index
62
- ? tokens.colors.primary
63
- : tokens.colors.borderLight,
64
- },
65
- ]}
66
- onPress={() => onSceneSelect(index)}
67
- onLongPress={() => onSceneLongPress(index)}
68
- >
69
- <View
70
- style={[
71
- styles.sceneThumbnail,
72
- {
73
- backgroundColor:
74
- scene.background.value || tokens.colors.surfaceSecondary,
75
- },
76
- ]}
77
- >
78
- <AtomicText
79
- type="labelSmall"
80
- style={{ color: tokens.colors.onPrimary, fontWeight: "600" }}
81
- >
82
- {index + 1}
83
- </AtomicText>
84
- </View>
85
- <AtomicText
86
- type="labelSmall"
87
- style={{
88
- color: tokens.colors.textPrimary,
89
- marginTop: 4,
90
- fontWeight: currentSceneIndex === index ? "600" : "400",
91
- }}
92
- >
93
- {scene.duration / 1000}s
94
- </AtomicText>
95
- </TouchableOpacity>
142
+ scene={scene}
143
+ index={index}
144
+ isSelected={currentSceneIndex === index}
145
+ onSelect={onSceneSelect}
146
+ onLongPress={onSceneLongPress}
147
+ primaryColor={tokens.colors.primary}
148
+ backgroundColor={tokens.colors.backgroundPrimary}
149
+ borderLightColor={tokens.colors.borderLight}
150
+ surfaceSecondaryColor={tokens.colors.surfaceSecondary}
151
+ onPrimaryColor={tokens.colors.onPrimary}
152
+ textPrimaryColor={tokens.colors.textPrimary}
153
+ />
96
154
  ))}
97
155
  </ScrollView>
98
156
  </View>