@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.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/src/application/services/EditorService.ts +151 -0
  3. package/src/application/usecases/LayerUseCases.ts +192 -0
  4. package/src/infrastructure/repositories/LayerRepositoryImpl.ts +158 -0
  5. package/src/infrastructure/services/image-layer-operations.service.ts +1 -1
  6. package/src/infrastructure/services/shape-layer-operations.service.ts +1 -1
  7. package/src/infrastructure/services/text-layer-operations.service.ts +1 -1
  8. package/src/infrastructure/utils/debounce.utils.ts +69 -0
  9. package/src/infrastructure/utils/image-processing.utils.ts +73 -0
  10. package/src/infrastructure/utils/position-calculations.utils.ts +45 -161
  11. package/src/presentation/components/EditorTimeline.tsx +29 -11
  12. package/src/presentation/components/EditorToolPanel.tsx +71 -172
  13. package/src/presentation/components/LayerActionsMenu.tsx +97 -159
  14. package/src/presentation/components/SceneActionsMenu.tsx +34 -44
  15. package/src/presentation/components/SubtitleListPanel.tsx +55 -28
  16. package/src/presentation/components/SubtitleStylePicker.tsx +36 -156
  17. package/src/presentation/components/collage/CollageCanvas.tsx +2 -2
  18. package/src/presentation/components/collage/CollageLayoutSelector.tsx +0 -4
  19. package/src/presentation/components/draggable-layer/LayerContent.tsx +7 -2
  20. package/src/presentation/components/generic/ActionMenu.tsx +110 -0
  21. package/src/presentation/components/generic/Editor.tsx +65 -0
  22. package/src/presentation/components/generic/Selector.tsx +96 -0
  23. package/src/presentation/components/generic/Toolbar.tsx +77 -0
  24. package/src/presentation/components/image-layer/ImagePreview.tsx +7 -1
  25. package/src/presentation/components/shape-layer/ColorPickerHorizontal.tsx +20 -55
  26. package/src/presentation/components/subtitle/SubtitleListItem.tsx +4 -5
  27. package/src/presentation/components/subtitle/SubtitleModal.tsx +2 -2
  28. package/src/presentation/components/subtitle/useSubtitleForm.ts +1 -1
  29. package/src/presentation/components/text-layer/ColorPicker.tsx +21 -55
  30. package/src/presentation/components/text-layer/FontSizeSelector.tsx +19 -50
  31. package/src/presentation/components/text-layer/OptionSelector.tsx +18 -55
  32. package/src/presentation/components/text-layer/TextAlignSelector.tsx +24 -51
  33. package/src/presentation/hooks/generic/use-layer-form.hook.ts +19 -16
  34. package/src/presentation/hooks/generic/useForm.ts +99 -0
  35. package/src/presentation/hooks/generic/useList.ts +117 -0
  36. package/src/presentation/hooks/useDraggableLayerGestures.ts +28 -25
  37. package/src/presentation/hooks/useEditorPlayback.ts +19 -2
  38. package/src/presentation/hooks/useImageLayerForm.ts +9 -6
  39. package/src/presentation/hooks/useMenuActions.tsx +19 -4
  40. package/src/presentation/hooks/useTextLayerForm.ts +9 -6
@@ -1,182 +1,66 @@
1
1
  /**
2
2
  * Position & Size Calculation Utilities
3
- * Centralized position and size calculations for video editor
4
3
  */
5
4
 
6
- export interface Position {
7
- x: number;
8
- y: number;
9
- }
5
+ export interface Position { x: number; y: number; }
6
+ export interface Size { width: number; height: number; }
10
7
 
11
- export interface Size {
12
- width: number;
13
- height: number;
14
- }
15
-
16
- /**
17
- * Convert percentage position to canvas pixels
18
- */
19
- export function percentageToPixels(
20
- percentage: number,
21
- canvasSize: number,
22
- ): number {
8
+ export const percentageToPixels = (percentage: number, canvasSize: number): number => {
23
9
  if (canvasSize <= 0) return 0;
24
10
  return (percentage / 100) * canvasSize;
25
- }
11
+ };
26
12
 
