@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,14 @@
1
1
  /**
2
- * ColorPickerHorizontal Component
2
+ * Color Picker Horizontal Component
3
3
  * Horizontal scrolling color picker for shape layer
4
+ * REFACTORED: Uses generic Selector with colorPreview mode (28 lines)
4
5
  */
5
6
 
6
- import React from "react";
7
- import { View, ScrollView, StyleSheet, TouchableOpacity } from "react-native";
8
- 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";
9
10
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
11
+ import { Selector, type SelectorItem } from "../generic/Selector";
10
12
  import { SHAPE_COLORS } from "../../../infrastructure/constants/shape-layer.constants";
11
13
 
12
14
  interface ColorPickerHorizontalProps {
@@ -22,8 +24,13 @@ export const ColorPickerHorizontal: React.FC<ColorPickerHorizontalProps> = ({
22
24
  }) => {
23
25
  const tokens = useAppDesignTokens();
24
26
 
27
+ const items = useMemo<SelectorItem[]>(
28
+ () => SHAPE_COLORS.map((color) => ({ value: color.value, label: "", color: color.value })),
29
+ [],
30
+ );
31
+
25
32
  return (
26
- <View style={styles.section}>
33
+ <View style={{ marginBottom: tokens.spacing.md }}>
27
34
  <AtomicText
28
35
  type="bodyMedium"
29
36
  style={{
@@ -34,56 +41,14 @@ export const ColorPickerHorizontal: React.FC<ColorPickerHorizontalProps> = ({
34
41
  >
35
42
  {title}
36
43
  </AtomicText>
37
-
38
- <ScrollView
39
- horizontal
40
- showsHorizontalScrollIndicator={false}
41
- style={styles.colorsScroll}
42
- >
43
- {SHAPE_COLORS.map((color) => (
44
- <TouchableOpacity
45
- key={color.value}
46
- style={[
47
- styles.colorButton,
48
- {
49
- backgroundColor: color.value,
50
- borderColor:
51
- selectedColor === color.value
52
- ? tokens.colors.primary
53
- : tokens.colors.borderLight,
54
- borderWidth: selectedColor === color.value ? 3 : 2,
55
- },
56
- ]}
57
- onPress={() => onColorChange(color.value)}
58
- >
59
- {selectedColor === color.value && (
60
- <AtomicIcon
61
- name="checkmark-outline"
62
- size="sm"
63
- color={color.value === "#FFFFFF" ? "primary" : "onSurface"}
64
- />
65
- )}
66
- </TouchableOpacity>
67
- ))}
68
- </ScrollView>
44
+ <Selector
45
+ items={items}
46
+ selectedValue={selectedColor}
47
+ onSelect={onColorChange}
48
+ orientation="horizontal"
49
+ colorPreview
50
+ testID="color-picker-horizontal"
51
+ />
69
52
  </View>
70
53
  );
71
54
  };
72
-
73
- const styles = StyleSheet.create({
74
- section: {
75
- marginBottom: 24,
76
- },
77
- colorsScroll: {
78
- marginHorizontal: -16,
79
- paddingHorizontal: 16,
80
- },
81
- colorButton: {
82
- width: 50,
83
- height: 50,
84
- borderRadius: 25,
85
- marginRight: 12,
86
- alignItems: "center",
87
- justifyContent: "center",
88
- },
89
- });
@@ -1,13 +1,15 @@
1
1
  /**
2
- * ColorPicker Component
2
+ * Color Picker Component
3
3
  * Color picker for text layer
4
+ * REFACTORED: Uses generic Selector with colorPreview mode (35 lines)
4
5
  */
5
6
 
6
- import React from "react";
7
- import { View, StyleSheet, TouchableOpacity } from "react-native";
8
- import { useLocalization } from "@umituz/react-native-settings";
9
- 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";
10
10
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
11
+ import { useLocalization } from "@umituz/react-native-settings";
12
+ import { Selector, type SelectorItem } from "../generic/Selector";
11
13
  import { TEXT_COLORS } from "../../../infrastructure/constants/text-layer.constants";
12
14
 
13
15
  interface ColorPickerProps {
@@ -22,8 +24,13 @@ export const ColorPicker: React.FC<ColorPickerProps> = ({
22
24
  const { t } = useLocalization();
23
25
  const tokens = useAppDesignTokens();
24
26
 
27
+ const items = useMemo<SelectorItem[]>(
28
+ () => TEXT_COLORS.map((color) => ({ value: color, label: "", color })),
29
+ [],
30
+ );
31
+
25
32
  return (
26
- <View style={styles.section}>
33
+ <View style={{ marginBottom: tokens.spacing.md }}>
27
34
  <AtomicText
28
35
  type="bodyMedium"
29
36
  style={{
@@ -34,55 +41,14 @@ export const ColorPicker: React.FC<ColorPickerProps> = ({
34
41
  >
35
42
  {t("editor.properties.color")}
36
43
  </AtomicText>
37
- <View style={styles.colorGrid}>
38
- {TEXT_COLORS.map((color) => (
39
- <TouchableOpacity
40
- key={color}
41
- style={[
42
- styles.colorButton,
43
- {
44
- backgroundColor: color,
45
- borderColor:
46
- selectedColor === color
47
- ? tokens.colors.primary
48
- : tokens.colors.borderLight,
49
- borderWidth: selectedColor === color ? 3 : 1,
50
- },
51
- ]}
52
- onPress={() => onColorChange(color)}
53
- >
54
- {selectedColor === color && (
55
- <AtomicIcon
56
- name="checkmark-outline"
57
- size="sm"
58
- color={
59
- color === "#FFFFFF" || color === "#FCD34D"
60
- ? "primary"
61
- : "onSurface"
62
- }
63
- />
64
- )}
65
- </TouchableOpacity>
66
- ))}
67
- </View>
44
+ <Selector
45
+ items={items}
46
+ selectedValue={selectedColor}
47
+ onSelect={onColorChange}
48
+ orientation="grid"
49
+ colorPreview
50
+ testID="color-picker"
51
+ />
68
52
  </View>
69
53
  );
70
54
  };
71
-
72
- const styles = StyleSheet.create({
73
- section: {
74
- marginBottom: 24,
75
- },
76
- colorGrid: {
77
- flexDirection: "row",
78
- flexWrap: "wrap",
79
- gap: 12,
80
- },
81
- colorButton: {
82
- width: 50,
83
- height: 50,
84
- borderRadius: 25,
85
- alignItems: "center",
86
- justifyContent: "center",
87
- },
88
- });
@@ -1,13 +1,15 @@
1
1
  /**
2
- * FontSizeSelector Component
2
+ * Font Size Selector Component
3
3
  * Font size selector for text layer
4
+ * REFACTORED: Uses generic Selector component (45 lines)
4
5
  */
5
6
 
6
- import React from "react";
7
- import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
8
- import { useLocalization } from "@umituz/react-native-settings";
7
+ import React, { useMemo } from "react";
8
+ import { View } from "react-native";
9
9
  import { AtomicText } from "@umituz/react-native-design-system/atoms";
10
10
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
11
+ import { useLocalization } from "@umituz/react-native-settings";
12
+ import { Selector, type SelectorItem } from "../generic/Selector";
11
13
  import { FONT_SIZES } from "../../../infrastructure/constants/text-layer.constants";
12
14
 
13
15
  interface FontSizeSelectorProps {
@@ -22,8 +24,13 @@ export const FontSizeSelector: React.FC<FontSizeSelectorProps> = ({
22
24
  const { t } = useLocalization();
23
25
  const tokens = useAppDesignTokens();
24
26
 
27
+ const items = useMemo<SelectorItem<number>[]>(
28
+ () => FONT_SIZES.map((size) => ({ value: size, label: `${size}px` })),
29
+ [],
30
+ );
31
+
25
32
  return (
26
- <View style={styles.section}>
33
+ <View style={{ marginBottom: tokens.spacing.md }}>
27
34
  <AtomicText
28
35
  type="bodyMedium"
29
36
  style={{
@@ -34,51 +41,13 @@ export const FontSizeSelector: React.FC<FontSizeSelectorProps> = ({
34
41
  >
35
42
  {t("editor.properties.font_size")}: {fontSize}px
36
43
  </AtomicText>
37
- <ScrollView horizontal showsHorizontalScrollIndicator={false}>
38
- {FONT_SIZES.map((size) => (
39
- <TouchableOpacity
40
- key={size}
41
- style={[
42
- styles.sizeButton,
43
- {
44
- backgroundColor:
45
- fontSize === size
46
- ? tokens.colors.primary
47
- : tokens.colors.surface,
48
- borderColor:
49
- fontSize === size
50
- ? tokens.colors.primary
51
- : tokens.colors.borderLight,
52
- },
53
- ]}
54
- onPress={() => onFontSizeChange(size)}
55
- >
56
- <AtomicText
57
- type="bodySmall"
58
- style={{
59
- color:
60
- fontSize === size ? tokens.colors.onPrimary : tokens.colors.textPrimary,
61
- fontWeight: fontSize === size ? "600" : "400",
62
- }}
63
- >
64
- {size}
65
- </AtomicText>
66
- </TouchableOpacity>
67
- ))}
68
- </ScrollView>
44
+ <Selector
45
+ items={items}
46
+ selectedValue={fontSize}
47
+ onSelect={onFontSizeChange}
48
+ orientation="horizontal"
49
+ testID="font-size-selector"
50
+ />
69
51
  </View>
70
52
  );
71
53
  };
72
-
73
- const styles = StyleSheet.create({
74
- section: {
75
- marginBottom: 24,
76
- },
77
- sizeButton: {
78
- paddingHorizontal: 16,
79
- paddingVertical: 10,
80
- borderRadius: 8,
81
- borderWidth: 1,
82
- marginRight: 8,
83
- },
84
- });
@@ -1,12 +1,14 @@
1
1
  /**
2
- * OptionSelector Component
2
+ * Option Selector Component
3
3
  * Reusable selector for font family, font weight, etc.
4
+ * REFACTORED: Uses generic Selector component (29 lines)
4
5
  */
5
6
 
6
- import React from "react";
7
- import { View, StyleSheet, TouchableOpacity } from "react-native";
7
+ import React, { useMemo } from "react";
8
+ import { View } from "react-native";
8
9
  import { AtomicText } from "@umituz/react-native-design-system/atoms";
9
10
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
11
+ import { Selector, type SelectorItem } from "../generic/Selector";
10
12
 
11
13
  interface Option {
12
14
  label: string;
@@ -28,8 +30,13 @@ export const OptionSelector: React.FC<OptionSelectorProps> = ({
28
30
  }) => {
29
31
  const tokens = useAppDesignTokens();
30
32
 
33
+ const items = useMemo<SelectorItem<string>[]>(
34
+ () => options.map((option) => ({ value: option.value, label: option.label })),
35
+ [options],
36
+ );
37
+
31
38
  return (
32
- <View style={styles.section}>
39
+ <View style={{ marginBottom: tokens.spacing.md }}>
33
40
  <AtomicText
34
41
  type="bodyMedium"
35
42
  style={{
@@ -40,57 +47,13 @@ export const OptionSelector: React.FC<OptionSelectorProps> = ({
40
47
  >
41
48
  {title}
42
49
  </AtomicText>
43
- <View style={styles.optionsGrid}>
44
- {options.map((option) => (
45
- <TouchableOpacity
46
- key={option.value}
47
- style={[
48
- styles.optionButton,
49
- {
50
- backgroundColor:
51
- selectedValue === option.value
52
- ? tokens.colors.primary + "20"
53
- : tokens.colors.surface,
54
- borderColor:
55
- selectedValue === option.value
56
- ? tokens.colors.primary
57
- : tokens.colors.borderLight,
58
- },
59
- ]}
60
- onPress={() => onValueChange(option.value)}
61
- >
62
- <AtomicText
63
- type="labelSmall"
64
- style={{
65
- color:
66
- selectedValue === option.value
67
- ? tokens.colors.primary
68
- : tokens.colors.textPrimary,
69
- fontWeight: selectedValue === option.value ? "600" : "400",
70
- }}
71
- >
72
- {option.label}
73
- </AtomicText>
74
- </TouchableOpacity>
75
- ))}
76
- </View>
50
+ <Selector
51
+ items={items}
52
+ selectedValue={selectedValue}
53
+ onSelect={onValueChange}
54
+ orientation="grid"
55
+ testID="option-selector"
56
+ />
77
57
  </View>
78
58
  );
79
59
  };
80
-
81
- const styles = StyleSheet.create({
82
- section: {
83
- marginBottom: 24,
84
- },
85
- optionsGrid: {
86
- flexDirection: "row",
87
- flexWrap: "wrap",
88
- gap: 8,
89
- },
90
- optionButton: {
91
- paddingHorizontal: 16,
92
- paddingVertical: 10,
93
- borderRadius: 8,
94
- borderWidth: 1,
95
- },
96
- });
@@ -1,13 +1,15 @@
1
1
  /**
2
- * TextAlignSelector Component
2
+ * Text Align Selector Component
3
3
  * Text alignment selector for text layer
4
+ * REFACTORED: Uses generic Selector component (38 lines)
4
5
  */
5
6
 
6
- import React from "react";
7
- import { View, StyleSheet, TouchableOpacity } from "react-native";
8
- import { useLocalization } from "@umituz/react-native-settings";
9
- 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";
10
10
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
11
+ import { useLocalization } from "@umituz/react-native-settings";
12
+ import { Selector, type SelectorItem } from "../generic/Selector";
11
13
  import { TEXT_ALIGNS } from "../../../infrastructure/constants/text-layer.constants";
12
14
 
13
15
  interface TextAlignSelectorProps {
@@ -22,8 +24,17 @@ export const TextAlignSelector: React.FC<TextAlignSelectorProps> = ({
22
24
  const { t } = useLocalization();
23
25
  const tokens = useAppDesignTokens();
24
26
 
27
+ const items = useMemo<SelectorItem<"left" | "center" | "right">[]>(
28
+ () => TEXT_ALIGNS.map((align) => ({
29
+ value: align.value,
30
+ label: "",
31
+ icon: align.icon,
32
+ })),
33
+ [],
34
+ );
35
+
25
36
  return (
26
- <View style={styles.section}>
37
+ <View style={{ marginBottom: tokens.spacing.md }}>
27
38
  <AtomicText
28
39
  type="bodyMedium"
29
40
  style={{
@@ -34,51 +45,13 @@ export const TextAlignSelector: React.FC<TextAlignSelectorProps> = ({
34
45
  >
35
46
  {t("editor.properties.text_align")}
36
47
  </AtomicText>
37
- <View style={styles.alignButtons}>
38
- {TEXT_ALIGNS.map((align) => (
39
- <TouchableOpacity
40
- key={align.value}
41
- style={[
42
- styles.alignButton,
43
- {
44
- backgroundColor:
45
- textAlign === align.value
46
- ? tokens.colors.primary
47
- : tokens.colors.surface,
48
- borderColor:
49
- textAlign === align.value
50
- ? tokens.colors.primary
51
- : tokens.colors.borderLight,
52
- },
53
- ]}
54
- onPress={() => onTextAlignChange(align.value)}
55
- >
56
- <AtomicIcon
57
- name={align.icon}
58
- size="md"
59
- color={textAlign === align.value ? "onSurface" : "secondary"}
60
- />
61
- </TouchableOpacity>
62
- ))}
63
- </View>
48
+ <Selector
49
+ items={items}
50
+ selectedValue={textAlign}
51
+ onSelect={onTextAlignChange}
52
+ icon
53
+ testID="text-align-selector"
54
+ />
64
55
  </View>
65
56
  );
66
57
  };
67
-
68
- const styles = StyleSheet.create({
69
- section: {
70
- marginBottom: 24,
71
- },
72
- alignButtons: {
73
- flexDirection: "row",
74
- gap: 8,
75
- },
76
- alignButton: {
77
- flex: 1,
78
- paddingVertical: 12,
79
- borderRadius: 8,
80
- borderWidth: 2,
81
- alignItems: "center",
82
- justifyContent: "center",
83
- },
84
- });
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Generic Form Hook
3
+ * Replaces: useImageLayerForm, useTextLayerForm, useShapeLayerForm, useAudioLayerForm, useAnimationLayerForm
4
+ */
5
+
6
+ import { useState, useCallback, useMemo } from "react";
7
+
8
+ export interface FormValidator<T = unknown> {
9
+ required?: boolean;
10
+ validate?: (value: T) => string | undefined;
11
+ }
12
+
13
+ export interface FormConfig<T extends Record<string, unknown>> {
14
+ initialValues: T;
15
+ validators?: Partial<Record<keyof T, FormValidator>>;
16
+ onSubmit: (values: T) => void | Promise<void>;
17
+ }
18
+
19
+ export interface FormReturn<T extends Record<string, unknown>> {
20
+ values: T;
21
+ errors: Partial<Record<keyof T, string>>;
22
+ touched: Partial<Record<keyof T, boolean>>;
23
+ isValid: boolean;
24
+ isSubmitting: boolean;
25
+ setValue: <K extends keyof T>(field: K, value: T[K]) => void;
26
+ setError: <K extends keyof T>(field: K, error: string | undefined) => void;
27
+ setTouched: <K extends keyof T>(field: K, touched: boolean) => void;
28
+ handleSubmit: () => Promise<void>;
29
+ resetForm: () => void;
30
+ }
31
+
32
+ export function useForm<T extends Record<string, unknown>>({ initialValues, validators = {}, onSubmit }: FormConfig<T>): FormReturn<T> {
33
+ const [values, setValues] = useState<T>(initialValues);
34
+ const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
35
+ const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
36
+ const [isSubmitting, setIsSubmitting] = useState(false);
37
+
38
+ const validateField = useCallback(<K extends keyof T>(field: K, value: T[K]): string | undefined => {
39
+ const validator = validators[field];
40
+ if (!validator) return undefined;
41
+ if (validator.required && (value === undefined || value === null || value === "")) return "This field is required";
42
+ if (validator.validate) return validator.validate(value);
43
+ return undefined;
44
+ }, [validators]);
45
+
46
+ const validateForm = useCallback((): boolean => {
47
+ const newErrors: Partial<Record<keyof T, string>> = {};
48
+ let isValid = true;
49
+ (Object.keys(validators) as Array<keyof T>).forEach((field) => {
50
+ const error = validateField(field, values[field]);
51
+ if (error) { newErrors[field] = error; isValid = false; }
52
+ });
53
+ setErrors(newErrors);
54
+ return isValid;
55
+ }, [validators, values, validateField]);
56
+
57
+ const setValue = useCallback(<K extends keyof T>(field: K, value: T[K]) => {
58
+ setValues((prev) => ({ ...prev, [field]: value }));
59
+ if (touched[field]) {
60
+ const error = validateField(field, value);
61
+ setErrors((prev) => ({ ...prev, [field]: error }));
62
+ }
63
+ }, [touched, validateField]);
64
+
65
+ const setError = useCallback(<K extends keyof T>(field: K, error: string | undefined) => {
66
+ setErrors((prev) => ({ ...prev, [field]: error }));
67
+ }, []);
68
+
69
+ const setFieldTouched = useCallback(<K extends keyof T>(field: K, touchedValue: boolean) => {
70
+ setTouched((prev) => ({ ...prev, [field]: touchedValue }));
71
+ if (touchedValue) {
72
+ const error = validateField(field, values[field]);
73
+ setErrors((prev) => ({ ...prev, [field]: error }));
74
+ }
75
+ }, [values, validateField]);
76
+
77
+ const isValid = useMemo(() => Object.keys(validators).every((field) => !errors[field as keyof T]), [errors, validators]);
78
+
79
+ const handleSubmit = useCallback(async () => {
80
+ const allTouched = Object.keys(validators).reduce((acc, field) => ({ ...acc, [field]: true }), {} as Partial<Record<keyof T, boolean>>);
81
+ setTouched(allTouched);
82
+ const valid = validateForm();
83
+ if (!valid) return;
84
+ setIsSubmitting(true);
85
+ try {
86
+ await onSubmit(values);
87
+ } finally {
88
+ setIsSubmitting(false);
89
+ }
90
+ }, [values, validators, onSubmit, validateForm]);
91
+
92
+ const resetForm = useCallback(() => {
93
+ setValues(initialValues);
94
+ setErrors({});
95
+ setTouched({});
96
+ }, [initialValues]);
97
+
98
+ return { values, errors, touched, isValid, isSubmitting, setValue, setError, setTouched: setFieldTouched, handleSubmit, resetForm };
99
+ }