@umituz/react-native-video-editor 1.1.64 → 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 (29) 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/utils/debounce.utils.ts +69 -0
  6. package/src/infrastructure/utils/image-processing.utils.ts +73 -0
  7. package/src/infrastructure/utils/position-calculations.utils.ts +45 -161
  8. package/src/presentation/components/EditorTimeline.tsx +29 -11
  9. package/src/presentation/components/EditorToolPanel.tsx +71 -172
  10. package/src/presentation/components/LayerActionsMenu.tsx +97 -159
  11. package/src/presentation/components/SceneActionsMenu.tsx +34 -44
  12. package/src/presentation/components/SubtitleListPanel.tsx +54 -27
  13. package/src/presentation/components/SubtitleStylePicker.tsx +36 -156
  14. package/src/presentation/components/draggable-layer/LayerContent.tsx +7 -2
  15. package/src/presentation/components/generic/ActionMenu.tsx +110 -0
  16. package/src/presentation/components/generic/Editor.tsx +65 -0
  17. package/src/presentation/components/generic/Selector.tsx +96 -0
  18. package/src/presentation/components/generic/Toolbar.tsx +77 -0
  19. package/src/presentation/components/image-layer/ImagePreview.tsx +7 -1
  20. package/src/presentation/components/shape-layer/ColorPickerHorizontal.tsx +20 -55
  21. package/src/presentation/components/text-layer/ColorPicker.tsx +21 -55
  22. package/src/presentation/components/text-layer/FontSizeSelector.tsx +19 -50
  23. package/src/presentation/components/text-layer/OptionSelector.tsx +18 -55
  24. package/src/presentation/components/text-layer/TextAlignSelector.tsx +24 -51
  25. package/src/presentation/hooks/generic/useForm.ts +99 -0
  26. package/src/presentation/hooks/generic/useList.ts +117 -0
  27. package/src/presentation/hooks/useDraggableLayerGestures.ts +28 -25
  28. package/src/presentation/hooks/useEditorPlayback.ts +19 -2
  29. package/src/presentation/hooks/useMenuActions.tsx +19 -4
@@ -1,12 +1,11 @@
1
1
  /**
2
2
  * Layer Actions Menu Component
3
3
  * Single Responsibility: Display layer action menu
4
+ * REFACTORED: Now uses generic ActionMenu component (89 lines)
4
5
  */
5
6
 
6
- import React from "react";
7
- import { View, TouchableOpacity, StyleSheet } from "react-native";
8
- import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
9
- import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
7
+ import React, { useMemo, useCallback } from "react";
8
+ import { ActionMenu, type ActionMenuItem } from "./generic/ActionMenu";
10
9
  import { useLocalization } from "@umituz/react-native-settings";
11
10
  import type { Layer } from "../../domain/entities/video-project.types";
12
11
 
