@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.
- 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/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 +54 -27
- package/src/presentation/components/SubtitleStylePicker.tsx +36 -156
- 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/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/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/useMenuActions.tsx +19 -4
|
@@ -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
|
-
|
|
8
|
-
y: number;
|
|
9
|
-
}
|
|
5
|
+
export interface Position { x: number; y: number; }
|
|
6
|
+
export interface Size { width: number; height: number; }
|
|
10
7
|
|
|
11
|
-
export
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
<
|
|
135
|
+
<FlatList
|
|
135
136
|
horizontal
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
7
|
-
import {
|
|
8
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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:
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
});
|