@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.
- package/package.json +1 -1
- package/src/application/services/EditorService.ts +151 -0
- package/src/application/usecases/LayerUseCases.ts +192 -0
- package/src/infrastructure/repositories/LayerRepositoryImpl.ts +158 -0
- package/src/infrastructure/services/image-layer-operations.service.ts +1 -1
- package/src/infrastructure/services/shape-layer-operations.service.ts +1 -1
- package/src/infrastructure/services/text-layer-operations.service.ts +1 -1
- package/src/infrastructure/utils/debounce.utils.ts +69 -0
- package/src/infrastructure/utils/image-processing.utils.ts +73 -0
- package/src/infrastructure/utils/position-calculations.utils.ts +45 -161
- package/src/presentation/components/EditorTimeline.tsx +29 -11
- package/src/presentation/components/EditorToolPanel.tsx +71 -172
- package/src/presentation/components/LayerActionsMenu.tsx +97 -159
- package/src/presentation/components/SceneActionsMenu.tsx +34 -44
- package/src/presentation/components/SubtitleListPanel.tsx +55 -28
- package/src/presentation/components/SubtitleStylePicker.tsx +36 -156
- package/src/presentation/components/collage/CollageCanvas.tsx +2 -2
- package/src/presentation/components/collage/CollageLayoutSelector.tsx +0 -4
- package/src/presentation/components/draggable-layer/LayerContent.tsx +7 -2
- package/src/presentation/components/generic/ActionMenu.tsx +110 -0
- package/src/presentation/components/generic/Editor.tsx +65 -0
- package/src/presentation/components/generic/Selector.tsx +96 -0
- package/src/presentation/components/generic/Toolbar.tsx +77 -0
- package/src/presentation/components/image-layer/ImagePreview.tsx +7 -1
- package/src/presentation/components/shape-layer/ColorPickerHorizontal.tsx +20 -55
- package/src/presentation/components/subtitle/SubtitleListItem.tsx +4 -5
- package/src/presentation/components/subtitle/SubtitleModal.tsx +2 -2
- package/src/presentation/components/subtitle/useSubtitleForm.ts +1 -1
- package/src/presentation/components/text-layer/ColorPicker.tsx +21 -55
- package/src/presentation/components/text-layer/FontSizeSelector.tsx +19 -50
- package/src/presentation/components/text-layer/OptionSelector.tsx +18 -55
- package/src/presentation/components/text-layer/TextAlignSelector.tsx +24 -51
- package/src/presentation/hooks/generic/use-layer-form.hook.ts +19 -16
- package/src/presentation/hooks/generic/useForm.ts +99 -0
- package/src/presentation/hooks/generic/useList.ts +117 -0
- package/src/presentation/hooks/useDraggableLayerGestures.ts +28 -25
- package/src/presentation/hooks/useEditorPlayback.ts +19 -2
- package/src/presentation/hooks/useImageLayerForm.ts +9 -6
- package/src/presentation/hooks/useMenuActions.tsx +19 -4
- package/src/presentation/hooks/useTextLayerForm.ts +9 -6
|
@@ -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 {
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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,
|
|
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";
|
|
@@ -28,7 +29,7 @@ export const SubtitleListPanel: React.FC<SubtitleListPanelProps> = ({
|
|
|
28
29
|
currentTime,
|
|
29
30
|
onAdd,
|
|
30
31
|
onUpdate,
|
|
31
|
-
onDelete,
|
|
32
|
+
onDelete: _onDelete,
|
|
32
33
|
onSeek,
|
|
33
34
|
t,
|
|
34
35
|
}) => {
|
|
@@ -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
|
-
|
|
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
|
-
<
|
|
60
|
-
{subtitles
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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}
|