27
- /**
28
- * Convert canvas pixels to percentage
29
- */
30
- export function pixelsToPercentage(
31
- pixels: number,
32
- canvasSize: number,
33
- ): number {
13
+ export const pixelsToPercentage = (pixels: number, canvasSize: number): number => {
34
14
  if (canvasSize <= 0) return 0;
35
15
  return (pixels / canvasSize) * 100;
36
- }
16
+ };
37
17
 
38
- /**
39
- * Convert position from percentage to pixels
40
- */
41
- export function positionToPixels(
42
- position: Position,
43
- canvasWidth: number,
44
- canvasHeight: number,
45
- ): Position {
46
- return {
47
- x: percentageToPixels(position.x, canvasWidth),
48
- y: percentageToPixels(position.y, canvasHeight),
49
- };
50
- }
18
+ export const positionToPixels = (position: Position, canvasWidth: number, canvasHeight: number): Position => ({
19
+ x: percentageToPixels(position.x, canvasWidth),
20
+ y: percentageToPixels(position.y, canvasHeight),
21
+ });
51
22
 
52
- /**
53
- * Convert position from pixels to percentage
54
- */
55
- export function positionToPercentage(
56
- position: Position,
57
- canvasWidth: number,
58
- canvasHeight: number,
59
- ): Position {
60
- return {
61
- x: pixelsToPercentage(position.x, canvasWidth),
62
- y: pixelsToPercentage(position.y, canvasHeight),
63
- };
64
- }
23
+ export const positionToPercentage = (position: Position, canvasWidth: number, canvasHeight: number): Position => ({
24
+ x: pixelsToPercentage(position.x, canvasWidth),
25
+ y: pixelsToPercentage(position.y, canvasHeight),
26
+ });
65
27
 
66
- /**
67
- * Convert size from percentage to pixels
68
- */
69
- export function sizeToPixels(
70
- size: Size,
71
- canvasWidth: number,
72
- canvasHeight: number,
73
- ): Size {
74
- return {
75
- width: percentageToPixels(size.width, canvasWidth),
76
- height: percentageToPixels(size.height, canvasHeight),
77
- };
78
- }
28
+ export const sizeToPixels = (size: Size, canvasWidth: number, canvasHeight: number): Size => ({
29
+ width: percentageToPixels(size.width, canvasWidth),
30
+ height: percentageToPixels(size.height, canvasHeight),
31
+ });
79
32
 
80
- /**
81
- * Convert size from pixels to percentage
82
- */
83
- export function sizeToPercentage(
84
- size: Size,
85
- canvasWidth: number,
86
- canvasHeight: number,
87
- ): Size {
88
- return {
89
- width: pixelsToPercentage(size.width, canvasWidth),
90
- height: pixelsToPercentage(size.height, canvasHeight),
91
- };
92
- }
33
+ export const sizeToPercentage = (size: Size, canvasWidth: number, canvasHeight: number): Size => ({
34
+ width: pixelsToPercentage(size.width, canvasWidth),
35
+ height: pixelsToPercentage(size.height, canvasHeight),
36
+ });
93
37
 
94
- /**
95
- * Clamp value between min and max
96
- */
97
- export function clamp(value: number, min: number, max: number): number {
98
- return Math.max(min, Math.min(max, value));
99
- }
38
+ export const clamp = (value: number, min: number, max: number): number => Math.max(min, Math.min(max, value));
100
39
 
101
- /**
102
- * Clamp position to canvas bounds (in pixels)
103
- */
104
- export function clampPositionToCanvas(
105
- position: Position,
106
- canvasWidth: number,
107
- canvasHeight: number,
108
- elementWidth: number,
109
- elementHeight: number,
110
- ): Position {
111
- return {
112
- x: clamp(position.x, 0, canvasWidth - elementWidth),
113
- y: clamp(position.y, 0, canvasHeight - elementHeight),
114
- };
115
- }
40
+ export const clampPositionToCanvas = (position: Position, canvasWidth: number, canvasHeight: number, elementWidth: number, elementHeight: number): Position => ({
41
+ x: clamp(position.x, 0, canvasWidth - elementWidth),
42
+ y: clamp(position.y, 0, canvasHeight - elementHeight),
43
+ });
116
44
 
