@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,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
|
8
|
-
import { AtomicText
|
|
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={
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
});
|
|
@@ -29,7 +29,6 @@ export const SubtitleListItem: React.FC<SubtitleListItemProps> = ({
|
|
|
29
29
|
item: {
|
|
30
30
|
flexDirection: "row" as const,
|
|
31
31
|
alignItems: "center" as const,
|
|
32
|
-
backgroundColor: tokens.colors.surface,
|
|
33
32
|
borderRadius: tokens.borders.radius.md,
|
|
34
33
|
marginHorizontal: tokens.spacing.md,
|
|
35
34
|
marginBottom: tokens.spacing.sm,
|
|
@@ -70,17 +69,17 @@ export const SubtitleListItem: React.FC<SubtitleListItemProps> = ({
|
|
|
70
69
|
activeOpacity={0.7}
|
|
71
70
|
>
|
|
72
71
|
<View style={styles.itemTimeRow}>
|
|
73
|
-
<AtomicText
|
|
72
|
+
<AtomicText color="textSecondary">
|
|
74
73
|
{formatTimeDetailed(subtitle.startTime)}
|
|
75
74
|
</AtomicText>
|
|
76
|
-
<AtomicText
|
|
75
|
+
<AtomicText color="textSecondary">
|
|
77
76
|
→
|
|
78
77
|
</AtomicText>
|
|
79
|
-
<AtomicText
|
|
78
|
+
<AtomicText color="textSecondary">
|
|
80
79
|
{formatTimeDetailed(subtitle.endTime)}
|
|
81
80
|
</AtomicText>
|
|
82
81
|
</View>
|
|
83
|
-
<AtomicText
|
|
82
|
+
<AtomicText color="textPrimary" numberOfLines={2}>
|
|
84
83
|
{subtitle.text}
|
|
85
84
|
</AtomicText>
|
|
86
85
|
</TouchableOpacity>
|
|
@@ -55,7 +55,7 @@ export const SubtitleModal: React.FC<SubtitleModalProps> = ({
|
|
|
55
55
|
paddingHorizontal: tokens.spacing.md,
|
|
56
56
|
paddingTop: tokens.spacing.md,
|
|
57
57
|
paddingBottom: tokens.spacing.xl,
|
|
58
|
-
maxHeight: "90%",
|
|
58
|
+
maxHeight: "90%" as const,
|
|
59
59
|
},
|
|
60
60
|
handle: {
|
|
61
61
|
width: 36,
|
|
@@ -136,7 +136,7 @@ export const SubtitleModal: React.FC<SubtitleModalProps> = ({
|
|
|
136
136
|
/>
|
|
137
137
|
</View>
|
|
138
138
|
|
|
139
|
-
<SubtitleStylePicker
|
|
139
|
+
<SubtitleStylePicker style={style} previewText={text} onChange={onChangeStyle} t={(key) => key} />
|
|
140
140
|
|
|
141
141
|
<View style={styles.actionRow}>
|
|
142
142
|
<TouchableOpacity style={styles.cancelBtn} onPress={onCancel}>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Manages subtitle form state and operations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useState, useCallback
|
|
6
|
+
import { useState, useCallback } from "react";
|
|
7
7
|
import { DEFAULT_SUBTITLE_STYLE } from "../../../infrastructure/constants/subtitle.constants";
|
|
8
8
|
import type { Subtitle, SubtitleStyle } from "../../../domain/entities/video-project.types";
|
|
9
9
|
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
|
8
|
-
import {
|
|
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={
|
|
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
|
-
<
|
|
38
|
-
{
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
*
|
|
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
|
|
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={
|
|
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
|
-
<
|
|
38
|
-
{
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
*
|
|
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
|
|
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={
|
|
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
|
-
<
|
|
44
|
-
{
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
*
|
|
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
|
|
8
|
-
import {
|
|
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={
|
|
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
|
-
<
|
|
38
|
-
{
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
});
|
|
@@ -5,30 +5,30 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { useState, useCallback } from "react";
|
|
8
|
-
import type { Layer } from "
|
|
8
|
+
import type { Layer, ImageLayer, TextLayer } from "../../../domain/entities/video-project.types";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Form field validator function type
|
|
12
12
|
*/
|
|
13
|
-
export type ValidatorFn<T> = (value: T[
|
|
13
|
+
export type ValidatorFn<T, K extends keyof T = keyof T> = (value: T[K]) => string | null;
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Layer form configuration
|
|
17
17
|
*/
|
|
18
18
|
export interface UseLayerFormConfig<T extends Record<string, unknown>> {
|
|
19
19
|
initialValues: Partial<T>;
|
|
20
|
-
validators?:
|
|
21
|
-
buildData: (formState: T) => Partial<Layer>;
|
|
20
|
+
validators?: Record<string, (value: unknown) => string | null>;
|
|
21
|
+
buildData: (formState: T) => Partial<Layer> | Partial<ImageLayer> | Partial<TextLayer>;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Layer form return type
|
|
26
26
|
*/
|
|
27
|
-
export interface UseLayerFormReturn<T extends Record<string, unknown>> {
|
|
27
|
+
export interface UseLayerFormReturn<T extends Record<string, unknown>, R = Partial<Layer>> {
|
|
28
28
|
formState: T;
|
|
29
29
|
updateField: <K extends keyof T>(field: K, value: T[K]) => void;
|
|
30
30
|
setFormState: (state: T | ((prev: T) => T)) => void;
|
|
31
|
-
buildLayerData: () =>
|
|
31
|
+
buildLayerData: () => R;
|
|
32
32
|
isValid: boolean;
|
|
33
33
|
errors: Partial<Record<keyof T, string | null>>;
|
|
34
34
|
validateField: <K extends keyof T>(field: K) => string | null;
|
|
@@ -39,9 +39,9 @@ export interface UseLayerFormReturn<T extends Record<string, unknown>> {
|
|
|
39
39
|
* Generic hook for managing layer form state
|
|
40
40
|
* Provides type-safe form management with validation support
|
|
41
41
|
*/
|
|
42
|
-
export function useLayerForm<T extends Record<string, unknown>>(
|
|
42
|
+
export function useLayerForm<T extends Record<string, unknown>, R = Partial<Layer>>(
|
|
43
43
|
config: UseLayerFormConfig<T>,
|
|
44
|
-
): UseLayerFormReturn<T> {
|
|
44
|
+
): UseLayerFormReturn<T, R> {
|
|
45
45
|
const { initialValues, validators = {}, buildData } = config;
|
|
46
46
|
|
|
47
47
|
const [formState, setFormState] = useState<T>(
|
|
@@ -60,7 +60,7 @@ export function useLayerForm<T extends Record<string, unknown>>(
|
|
|
60
60
|
}));
|
|
61
61
|
|
|
62
62
|
// Clear error for this field
|
|
63
|
-
if (errors[field]) {
|
|
63
|
+
if (errors[field as keyof typeof errors]) {
|
|
64
64
|
setErrors((prev) => ({
|
|
65
65
|
...prev,
|
|
66
66
|
[field]: null,
|
|
@@ -72,7 +72,7 @@ export function useLayerForm<T extends Record<string, unknown>>(
|
|
|
72
72
|
|
|
73
73
|
const validateField = useCallback(
|
|
74
74
|
<K extends keyof T>(field: K): string | null => {
|
|
75
|
-
const validator = validators[field];
|
|
75
|
+
const validator = validators[String(field)];
|
|
76
76
|
if (!validator) return null;
|
|
77
77
|
|
|
78
78
|
const error = validator(formState[field]);
|
|
@@ -91,10 +91,13 @@ export function useLayerForm<T extends Record<string, unknown>>(
|
|
|
91
91
|
const newErrors: Partial<Record<keyof T, string | null>> = {};
|
|
92
92
|
|
|
93
93
|
for (const field in validators) {
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
const validator = validators[field];
|
|
95
|
+
if (validator) {
|
|
96
|
+
const error = validator(formState[field as keyof T]);
|
|
97
|
+
if (error) {
|
|
98
|
+
newErrors[field as keyof T] = error;
|
|
99
|
+
hasError = true;
|
|
100
|
+
}
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
103
|
|
|
@@ -102,8 +105,8 @@ export function useLayerForm<T extends Record<string, unknown>>(
|
|
|
102
105
|
return !hasError;
|
|
103
106
|
}, [formState, validators]);
|
|
104
107
|
|
|
105
|
-
const buildLayerData = useCallback(():
|
|
106
|
-
return buildData(formState);
|
|
108
|
+
const buildLayerData = useCallback((): R => {
|
|
109
|
+
return buildData(formState) as R;
|
|
107
110
|
}, [formState, buildData]);
|
|
108
111
|
|
|
109
112
|
const isValid = Object.values(errors).every((error) => error === null);
|