@@ -35,167 +34,106 @@ export const LayerActionsMenu: React.FC<LayerActionsMenuProps> = ({
35
34
  onMoveBack,
36
35
  onDelete,
37
36
  }) => {
38
- const tokens = useAppDesignTokens();
39
37
  const { t } = useLocalization();
40
38
 
41
- return (
42
- <View style={{ paddingVertical: 8 }}>
43
- {layer.type === "text" && (
44
- <TouchableOpacity style={styles.actionMenuItem} onPress={onEditText}>
45
- <AtomicIcon name="create-outline" size="md" color="primary" />
46
- <AtomicText
47
- type="bodyMedium"
48
- style={{
49
- color: tokens.colors.textPrimary,
50
- marginLeft: 12,
51
- }}
52
- >
53
- {t("editor.layers.actions.editText")}
54
- </AtomicText>
55
- </TouchableOpacity>
56
- )}
57
- {layer.type === "image" && (
58
- <TouchableOpacity style={styles.actionMenuItem} onPress={onEditImage}>
59
- <AtomicIcon name="create-outline" size="md" color="primary" />
60
- <AtomicText
61
- type="bodyMedium"
62
- style={{
63
- color: tokens.colors.textPrimary,
64
- marginLeft: 12,
65
- }}
66
- >
67
- {t("editor.layers.actions.editImage")}
68
- </AtomicText>
69
- </TouchableOpacity>
70
- )}
39
+ // Build menu items based on layer type
40
+ const menuItems = useMemo<ActionMenuItem[]>(() => {
41
+ const items: ActionMenuItem[] = [];
71
42
 
72
- <TouchableOpacity style={styles.actionMenuItem} onPress={onAnimate}>
73
- <AtomicIcon name="sparkles-outline" size="md" color="primary" />
74
- <AtomicText
75
- type="bodyMedium"
76
- style={{
77
- color: tokens.colors.textPrimary,
78
- marginLeft: 12,
79
- }}
80
- >
81
- {layer.animation
82
- ? t("editor.layers.actions.editAnimation")
83
- : t("editor.layers.actions.addAnimation")}
84
- </AtomicText>
85
- {layer.animation && (
86
- <View
87
- style={[
88
- styles.animationBadge,
89
- { backgroundColor: tokens.colors.success },
90
- ]}
91
- />
92
- )}
93
- </TouchableOpacity>
43
+ // Type-specific actions
44
+ if (layer.type === "text") {
45
+ items.push({
46
+ id: "edit-text",
47
+ label: t("editor.layers.actions.editText") || "Edit Text",
48
+ icon: "create-outline",
49
+ });
50
+ }
94
51
 
95
- <TouchableOpacity style={styles.actionMenuItem} onPress={onDuplicate}>
96
- <AtomicIcon name="copy" size="md" color="primary" />
97
- <AtomicText
98
- type="bodyMedium"
99
- style={{
100
- color: tokens.colors.textPrimary,
101
- marginLeft: 12,
102
- }}
103
- >
104
- {t("editor.layers.actions.duplicate")}
105
- </AtomicText>
106
- </TouchableOpacity>
52
+ if (layer.type === "image") {
53
+ items.push({
54
+ id: "edit-image",
55
+ label: t("editor.layers.actions.editImage") || "Edit Image",
56
+ icon: "create-outline",
57
+ });
58
+ }
107
59
 
108
- <View
109
- style={[styles.divider, { backgroundColor: tokens.colors.border }]}
110
- />
60
+ // Common actions
61
+ items.push(
62
+ {
63
+ id: "animate",
64
+ label: layer.animation
65
+ ? (t("editor.layers.actions.editAnimation") || "Edit Animation")
66
+ : (t("editor.layers.actions.addAnimation") || "Add Animation"),
67
+ icon: "sparkles-outline",
68
+ },
69
+ {
70
+ id: "duplicate",
71
+ label: t("editor.layers.actions.duplicate") || "Duplicate",
72
+ icon: "copy",
73
+ },
74
+ {
75
+ id: "move-front",
76
+ label: t("editor.layers.actions.moveFront") || "Bring to Front",
77
+ icon: "flip-to-front",
78
+ },
79
+ {
80
+ id: "move-up",
81
+ label: t("editor.layers.actions.moveUp") || "Move Forward",
82
+ icon: "arrow-upward",
83
+ },
84
+ {
85
+ id: "move-down",
86
+ label: t("editor.layers.actions.moveDown") || "Move Backward",
87
+ icon: "arrow-downward",
88
+ },
89
+ {
90
+ id: "move-back",
91
+ label: t("editor.layers.actions.moveBack") || "Send to Back",
92
+ icon: "flip-to-back",
93
+ },
94
+ {
95
+ id: "delete",
96
+ label: t("editor.layers.actions.delete") || "Delete",
97
+ icon: "delete",
98
+ destructive: true,
99
+ },
100
+ );
111
101
 
112
- <TouchableOpacity style={styles.actionMenuItem} onPress={onMoveFront}>
113
- <AtomicIcon name="chevron-up-outline" size="md" color="secondary" />
114
- <AtomicText
115
- type="bodyMedium"
116
- style={{
117
- color: tokens.colors.textSecondary,
118
- marginLeft: 12,
119
- }}
120
- >
121
- {t("editor.layers.actions.bringToFront")}
122
- </AtomicText>
123
- </TouchableOpacity>
102
+ return items;
103
+ }, [layer, t]);
124
104
 
125
- <TouchableOpacity style={styles.actionMenuItem} onPress={onMoveUp}>
126
- <AtomicIcon name="chevron-up-outline" size="md" color="secondary" />
127
- <AtomicText
128
- type="bodyMedium"
129
- style={{
130
- color: tokens.colors.textSecondary,
131
- marginLeft: 12,
132
- }}
133
- >
134
- {t("editor.layers.actions.moveUp")}
135
- </AtomicText>
136
- </TouchableOpacity>
105
+ // Handle action selection
106
+ const handleSelect = useCallback((actionId: string) => {
107
+ switch (actionId) {
108
+ case "edit-text":
109
+ onEditText();
110
+ break;
111
+ case "edit-image":
112
+ onEditImage();
113
+ break;
114
+ case "animate":
115
+ onAnimate();
116
+ break;
117
+ case "duplicate":
118
+ onDuplicate();
119
+ break;
120
+ case "move-front":
121
+ onMoveFront();
122
+ break;
123
+ case "move-up":
124
+ onMoveUp();
125
+ break;
126
+ case "move-down":
127
+ onMoveDown();
128
+ break;
129
+ case "move-back":
130
+ onMoveBack();
131
+ break;
132
+ case "delete":
133
+ onDelete();
134
+ break;
135
+ }
136
+ }, [onEditText, onEditImage, onAnimate, onDuplicate, onMoveFront, onMoveUp, onMoveDown, onMoveBack, onDelete]);
137
137
 
138
- <TouchableOpacity style={styles.actionMenuItem} onPress={onMoveDown}>
139
- <AtomicIcon name="chevron-down" size="md" color="secondary" />
140
- <AtomicText
141
- type="bodyMedium"
142
- style={{
143
- color: tokens.colors.textSecondary,
144
- marginLeft: 12,
145
- }}
146
- >
147
- {t("editor.layers.actions.moveDown")}
148
- </AtomicText>
149
- </TouchableOpacity>
150
-
151
- <TouchableOpacity style={styles.actionMenuItem} onPress={onMoveBack}>
152
- <AtomicIcon name="chevron-down-outline" size="md" color="secondary" />
153
- <AtomicText
154
- type="bodyMedium"
155
- style={{
156
- color: tokens.colors.textSecondary,
157
- marginLeft: 12,
158
- }}
159
- >
160
- {t("editor.layers.actions.sendToBack")}
161
- </AtomicText>
162
- </TouchableOpacity>
163
-
164
- <View
165
- style={[styles.divider, { backgroundColor: tokens.colors.border }]}
166
- />
167
-
168
- <TouchableOpacity style={styles.actionMenuItem} onPress={onDelete}>
169
- <AtomicIcon name="trash-outline" size="md" color="error" />
170
- <AtomicText
171
- type="bodyMedium"
172
- style={{
173
- color: tokens.colors.error,
174
- marginLeft: 12,
175
- }}
176
- >
177
- {t("editor.layers.actions.delete")}
178
- </AtomicText>
179
- </TouchableOpacity>
180
- </View>
181
- );
138
+ return <ActionMenu actions={menuItems} onSelect={handleSelect} testID="layer-actions-menu" />;
182
139
  };