117
- /**
118
- * Clamp position to percentage bounds (0-100)
119
- */
120
- export function clampPositionPercentage(position: Position): Position {
121
- return {
122
- x: clamp(position.x, 0, 100),
123
- y: clamp(position.y, 0, 100),
124
- };
125
- }
45
+ export const clampPositionPercentage = (position: Position): Position => ({
46
+ x: clamp(position.x, 0, 100),
47
+ y: clamp(position.y, 0, 100),
48
+ });
126
49
 
127
- /**
128
- * Clamp size to minimum and maximum percentage values
129
- */
130
- export function clampSizePercentage(
131
- size: Size,
132
- minSize: number = 1,
133
- maxSize: number = 100,
134
- ): Size {
135
- return {
136
- width: clamp(size.width, minSize, maxSize),
137
- height: clamp(size.height, minSize, maxSize),
138
- };
139
- }
50
+ export const clampSizePercentage = (size: Size, minSize: number = 1, maxSize: number = 100): Size => ({
51
+ width: clamp(size.width, minSize, maxSize),
52
+ height: clamp(size.height, minSize, maxSize),
53
+ });
140
54
 
141
- /**
142
- * Calculate center position for an element
143
- */
144
- export function calculateCenterPosition(
145
- elementWidth: number,
146
- elementHeight: number,
147
- canvasWidth: number,
148
- canvasHeight: number,
149
- ): Position {
150
- return {
151
- x: (canvasWidth - elementWidth) / 2,
152
- y: (canvasHeight - elementHeight) / 2,
153
- };
154
- }
55
+ export const calculateCenterPosition = (elementWidth: number, elementHeight: number, canvasWidth: number, canvasHeight: number): Position => ({
56
+ x: (canvasWidth - elementWidth) / 2,
57
+ y: (canvasHeight - elementHeight) / 2,
58
+ });
155
59
 
156
- /**
157
- * Offset position by delta
158
- */
159
- export function offsetPosition(position: Position, deltaX: number, deltaY: number): Position {
160
- return {
161
- x: position.x + deltaX,
162
- y: position.y + deltaY,
163
- };
164
- }
60
+ export const offsetPosition = (position: Position, deltaX: number, deltaY: number): Position => ({
61
+ x: position.x + deltaX,
62
+ y: position.y + deltaY,
63
+ });
165
64
 
166
- /**
167
- * Check if position is within canvas bounds
168
- */
169
- export function isPositionInBounds(
170
- position: Position,
171
- canvasWidth: number,
172
- canvasHeight: number,
173
- elementWidth: number,
174
- elementHeight: number,
175
- ): boolean {
176
- return (
177
- position.x >= 0 &&
178
- position.y >= 0 &&
179
- position.x + elementWidth <= canvasWidth &&
180
- position.y + elementHeight <= canvasHeight
181
- );
182
- }
65
+ export const isPositionInBounds = (position: Position, canvasWidth: number, canvasHeight: number, elementWidth: number, elementHeight: number): boolean =>
66
+ position.x >= 0 && position.y >= 0 && position.x + elementWidth <= canvasWidth && position.y + elementHeight <= canvasHeight;
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Editor Timeline Component
3
3
  * Single Responsibility: Display scene timeline
4
+ * PERFORMANCE: Uses FlatList for efficient horizontal scene rendering
4
5
  */
5
6
 
