@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.0",
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
+ }