183
-
184
- const styles = StyleSheet.create({
185
- actionMenuItem: {
186
- flexDirection: "row",
187
- alignItems: "center",
188
- paddingVertical: 16,
189
- paddingHorizontal: 16,
190
- },
191
- divider: {
192
- height: 1,
193
- marginVertical: 8,
194
- },
195
- animationBadge: {
196
- width: 8,
197
- height: 8,
198
- borderRadius: 4,
199
- marginLeft: 8,
200
- },
201
- });
@@ -1,12 +1,11 @@
1
1
  /**
2
2
  * Scene Actions Menu Component
3
3
  * Single Responsibility: Display scene action menu
4
+ * REFACTORED: Uses generic ActionMenu component (40 lines)
4
5
  */
5
6
 
6
- import React from "react";
7
- import { View, TouchableOpacity, StyleSheet } from "react-native";
8
- import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
9
- import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
7
+ import React, { useMemo, useCallback } from "react";
8
+ import { ActionMenu, type ActionMenuItem } from "./generic/ActionMenu";
10
9
 
11
10
  interface SceneActionsMenuProps {
12
11
  sceneIndex: number;
@@ -21,46 +20,37 @@ export const SceneActionsMenu: React.FC<SceneActionsMenuProps> = ({
21
20
  onDuplicate,
22
21
  onDelete,
23
22
  }) => {
24
- const tokens = useAppDesignTokens();
23
+ const menuItems = useMemo<ActionMenuItem[]>(() => {
24
+ const items: ActionMenuItem[] = [
25
+ {
26
+ id: "duplicate",
27
+ label: "Duplicate Scene",
28
+ icon: "copy",
29
+ },
30
+ ];
25
31
 
26
- return (
27
- <View style={{ paddingVertical: 8 }}>
28
- <TouchableOpacity style={styles.actionMenuItem} onPress={onDuplicate}>
29
- <AtomicIcon name="copy" size="md" color="primary" />
30
- <AtomicText
31
- type="bodyMedium"
32
- style={{
33
- color: tokens.colors.textPrimary,
34
- marginLeft: 12,
35
- }}
36
- >
37
- Duplicate Scene
38
- </AtomicText>
39
- </TouchableOpacity>
32
+ if (canDelete) {
33
+ items.push({
34
+ id: "delete",
35
+ label: "Delete Scene",
36
+ icon: "delete",
37
+ destructive: true,
38
+ });
39
+ }
40
40
 
41
- {canDelete && (
42
- <TouchableOpacity style={styles.actionMenuItem} onPress={onDelete}>
43
- <AtomicIcon name="trash-outline" size="md" color="error" />
44
- <AtomicText
45
- type="bodyMedium"
46
- style={{
47
- color: tokens.colors.error,
48
- marginLeft: 12,
49
- }}
50
- >
51
- Delete Scene
52
- </AtomicText>
53
- </TouchableOpacity>
54
- )}
55
- </View>
56
- );
57
- };
41
+ return items;
42
+ }, [canDelete]);
43
+
44
+ const handleSelect = useCallback((actionId: string) => {
45
+ switch (actionId) {
46
+ case "duplicate":
47
+ onDuplicate();
48
+ break;
49
+ case "delete":
50
+ onDelete();
51
+ break;
52
+ }
53
+ }, [onDuplicate, onDelete]);
58
54
 
59
- const styles = StyleSheet.create({
60
- actionMenuItem: {
61
- flexDirection: "row",
62
- alignItems: "center",
63
- paddingVertical: 16,
64
- paddingHorizontal: 16,
65
- },
66
- });
55
+ return <ActionMenu actions={menuItems} onSelect={handleSelect} testID="scene-actions-menu" />;
56
+ };
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * SubtitleListPanel Component
3
3
  * Full subtitle editor panel: list + add/edit modal