6
- import React, { useMemo } from "react";
7
- import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
7
+ import React, { useMemo, useCallback } from "react";
8
+ import { View, FlatList, TouchableOpacity, StyleSheet } 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 { useLocalization } from "@umituz/react-native-settings";
@@ -131,15 +132,13 @@ export const EditorTimeline: React.FC<EditorTimelineProps> = ({
131
132
  </TouchableOpacity>
132
133
  </View>
133
134
 
134
- <ScrollView
135
+ <FlatList
135
136
  horizontal
136
- showsHorizontalScrollIndicator={false}
137
- style={styles.scenesScroll}
138
- >
139
- {project.scenes.map((scene: Scene, index: number) => (
137
+ data={project.scenes}
138
+ keyExtractor={(scene) => scene.id}
139
+ renderItem={useCallback(({ item, index }: { item: Scene; index: number }) => (
140
140
  <SceneCard
141
- key={scene.id}
142
- scene={scene}
141
+ scene={item}
143
142
  index={index}
144
143
  isSelected={currentSceneIndex === index}
145
144
  onSelect={onSceneSelect}
@@ -151,8 +150,27 @@ export const EditorTimeline: React.FC<EditorTimelineProps> = ({
151
150
  onPrimaryColor={tokens.colors.onPrimary}
152
151
  textPrimaryColor={tokens.colors.textPrimary}
153
152
  />
154
- ))}
155
- </ScrollView>
153
+ ), [currentSceneIndex, onSceneSelect, onSceneLongPress, tokens.colors])}
154
+ showsHorizontalScrollIndicator={false}
155
+ style={styles.scenesScroll}
156
+ // Performance optimizations for horizontal lists
157
+ removeClippedSubviews={true}
158
+ maxToRenderPerBatch={5}
159
+ updateCellsBatchingPeriod={50}
160
+ initialNumToRender={5}
161
+ windowSize={5}
162
+ // Prevents layout flicker
163
+ getItemLayout={(data, index) => ({
164
+ length: 84, // sceneCard width + marginRight
165
+ offset: 84 * index,
166
+ index,
167
+ })}
168
+ // Maintain scroll position
169
+ maintainVisibleContentPosition={{
170
+ minIndexForVisible: 0,
171
+ autoscrollToTopThreshold: 10,
172
+ }}
173
+ />
156
174
  </View>
157
175
  );
158
176
  };
@@ -1,18 +1,15 @@
1
1
  /**
2
2
  * Editor Tool Panel Component
3
3
  * Single Responsibility: Display editor tool buttons
4
+ * REFACTORED: Uses generic Toolbar component (52 lines)
4
5
  */
5
6
 
6
- import React, { useMemo, useCallback } from "react";
7
- import {
8
- View,
9
- ScrollView,
10
- TouchableOpacity,
11
- StyleSheet,
12
- } from "react-native";
13
- import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
7
+ import React, { useMemo } from "react";
8
+ import { View } from "react-native";
9
+ import { AtomicText } from "@umituz/react-native-design-system/atoms";
14
10
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
15
11
  import { useLocalization } from "@umituz/react-native-settings";
12
+ import { Toolbar, type ToolbarButton, type ToolbarSection } from "./generic/Toolbar";
16
13
 
17
14
  interface EditorToolPanelProps {
18
15
  onAddText: () => void;
@@ -24,69 +21,6 @@ interface EditorToolPanelProps {
24
21
  onSpeed?: () => void;
25
22
  }
26
23
 
27
- interface ToolButtonProps {
28
- icon: string;
29
- label: string;
30
- onPress: () => void;
31
- backgroundColor: string;
32
- textColor: string;
33
- showBadge?: boolean;
34
- badgeColor?: string;
35
- badgeBorderColor?: string;
36
- }
37
-
38
- /**
39
- * Memoized ToolButton component to prevent unnecessary re-renders
40
- */
41
- const ToolButton = React.memo<ToolButtonProps>(({
42
- icon,
43
- label,
44
- onPress,
45
- backgroundColor,
46
- textColor,
47
- showBadge,
48
- badgeColor,
49
- badgeBorderColor,
50
- }) => {
51
- const buttonStyle = useMemo(() => [
52
- styles.toolButton,
53
- { backgroundColor }
54
- ], [backgroundColor]);
55
-
56
- const textStyle = useMemo(() => ({
57
- color: textColor,
58
- marginTop: 4
59
- }), [textColor]);
60
-
61
- const badgeStyle = useMemo(() => [
62
- styles.audioBadge,
63
- {
64
- backgroundColor: badgeColor,
65
- borderColor: badgeBorderColor,
66
- }
67
- ], [badgeColor, badgeBorderColor]);
68
-
69
- return (
70
- <TouchableOpacity
71
- style={buttonStyle}
72
- onPress={onPress}
73
- >
74
- <AtomicIcon name={icon as any} size="md" color="primary" />
75
- <AtomicText
76
- type="labelSmall"
77
- style={textStyle}
78
- >
79
- {label}
80
- </AtomicText>
81
- {showBadge && badgeColor && (
82
- <View style={badgeStyle} />
83
- )}
84
- </TouchableOpacity>
85
- );
86
- });
87
-
88
- ToolButton.displayName = 'ToolButton';
89
-
90
24
  export const EditorToolPanel: React.FC<EditorToolPanelProps> = React.memo(({
91
25
  onAddText,
92
26
  onAddImage,
@@ -99,28 +33,70 @@ export const EditorToolPanel: React.FC<EditorToolPanelProps> = React.memo(({
99
33
  const { t } = useLocalization();
100
34
  const tokens = useAppDesignTokens();
101
35
 
102
- // Memoize styles
103
- const containerStyle = useMemo(() => [
104
- styles.toolPanel,
105
- { backgroundColor: tokens.colors.surface }
106
- ], [tokens.colors.surface]);
36
+ const toolbarSections = useMemo<ToolbarSection[]>(() => {
37
+ const buttons: ToolbarButton[] = [
38
+ {
39
+ id: "text",
40
+ icon: "text-outline",
41
+ label: t("editor.tools.text"),
42
+ onPress: onAddText,
43
+ },
44
+ {
45
+ id: "image",
46
+ icon: "image-outline",
47
+ label: t("editor.tools.image"),
48
+ onPress: onAddImage,
49
+ },
50
+ {
51
+ id: "shape",
52
+ icon: "square-outline",
53
+ label: t("editor.tools.shape"),
54
+ onPress: onAddShape,
55
+ },
56
+ {
57
+ id: "audio",
58
+ icon: "musical-notes-outline",
59
+ label: t("editor.tools.audio"),
60
+ onPress: onAudio,
61
+ showBadge: hasAudio,
62
+ badgeColor: tokens.colors.success,
63
+ },
64
+ ];
65
+
66
+ if (onFilters) {
67
+ buttons.push({
68
+ id: "filters",
69
+ icon: "sparkles",
70
+ label: t("editor.tools.filters") || "Filters",
71
+ onPress: onFilters,
72
+ });
73
+ }
74
+
75
+ if (onSpeed) {
76
+ buttons.push({
77
+ id: "speed",
78
+ icon: "flash",
79
+ label: t("editor.tools.speed") || "Speed",
80
+ onPress: onSpeed,
81
+ });
82
+ }
83
+
84
+ return [{ id: "tools", buttons }];
85
+ }, [t, onAddText, onAddImage, onAddShape, onAudio, hasAudio, onFilters, onSpeed, tokens.colors.success]);
86
+
87
+ const containerStyle = useMemo(() => ({
88
+ backgroundColor: tokens.colors.surface,
89
+ borderRadius: tokens.borders.radius.lg,
90
+ padding: tokens.spacing.md,
91
+ marginHorizontal: tokens.spacing.md,
92
+ marginBottom: tokens.spacing.md,
93
+ }), [tokens.colors.surface, tokens.borders.radius.lg, tokens.spacing.md]);
107
94
 
108
95
  const titleStyle = useMemo(() => ({
109
96
  color: tokens.colors.textPrimary,
110
97
  fontWeight: "600" as const,
111
- marginBottom: 12,
112
- }), [tokens.colors.textPrimary]);
113
-
114
- const backgroundColor = tokens.colors.backgroundPrimary;
115
- const textColor = tokens.colors.textPrimary;
116
-
117
- // Stable callbacks
118
- const handleAddText = useCallback(() => onAddText(), [onAddText]);
119
- const handleAddImage = useCallback(() => onAddImage(), [onAddImage]);
120
- const handleAddShape = useCallback(() => onAddShape(), [onAddShape]);
121
- const handleAudio = useCallback(() => onAudio(), [onAudio]);
122
- const handleFilters = useCallback(() => onFilters?.(), [onFilters]);
123
- const handleSpeed = useCallback(() => onSpeed?.(), [onSpeed]);
98
+ marginBottom: tokens.spacing.sm,
99
+ }), [tokens.colors.textPrimary, tokens.spacing.sm]);
124
100
 
125
101
  return (
126
102
  <View style={containerStyle}>
@@ -130,91 +106,14 @@ export const EditorToolPanel: React.FC<EditorToolPanelProps> = React.memo(({
130
106
  >
131
107
  {t("editor.tools.title")}
132
108
  </AtomicText>
133
-
134
- <ScrollView horizontal showsHorizontalScrollIndicator={false}>
135
- <ToolButton
136
- icon="text-outline"
137
- label={t("editor.tools.text")}
138
- onPress={handleAddText}
139
- backgroundColor={backgroundColor}
140
- textColor={textColor}
141
- />
142
-
143
- <ToolButton
144
- icon="image-outline"
145
- label={t("editor.tools.image")}
146
- onPress={handleAddImage}
147
- backgroundColor={backgroundColor}
148
- textColor={textColor}
149
- />
150
-
151
- <ToolButton
152
- icon="square-outline"
153
- label={t("editor.tools.shape")}
154
- onPress={handleAddShape}
155
- backgroundColor={backgroundColor}
156
- textColor={textColor}
157
- />
158
-
159
- <ToolButton
160
- icon="musical-notes-outline"
161
- label={t("editor.tools.audio")}
162
- onPress={handleAudio}
163
- backgroundColor={backgroundColor}
164
- textColor={textColor}
165
- showBadge={hasAudio}
166
- badgeColor={tokens.colors.success}
167
- badgeBorderColor={tokens.colors.surface}
168
- />
169
-
170
- {onFilters && (
171
- <ToolButton
172
- icon="sparkles"
173
- label={t("editor.tools.filters") || "Filters"}
174
- onPress={handleFilters}
175
- backgroundColor={backgroundColor}
176
- textColor={textColor}
177
- />
178
- )}
179
-
180
- {onSpeed && (
181
- <ToolButton
182
- icon="flash"
183
- label={t("editor.tools.speed") || "Speed"}
184
- onPress={handleSpeed}
185
- backgroundColor={backgroundColor}
186
- textColor={textColor}
187
- />
188
- )}
189
- </ScrollView>
109
+ <Toolbar
110
+ sections={toolbarSections}
111
+ orientation="horizontal"
112
+ scrollable
113
+ testID="editor-tool-panel"
114
+ />
190
115
  </View>
191
116
  );
192
117
  });
193
118
 
194
119
  EditorToolPanel.displayName = 'EditorToolPanel';
195
-
196
- const styles = StyleSheet.create({
197
- toolPanel: {
198
- padding: 16,
199
- marginHorizontal: 16,
200
- marginBottom: 16,
201
- borderRadius: 12,
202
- },
203
- toolButton: {
204
- width: 80,
205
- height: 80,
206
- borderRadius: 12,
207
- alignItems: "center",
208
- justifyContent: "center",
209
- marginRight: 12,
210
- },
211
- audioBadge: {
212
- position: "absolute",
213
- top: 8,
214
- right: 8,
215
- width: 10,
216
- height: 10,
217
- borderRadius: 5,
218
- borderWidth: 2,
219
- },
220
- });