@umituz/react-native-video-editor 1.2.0 → 1.2.1
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-video-editor",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Professional video editor with layer-based timeline, text/image/shape/audio/animation layers, and export functionality",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AnimationEditor Component
|
|
3
|
+
* Main component for editing layer animations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useCallback } from "react";
|
|
7
|
+
import { View, ScrollView, StyleSheet } from "react-native";
|
|
8
|
+
import { useLocalization } from "@umituz/react-native-settings";
|
|
9
|
+
import { AtomicText } from "@umituz/react-native-design-system/atoms";
|
|
10
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
11
|
+
import type { Animation } from "../../domain/entities/video-project.types";
|
|
12
|
+
import { useAnimationForm } from "../hooks/useAnimationForm";
|
|
13
|
+
import { EditorActions } from "./text-layer/EditorActions";
|
|
14
|
+
|
|
15
|
+
interface AnimationEditorProps {
|
|
16
|
+
readonly animation?: Animation;
|
|
17
|
+
readonly onSave: (animation: Animation) => void;
|
|
18
|
+
readonly onRemove?: () => void;
|
|
19
|
+
readonly onCancel: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const ANIMATION_TYPES = [
|
|
23
|
+
{ label: "None", value: "none" as const },
|
|
24
|
+
{ label: "Fade", value: "fade" as const },
|
|
25
|
+
{ label: "Slide", value: "slide" as const },
|
|
26
|
+
{ label: "Bounce", value: "bounce" as const },
|
|
27
|
+
{ label: "Zoom", value: "zoom" as const },
|
|
28
|
+
{ label: "Rotate", value: "rotate" as const },
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
const EASING_OPTIONS = [
|
|
32
|
+
{ label: "Linear", value: "linear" as const },
|
|
33
|
+
{ label: "Ease In", value: "ease-in" as const },
|
|
34
|
+
{ label: "Ease Out", value: "ease-out" as const },
|
|
35
|
+
{ label: "Ease In Out", value: "ease-in-out" as const },
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
export const AnimationEditor: React.FC<AnimationEditorProps> = ({
|
|
39
|
+
animation,
|
|
40
|
+
onSave,
|
|
41
|
+
onRemove,
|
|
42
|
+
onCancel,
|
|
43
|
+
}) => {
|
|
44
|
+
const { t } = useLocalization();
|
|
45
|
+
const tokens = useAppDesignTokens();
|
|
46
|
+
const {
|
|
47
|
+
formState,
|
|
48
|
+
setAnimationType,
|
|
49
|
+
setDuration,
|
|
50
|
+
setDelay,
|
|
51
|
+
setEasing,
|
|
52
|
+
buildAnimationData,
|
|
53
|
+
isValid,
|
|
54
|
+
} = useAnimationForm(animation);
|
|
55
|
+
|
|
56
|
+
const handleSave = useCallback(() => {
|
|
57
|
+
if (!isValid) return;
|
|
58
|
+
onSave(buildAnimationData());
|
|
59
|
+
}, [isValid, buildAnimationData, onSave]);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<View style={styles.container}>
|
|
63
|
+
<ScrollView showsVerticalScrollIndicator={false}>
|
|
64
|
+
{/* Animation Type Selector */}
|
|
65
|
+
<View style={styles.section}>
|
|
66
|
+
<AtomicText
|
|
67
|
+
type="labelMedium"
|
|
68
|
+
color="textSecondary"
|
|
69
|
+
style={{ marginBottom: tokens.spacing.sm }}
|
|
70
|
+
>
|
|
71
|
+
{t("editor.properties.animation_type") || "Animation Type"}
|
|
72
|
+
</AtomicText>
|
|
73
|
+
<View style={styles.optionsContainer}>
|
|
74
|
+
{ANIMATION_TYPES.map((option) => (
|
|
75
|
+
<View
|
|
76
|
+
key={option.value}
|
|
77
|
+
style={[
|
|
78
|
+
styles.optionButton,
|
|
79
|
+
formState.type === option.value && {
|
|
80
|
+
backgroundColor: tokens.colors.primary,
|
|
81
|
+
},
|
|
82
|
+
]}
|
|
83
|
+
>
|
|
84
|
+
<AtomicText
|
|
85
|
+
fontWeight="medium"
|
|
86
|
+
color={
|
|
87
|
+
formState.type === option.value
|
|
88
|
+
? "onPrimary"
|
|
89
|
+
: "textPrimary"
|
|
90
|
+
}
|
|
91
|
+
onPress={() => setAnimationType(option.value)}
|
|
92
|
+
>
|
|
93
|
+
{option.label}
|
|
94
|
+
</AtomicText>
|
|
95
|
+
</View>
|
|
96
|
+
))}
|
|
97
|
+
</View>
|
|
98
|
+
</View>
|
|
99
|
+
|
|
100
|
+
{/* Duration Selector */}
|
|
101
|
+
{formState.type !== "none" && (
|
|
102
|
+
<>
|
|
103
|
+
<View style={styles.section}>
|
|
104
|
+
<AtomicText
|
|
105
|
+
type="labelMedium"
|
|
106
|
+
color="textSecondary"
|
|
107
|
+
style={{ marginBottom: tokens.spacing.sm }}
|
|
108
|
+
>
|
|
109
|
+
{t("editor.properties.duration") || "Duration"}
|
|
110
|
+
</AtomicText>
|
|
111
|
+
<View style={styles.sliderContainer}>
|
|
112
|
+
<AtomicText type="bodySmall" color="textSecondary">
|
|
113
|
+
{formState.duration}ms
|
|
114
|
+
</AtomicText>
|
|
115
|
+
</View>
|
|
116
|
+
<View style={styles.valueButtons}>
|
|
117
|
+
{[300, 500, 1000, 1500, 2000].map((value) => (
|
|
118
|
+
<View
|
|
119
|
+
key={value}
|
|
120
|
+
style={[
|
|
121
|
+
styles.valueButton,
|
|
122
|
+
formState.duration === value && {
|
|
123
|
+
backgroundColor: tokens.colors.primary,
|
|
124
|
+
},
|
|
125
|
+
]}
|
|
126
|
+
>
|
|
127
|
+
<AtomicText
|
|
128
|
+
type="labelSmall"
|
|
129
|
+
color={
|
|
130
|
+
formState.duration === value
|
|
131
|
+
? "onPrimary"
|
|
132
|
+
: "textPrimary"
|
|
133
|
+
}
|
|
134
|
+
onPress={() => setDuration(value)}
|
|
135
|
+
>
|
|
136
|
+
{value}ms
|
|
137
|
+
</AtomicText>
|
|
138
|
+
</View>
|
|
139
|
+
))}
|
|
140
|
+
</View>
|
|
141
|
+
</View>
|
|
142
|
+
|
|
143
|
+
{/* Delay Selector */}
|
|
144
|
+
<View style={styles.section}>
|
|
145
|
+
<AtomicText
|
|
146
|
+
type="labelMedium"
|
|
147
|
+
color="textSecondary"
|
|
148
|
+
style={{ marginBottom: tokens.spacing.sm }}
|
|
149
|
+
>
|
|
150
|
+
{t("editor.properties.delay") || "Delay"}
|
|
151
|
+
</AtomicText>
|
|
152
|
+
<View style={styles.sliderContainer}>
|
|
153
|
+
<AtomicText type="bodySmall" color="textSecondary">
|
|
154
|
+
{formState.delay || 0}ms
|
|
155
|
+
</AtomicText>
|
|
156
|
+
</View>
|
|
157
|
+
<View style={styles.valueButtons}>
|
|
158
|
+
{[0, 100, 200, 500, 1000].map((value) => (
|
|
159
|
+
<View
|
|
160
|
+
key={value}
|
|
161
|
+
style={[
|
|
162
|
+
styles.valueButton,
|
|
163
|
+
(formState.delay || 0) === value && {
|
|
164
|
+
backgroundColor: tokens.colors.primary,
|
|
165
|
+
},
|
|
166
|
+
]}
|
|
167
|
+
>
|
|
168
|
+
<AtomicText
|
|
169
|
+
type="labelSmall"
|
|
170
|
+
color={
|
|
171
|
+
(formState.delay || 0) === value
|
|
172
|
+
? "onPrimary"
|
|
173
|
+
: "textPrimary"
|
|
174
|
+
}
|
|
175
|
+
onPress={() => setDelay(value)}
|
|
176
|
+
>
|
|
177
|
+
{value}ms
|
|
178
|
+
</AtomicText>
|
|
179
|
+
</View>
|
|
180
|
+
))}
|
|
181
|
+
</View>
|
|
182
|
+
</View>
|
|
183
|
+
|
|
184
|
+
{/* Easing Selector */}
|
|
185
|
+
<View style={styles.section}>
|
|
186
|
+
<AtomicText
|
|
187
|
+
type="labelMedium"
|
|
188
|
+
color="textSecondary"
|
|
189
|
+
style={{ marginBottom: tokens.spacing.sm }}
|
|
190
|
+
>
|
|
191
|
+
{t("editor.properties.easing") || "Easing"}
|
|
192
|
+
</AtomicText>
|
|
193
|
+
<View style={styles.optionsContainer}>
|
|
194
|
+
{EASING_OPTIONS.map((option) => (
|
|
195
|
+
<View
|
|
196
|
+
key={option.value}
|
|
197
|
+
style={[
|
|
198
|
+
styles.optionButton,
|
|
199
|
+
formState.easing === option.value && {
|
|
200
|
+
backgroundColor: tokens.colors.primary,
|
|
201
|
+
},
|
|
202
|
+
]}
|
|
203
|
+
>
|
|
204
|
+
<AtomicText
|
|
205
|
+
fontWeight="medium"
|
|
206
|
+
color={
|
|
207
|
+
formState.easing === option.value
|
|
208
|
+
? "onPrimary"
|
|
209
|
+
: "textPrimary"
|
|
210
|
+
}
|
|
211
|
+
onPress={() => setEasing(option.value)}
|
|
212
|
+
>
|
|
213
|
+
{option.label}
|
|
214
|
+
</AtomicText>
|
|
215
|
+
</View>
|
|
216
|
+
))}
|
|
217
|
+
</View>
|
|
218
|
+
</View>
|
|
219
|
+
</>
|
|
220
|
+
)}
|
|
221
|
+
</ScrollView>
|
|
222
|
+
|
|
223
|
+
<EditorActions
|
|
224
|
+
onCancel={onCancel}
|
|
225
|
+
onSave={handleSave}
|
|
226
|
+
saveLabel={animation ? "Update Animation" : "Add Animation"}
|
|
227
|
+
isValid={isValid}
|
|
228
|
+
onRemove={onRemove}
|
|
229
|
+
removeLabel="Remove Animation"
|
|
230
|
+
/>
|
|
231
|
+
</View>
|
|
232
|
+
);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const styles = StyleSheet.create({
|
|
236
|
+
container: {
|
|
237
|
+
maxHeight: "80%",
|
|
238
|
+
},
|
|
239
|
+
section: {
|
|
240
|
+
marginBottom: 20,
|
|
241
|
+
},
|
|
242
|
+
optionsContainer: {
|
|
243
|
+
flexDirection: "row",
|
|
244
|
+
flexWrap: "wrap",
|
|
245
|
+
gap: 8,
|
|
246
|
+
},
|
|
247
|
+
optionButton: {
|
|
248
|
+
paddingHorizontal: 12,
|
|
249
|
+
paddingVertical: 8,
|
|
250
|
+
borderRadius: 8,
|
|
251
|
+
borderWidth: 1,
|
|
252
|
+
borderColor: "#ccc",
|
|
253
|
+
minWidth: 80,
|
|
254
|
+
alignItems: "center",
|
|
255
|
+
},
|
|
256
|
+
sliderContainer: {
|
|
257
|
+
marginBottom: 12,
|
|
258
|
+
},
|
|
259
|
+
valueButtons: {
|
|
260
|
+
flexDirection: "row",
|
|
261
|
+
flexWrap: "wrap",
|
|
262
|
+
gap: 8,
|
|
263
|
+
},
|
|
264
|
+
valueButton: {
|
|
265
|
+
paddingHorizontal: 12,
|
|
266
|
+
paddingVertical: 8,
|
|
267
|
+
borderRadius: 8,
|
|
268
|
+
borderWidth: 1,
|
|
269
|
+
borderColor: "#ccc",
|
|
270
|
+
minWidth: 60,
|
|
271
|
+
alignItems: "center",
|
|
272
|
+
},
|
|
273
|
+
});
|
|
@@ -14,6 +14,8 @@ interface EditorActionsProps {
|
|
|
14
14
|
onSave: () => void;
|
|
15
15
|
saveLabel: string;
|
|
16
16
|
isValid: boolean;
|
|
17
|
+
onRemove?: () => void;
|
|
18
|
+
removeLabel?: string;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export const EditorActions: React.FC<EditorActionsProps> = ({
|
|
@@ -21,6 +23,8 @@ export const EditorActions: React.FC<EditorActionsProps> = ({
|
|
|
21
23
|
onSave,
|
|
22
24
|
saveLabel,
|
|
23
25
|
isValid,
|
|
26
|
+
onRemove,
|
|
27
|
+
removeLabel,
|
|
24
28
|
}) => {
|
|
25
29
|
const tokens = useAppDesignTokens();
|
|
26
30
|
const { t } = useLocalization();
|
|
@@ -29,6 +33,24 @@ export const EditorActions: React.FC<EditorActionsProps> = ({
|
|
|
29
33
|
<View
|
|
30
34
|
style={[styles.actions, { borderTopColor: tokens.colors.borderLight }]}
|
|
31
35
|
>
|
|
36
|
+
{onRemove && (
|
|
37
|
+
<TouchableOpacity
|
|
38
|
+
style={[
|
|
39
|
+
styles.actionButton,
|
|
40
|
+
styles.removeButton,
|
|
41
|
+
{ borderColor: tokens.colors.error },
|
|
42
|
+
]}
|
|
43
|
+
onPress={onRemove}
|
|
44
|
+
>
|
|
45
|
+
<AtomicText
|
|
46
|
+
type="bodyMedium"
|
|
47
|
+
style={{ color: tokens.colors.error }}
|
|
48
|
+
>
|
|
49
|
+
{removeLabel || t("common.buttons.remove")}
|
|
50
|
+
</AtomicText>
|
|
51
|
+
</TouchableOpacity>
|
|
52
|
+
)}
|
|
53
|
+
|
|
32
54
|
<TouchableOpacity
|
|
33
55
|
style={[
|
|
34
56
|
styles.actionButton,
|
|
@@ -83,6 +105,9 @@ const styles = StyleSheet.create({
|
|
|
83
105
|
alignItems: "center",
|
|
84
106
|
justifyContent: "center",
|
|
85
107
|
},
|
|
108
|
+
removeButton: {
|
|
109
|
+
borderWidth: 1,
|
|
110
|
+
},
|
|
86
111
|
cancelButton: {
|
|
87
112
|
borderWidth: 1,
|
|
88
113
|
},
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentation Hooks
|
|
3
|
+
* All hooks for the video editor presentation layer
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { useAnimationForm } from "./useAnimationForm";
|
|
7
|
+
export { useAudioLayerForm } from "./useAudioLayerForm";
|
|
8
|
+
export { useCollageEditor } from "./useCollageEditor";
|
|
9
|
+
export { useDraggableLayerGestures } from "./useDraggableLayerGestures";
|
|
10
|
+
export { useEditorActions } from "./useEditorActions";
|
|
11
|
+
export { useEditorBottomSheet } from "./useEditorBottomSheet";
|
|
12
|
+
export { useEditorHistory } from "./useEditorHistory";
|
|
13
|
+
export { useEditorLayers } from "./useEditorLayers";
|
|
14
|
+
export { useEditorPlayback } from "./useEditorPlayback";
|
|
15
|
+
export { useEditorScenes } from "./useEditorScenes";
|
|
16
|
+
export { useExport } from "./useExport";
|
|
17
|
+
export { useExportActions } from "./useExportActions";
|
|
18
|
+
export { useExportForm } from "./useExportForm";
|
|
19
|
+
export { useImageLayerForm } from "./useImageLayerForm";
|
|
20
|
+
export { useImageLayerOperations } from "./useImageLayerOperations";
|
|
21
|
+
export { useLayerActions } from "./useLayerActions";
|
|
22
|
+
export { useLayerManipulation } from "./useLayerManipulation";
|
|
23
|
+
export { useMenuActions } from "./useMenuActions";
|
|
24
|
+
export { useSceneActions } from "./useSceneActions";
|
|
25
|
+
export { useShapeLayerForm } from "./useShapeLayerForm";
|
|
26
|
+
export { useShapeLayerOperations } from "./useShapeLayerOperations";
|
|
27
|
+
export { useSubtitleEditor } from "./useSubtitleEditor";
|
|
28
|
+
export { useTextLayerForm } from "./useTextLayerForm";
|
|
29
|
+
export { useTextLayerOperations } from "./useTextLayerOperations";
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAnimationForm Hook
|
|
3
|
+
* Manages form state for animation editing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback, useMemo } from "react";
|
|
7
|
+
import type { Animation, AnimationType } from "../../domain/entities/video-project.types";
|
|
8
|
+
|
|
9
|
+
interface AnimationFormState {
|
|
10
|
+
type: AnimationType;
|
|
11
|
+
duration: number;
|
|
12
|
+
delay?: number;
|
|
13
|
+
easing: "linear" | "ease-in" | "ease-out" | "ease-in-out";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface UseAnimationFormParams {
|
|
17
|
+
animation?: Animation;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface UseAnimationFormReturn {
|
|
21
|
+
formState: AnimationFormState;
|
|
22
|
+
setAnimationType: (type: AnimationType) => void;
|
|
23
|
+
setDuration: (duration: number) => void;
|
|
24
|
+
setDelay: (delay: number) => void;
|
|
25
|
+
setEasing: (easing: "linear" | "ease-in" | "ease-out" | "ease-in-out") => void;
|
|
26
|
+
buildAnimationData: () => Animation;
|
|
27
|
+
isValid: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DEFAULT_ANIMATION: AnimationFormState = {
|
|
31
|
+
type: "none",
|
|
32
|
+
duration: 500,
|
|
33
|
+
delay: 0,
|
|
34
|
+
easing: "ease-in-out",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function useAnimationForm({
|
|
38
|
+
animation,
|
|
39
|
+
}: UseAnimationFormParams = {}): UseAnimationFormReturn {
|
|
40
|
+
const formState = useMemo<AnimationFormState>(() => {
|
|
41
|
+
if (!animation) {
|
|
42
|
+
return DEFAULT_ANIMATION;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
type: animation.type,
|
|
47
|
+
duration: animation.duration,
|
|
48
|
+
delay: animation.delay,
|
|
49
|
+
easing: animation.easing || "ease-in-out",
|
|
50
|
+
};
|
|
51
|
+
}, [animation]);
|
|
52
|
+
|
|
53
|
+
const setAnimationType = useCallback((type: AnimationType) => {
|
|
54
|
+
(formState as any).type = type;
|
|
55
|
+
}, [formState]);
|
|
56
|
+
|
|
57
|
+
const setDuration = useCallback((duration: number) => {
|
|
58
|
+
(formState as any).duration = duration;
|
|
59
|
+
}, [formState]);
|
|
60
|
+
|
|
61
|
+
const setDelay = useCallback((delay: number) => {
|
|
62
|
+
(formState as any).delay = delay;
|
|
63
|
+
}, [formState]);
|
|
64
|
+
|
|
65
|
+
const setEasing = useCallback((
|
|
66
|
+
easing: "linear" | "ease-in" | "ease-out" | "ease-in-out"
|
|
67
|
+
) => {
|
|
68
|
+
(formState as any).easing = easing;
|
|
69
|
+
}, [formState]);
|
|
70
|
+
|
|
71
|
+
const buildAnimationData = useCallback((): Animation => {
|
|
72
|
+
const data: Animation = {
|
|
73
|
+
type: formState.type,
|
|
74
|
+
duration: formState.duration,
|
|
75
|
+
easing: formState.easing,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (formState.delay && formState.delay > 0) {
|
|
79
|
+
data.delay = formState.delay;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return data;
|
|
83
|
+
}, [formState]);
|
|
84
|
+
|
|
85
|
+
const isValid = useMemo(() => {
|
|
86
|
+
return formState.type !== "none" && formState.duration > 0;
|
|
87
|
+
}, [formState]);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
formState,
|
|
91
|
+
setAnimationType,
|
|
92
|
+
setDuration,
|
|
93
|
+
setDelay,
|
|
94
|
+
setEasing,
|
|
95
|
+
buildAnimationData,
|
|
96
|
+
isValid,
|
|
97
|
+
};
|
|
98
|
+
}
|