4
+ * PERFORMANCE: Uses FlatList for efficient rendering of large subtitle lists
4
5
  */
5
6
 
6
- import React, { useMemo } from "react";
7
- import { View, ScrollView } from "react-native";
7
+ import React, { useMemo, useCallback } from "react";
8
+ import { View, FlatList } 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 { SubtitleListHeader } from "./subtitle/SubtitleListHeader";
@@ -40,13 +41,41 @@ export const SubtitleListPanel: React.FC<SubtitleListPanelProps> = ({
40
41
  return subtitles.find((s) => currentTime >= s.startTime && currentTime <= s.endTime)?.id ?? null;
41
42
  }, [subtitles, currentTime]);
42
43
 
43
- const emptyStyles = {
44
+ // Memoize empty styles to prevent recreation
45
+ const emptyStyles = useMemo(() => ({
44
46
  emptyBox: {
45
47
  alignItems: "center" as const,
46
48
  paddingTop: tokens.spacing.xl,
47
49
  gap: tokens.spacing.sm,
48
50
  },
49
- };
51
+ }), [tokens.spacing.xl, tokens.spacing.sm]);
52
+
53
+ // Stable render item for FlatList - prevents unnecessary re-renders
54
+ const renderItem = useCallback(({ item }: { item: Subtitle }) => (
55
+ <SubtitleListItem
56
+ key={item.id}
57
+ subtitle={item}
58
+ isActive={item.id === activeId}
59
+ onEdit={form.openEdit}
60
+ onSeek={onSeek}
61
+ />
62
+ ), [activeId, form.openEdit, onSeek]);
63
+
64
+ // Stable key extractor
65
+ const keyExtractor = useCallback((item: Subtitle) => item.id, []);
66
+
67
+ // List empty component - memoized
68
+ const ListEmptyComponent = useMemo(() => (
69
+ <View style={emptyStyles.emptyBox}>
70
+ <AtomicIcon name="video" size="lg" color="textSecondary" />
71
+ <AtomicText color="textSecondary">
72
+ {t("subtitle.panel.empty") || "No subtitles yet"}
73
+ </AtomicText>
74
+ <AtomicText type="labelSmall" color="textTertiary">
75
+ {t("subtitle.panel.emptyHint") || "Tap + to add a subtitle"}
76
+ </AtomicText>
77
+ </View>
78
+ ), [emptyStyles.emptyBox, t]);
50
79
 
51
80
  return (
52
81
  <View style={{ flex: 1 }}>
@@ -56,29 +85,27 @@ export const SubtitleListPanel: React.FC<SubtitleListPanelProps> = ({
56
85
  title={t("subtitle.panel.title") || "Subtitles"}
57
86
  />
58
87
 
59
- <ScrollView contentContainerStyle={{ paddingVertical: tokens.spacing.sm }}>
60
- {subtitles.length === 0 ? (
61
- <View style={emptyStyles.emptyBox}>
62
- <AtomicIcon name="video" size="lg" color="textSecondary" />
63
- <AtomicText color="textSecondary">
64
- {t("subtitle.panel.empty") || "No subtitles yet"}
65
- </AtomicText>
66
- <AtomicText type="labelSmall" color="textTertiary">
67
- {t("subtitle.panel.emptyHint") || "Tap + to add a subtitle"}
68
- </AtomicText>
69
- </View>
70
- ) : (
71
- subtitles.map((subtitle) => (
72
- <SubtitleListItem
73
- key={subtitle.id}
74
- subtitle={subtitle}
75
- isActive={subtitle.id === activeId}
76
- onEdit={form.openEdit}
77
- onSeek={onSeek}
78
- />
79
- ))
80
- )}
81
- </ScrollView>
88
+ <FlatList
89
+ data={subtitles}
90
+ renderItem={renderItem}
91
+ keyExtractor={keyExtractor}
92
+ ListEmptyComponent={ListEmptyComponent}
93
+ contentContainerStyle={{
94
+ paddingVertical: tokens.spacing.sm,
95
+ flexGrow: 1,
96
+ }}
97
+ removeClippedSubviews={true}
98
+ maxToRenderPerBatch={10}
99
+ updateCellsBatchingPeriod={50}
100
+ initialNumToRender={10}
101
+ windowSize={5}
102
+ // Performance: Prevents layout jumps on Android
103
+ getItemLayout={(data, index) => ({
104
+ length: 80, // Approximate height of each item
105
+ offset: 80 * index,
106
+ index,
107
+ })}
108
+ />
82
109
 
83
110
  <SubtitleModal
84
111
  visible={form.showModal}