@umituz/react-native-video-editor 1.0.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/README.md +92 -0
- package/package.json +48 -0
- package/src/domain/entities/index.ts +50 -0
- package/src/domain/entities/video-project.types.ts +153 -0
- package/src/index.ts +100 -0
- package/src/infrastructure/constants/animation-layer.constants.ts +32 -0
- package/src/infrastructure/constants/audio-layer.constants.ts +14 -0
- package/src/infrastructure/constants/export.constants.ts +28 -0
- package/src/infrastructure/constants/image-layer.constants.ts +12 -0
- package/src/infrastructure/constants/index.ts +11 -0
- package/src/infrastructure/constants/shape-layer.constants.ts +29 -0
- package/src/infrastructure/constants/text-layer.constants.ts +40 -0
- package/src/infrastructure/services/export-orchestrator.service.ts +122 -0
- package/src/infrastructure/services/image-layer-operations.service.ts +108 -0
- package/src/infrastructure/services/layer-manipulation.service.ts +93 -0
- package/src/infrastructure/services/layer-operations/index.ts +9 -0
- package/src/infrastructure/services/layer-operations/layer-delete.service.ts +47 -0
- package/src/infrastructure/services/layer-operations/layer-duplicate.service.ts +66 -0
- package/src/infrastructure/services/layer-operations/layer-order.service.ts +82 -0
- package/src/infrastructure/services/layer-operations/layer-transform.service.ts +160 -0
- package/src/infrastructure/services/layer-operations.service.ts +198 -0
- package/src/infrastructure/services/scene-operations.service.ts +166 -0
- package/src/infrastructure/services/shape-layer-operations.service.ts +65 -0
- package/src/infrastructure/services/text-layer-operations.service.ts +114 -0
- package/src/presentation/components/AnimationEditor.tsx +103 -0
- package/src/presentation/components/AudioEditor.tsx +144 -0
- package/src/presentation/components/DraggableLayer.tsx +110 -0
- package/src/presentation/components/EditorHeader.tsx +107 -0
- package/src/presentation/components/EditorPreviewArea.tsx +221 -0
- package/src/presentation/components/EditorTimeline.tsx +136 -0
- package/src/presentation/components/EditorToolPanel.tsx +180 -0
- package/src/presentation/components/ExportDialog.tsx +135 -0
- package/src/presentation/components/ImageLayerEditor.tsx +95 -0
- package/src/presentation/components/LayerActionsMenu.tsx +197 -0
- package/src/presentation/components/SceneActionsMenu.tsx +69 -0
- package/src/presentation/components/ShapeLayerEditor.tsx +108 -0
- package/src/presentation/components/TextLayerEditor.tsx +104 -0
- package/src/presentation/components/animation-layer/AnimationEditorActions.tsx +104 -0
- package/src/presentation/components/animation-layer/AnimationInfoBanner.tsx +43 -0
- package/src/presentation/components/animation-layer/AnimationTypeSelector.tsx +105 -0
- package/src/presentation/components/animation-layer/index.ts +8 -0
- package/src/presentation/components/audio-layer/AudioEditorActions.tsx +115 -0
- package/src/presentation/components/audio-layer/AudioFileSelector.tsx +126 -0
- package/src/presentation/components/audio-layer/FadeEffectsSelector.tsx +151 -0
- package/src/presentation/components/audio-layer/InfoBanner.tsx +43 -0
- package/src/presentation/components/audio-layer/VolumeSelector.tsx +98 -0
- package/src/presentation/components/audio-layer/index.ts +10 -0
- package/src/presentation/components/draggable-layer/LayerContent.tsx +106 -0
- package/src/presentation/components/draggable-layer/ResizeHandles.tsx +97 -0
- package/src/presentation/components/draggable-layer/index.ts +7 -0
- package/src/presentation/components/export/ExportActions.tsx +101 -0
- package/src/presentation/components/export/ExportInfoBanner.tsx +44 -0
- package/src/presentation/components/export/ExportProgress.tsx +114 -0
- package/src/presentation/components/export/OptionSelectorRow.tsx +101 -0
- package/src/presentation/components/export/ProjectInfoBox.tsx +61 -0
- package/src/presentation/components/export/WatermarkToggle.tsx +87 -0
- package/src/presentation/components/export/index.ts +11 -0
- package/src/presentation/components/image-layer/ImagePreview.tsx +70 -0
- package/src/presentation/components/image-layer/ImageSelectionButtons.tsx +82 -0
- package/src/presentation/components/image-layer/OpacitySelector.tsx +91 -0
- package/src/presentation/components/image-layer/index.ts +8 -0
- package/src/presentation/components/index.ts +17 -0
- package/src/presentation/components/shape-layer/ColorPickerHorizontal.tsx +92 -0
- package/src/presentation/components/shape-layer/ShapePreview.tsx +57 -0
- package/src/presentation/components/shape-layer/ShapeTypeSelector.tsx +102 -0
- package/src/presentation/components/shape-layer/ValueSelector.tsx +106 -0
- package/src/presentation/components/shape-layer/index.ts +9 -0
- package/src/presentation/components/text-layer/ColorPicker.tsx +91 -0
- package/src/presentation/components/text-layer/EditorActions.tsx +95 -0
- package/src/presentation/components/text-layer/FontSizeSelector.tsx +86 -0
- package/src/presentation/components/text-layer/OptionSelector.tsx +98 -0
- package/src/presentation/components/text-layer/TextAlignSelector.tsx +87 -0
- package/src/presentation/components/text-layer/TextInputSection.tsx +70 -0
- package/src/presentation/components/text-layer/TextPreview.tsx +71 -0
- package/src/presentation/components/text-layer/index.ts +12 -0
- package/src/presentation/hooks/useAnimationLayerForm.ts +72 -0
- package/src/presentation/hooks/useAudioLayerForm.ts +76 -0
- package/src/presentation/hooks/useDraggableLayerGestures.ts +166 -0
- package/src/presentation/hooks/useEditorActions.tsx +93 -0
- package/src/presentation/hooks/useEditorBottomSheet.ts +43 -0
- package/src/presentation/hooks/useEditorHistory.ts +80 -0
- package/src/presentation/hooks/useEditorLayers.ts +97 -0
- package/src/presentation/hooks/useEditorPlayback.ts +90 -0
- package/src/presentation/hooks/useEditorScenes.ts +106 -0
- package/src/presentation/hooks/useExport.ts +67 -0
- package/src/presentation/hooks/useExportActions.tsx +51 -0
- package/src/presentation/hooks/useExportForm.ts +96 -0
- package/src/presentation/hooks/useImageLayerForm.ts +57 -0
- package/src/presentation/hooks/useImageLayerOperations.ts +71 -0
- package/src/presentation/hooks/useLayerActions.tsx +162 -0
- package/src/presentation/hooks/useLayerManipulation.ts +178 -0
- package/src/presentation/hooks/useMenuActions.tsx +92 -0
- package/src/presentation/hooks/useSceneActions.tsx +81 -0
- package/src/presentation/hooks/useShapeLayerForm.ts +84 -0
- package/src/presentation/hooks/useShapeLayerOperations.ts +52 -0
- package/src/presentation/hooks/useTextLayerForm.ts +100 -0
- package/src/presentation/hooks/useTextLayerOperations.ts +74 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor Tool Panel Component
|
|
3
|
+
* Single Responsibility: Display editor tool buttons
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import {
|
|
8
|
+
View,
|
|
9
|
+
ScrollView,
|
|
10
|
+
TouchableOpacity,
|
|
11
|
+
StyleSheet,
|
|
12
|
+
Alert,
|
|
13
|
+
} from "react-native";
|
|
14
|
+
import {
|
|
15
|
+
AtomicText,
|
|
16
|
+
AtomicIcon,
|
|
17
|
+
useAppDesignTokens,
|
|
18
|
+
} from "@umituz/react-native-design-system";
|
|
19
|
+
import { useLocalization } from "@umituz/react-native-localization";
|
|
20
|
+
import type { Audio } from "@domains/video";
|
|
21
|
+
|
|
22
|
+
export interface EditorToolPanelProps {
|
|
23
|
+
onAddText: () => void;
|
|
24
|
+
onAddImage: () => void;
|
|
25
|
+
onAddShape: () => void;
|
|
26
|
+
onAudio: () => void;
|
|
27
|
+
hasAudio: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const EditorToolPanel: React.FC<EditorToolPanelProps> = ({
|
|
31
|
+
onAddText,
|
|
32
|
+
onAddImage,
|
|
33
|
+
onAddShape,
|
|
34
|
+
onAudio,
|
|
35
|
+
hasAudio,
|
|
36
|
+
}) => {
|
|
37
|
+
const { t } = useLocalization();
|
|
38
|
+
const tokens = useAppDesignTokens();
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<View
|
|
42
|
+
style={[styles.toolPanel, { backgroundColor: tokens.colors.surface }]}
|
|
43
|
+
>
|
|
44
|
+
<AtomicText
|
|
45
|
+
type="bodyMedium"
|
|
46
|
+
style={{
|
|
47
|
+
color: tokens.colors.textPrimary,
|
|
48
|
+
fontWeight: "600",
|
|
49
|
+
marginBottom: 12,
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
{t("editor.tools.title")}
|
|
53
|
+
</AtomicText>
|
|
54
|
+
|
|
55
|
+
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
|
56
|
+
<TouchableOpacity
|
|
57
|
+
style={[
|
|
58
|
+
styles.toolButton,
|
|
59
|
+
{ backgroundColor: tokens.colors.backgroundPrimary },
|
|
60
|
+
]}
|
|
61
|
+
onPress={onAddText}
|
|
62
|
+
>
|
|
63
|
+
<AtomicIcon name="Type" size="md" color="primary" />
|
|
64
|
+
<AtomicText
|
|
65
|
+
type="labelSmall"
|
|
66
|
+
style={{ color: tokens.colors.textPrimary, marginTop: 4 }}
|
|
67
|
+
>
|
|
68
|
+
{t("editor.tools.text")}
|
|
69
|
+
</AtomicText>
|
|
70
|
+
</TouchableOpacity>
|
|
71
|
+
|
|
72
|
+
<TouchableOpacity
|
|
73
|
+
style={[
|
|
74
|
+
styles.toolButton,
|
|
75
|
+
{ backgroundColor: tokens.colors.backgroundPrimary },
|
|
76
|
+
]}
|
|
77
|
+
onPress={onAddImage}
|
|
78
|
+
>
|
|
79
|
+
<AtomicIcon name="image-outline" size="md" color="primary" />
|
|
80
|
+
<AtomicText
|
|
81
|
+
type="labelSmall"
|
|
82
|
+
style={{ color: tokens.colors.textPrimary, marginTop: 4 }}
|
|
83
|
+
>
|
|
84
|
+
{t("editor.tools.image")}
|
|
85
|
+
</AtomicText>
|
|
86
|
+
</TouchableOpacity>
|
|
87
|
+
|
|
88
|
+
<TouchableOpacity
|
|
89
|
+
style={[
|
|
90
|
+
styles.toolButton,
|
|
91
|
+
{ backgroundColor: tokens.colors.backgroundPrimary },
|
|
92
|
+
]}
|
|
93
|
+
onPress={onAddShape}
|
|
94
|
+
>
|
|
95
|
+
<AtomicIcon name="Square" size="md" color="primary" />
|
|
96
|
+
<AtomicText
|
|
97
|
+
type="labelSmall"
|
|
98
|
+
style={{ color: tokens.colors.textPrimary, marginTop: 4 }}
|
|
99
|
+
>
|
|
100
|
+
{t("editor.tools.shape")}
|
|
101
|
+
</AtomicText>
|
|
102
|
+
</TouchableOpacity>
|
|
103
|
+
|
|
104
|
+
<TouchableOpacity
|
|
105
|
+
style={[
|
|
106
|
+
styles.toolButton,
|
|
107
|
+
{ backgroundColor: tokens.colors.backgroundPrimary },
|
|
108
|
+
]}
|
|
109
|
+
onPress={onAudio}
|
|
110
|
+
>
|
|
111
|
+
<AtomicIcon name="Music" size="md" color="primary" />
|
|
112
|
+
<AtomicText
|
|
113
|
+
type="labelSmall"
|
|
114
|
+
style={{ color: tokens.colors.textPrimary, marginTop: 4 }}
|
|
115
|
+
>
|
|
116
|
+
{t("editor.tools.audio")}
|
|
117
|
+
</AtomicText>
|
|
118
|
+
{hasAudio && (
|
|
119
|
+
<View
|
|
120
|
+
style={[
|
|
121
|
+
styles.audioBadge,
|
|
122
|
+
{
|
|
123
|
+
backgroundColor: tokens.colors.success,
|
|
124
|
+
borderColor: tokens.colors.surface,
|
|
125
|
+
},
|
|
126
|
+
]}
|
|
127
|
+
/>
|
|
128
|
+
)}
|
|
129
|
+
</TouchableOpacity>
|
|
130
|
+
|
|
131
|
+
<TouchableOpacity
|
|
132
|
+
style={[
|
|
133
|
+
styles.toolButton,
|
|
134
|
+
{ backgroundColor: tokens.colors.backgroundPrimary },
|
|
135
|
+
]}
|
|
136
|
+
onPress={() =>
|
|
137
|
+
Alert.alert(
|
|
138
|
+
t("editor.tools.effects", "Effects"),
|
|
139
|
+
t("editor.tools.effectsComingSoon", "Coming soon!"),
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
>
|
|
143
|
+
<AtomicIcon name="sparkles-outline" size="md" color="primary" />
|
|
144
|
+
<AtomicText
|
|
145
|
+
type="labelSmall"
|
|
146
|
+
style={{ color: tokens.colors.textPrimary, marginTop: 4 }}
|
|
147
|
+
>
|
|
148
|
+
{t("editor.tools.effects")}
|
|
149
|
+
</AtomicText>
|
|
150
|
+
</TouchableOpacity>
|
|
151
|
+
</ScrollView>
|
|
152
|
+
</View>
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const styles = StyleSheet.create({
|
|
157
|
+
toolPanel: {
|
|
158
|
+
padding: 16,
|
|
159
|
+
marginHorizontal: 16,
|
|
160
|
+
marginBottom: 16,
|
|
161
|
+
borderRadius: 12,
|
|
162
|
+
},
|
|
163
|
+
toolButton: {
|
|
164
|
+
width: 80,
|
|
165
|
+
height: 80,
|
|
166
|
+
borderRadius: 12,
|
|
167
|
+
alignItems: "center",
|
|
168
|
+
justifyContent: "center",
|
|
169
|
+
marginRight: 12,
|
|
170
|
+
},
|
|
171
|
+
audioBadge: {
|
|
172
|
+
position: "absolute",
|
|
173
|
+
top: 8,
|
|
174
|
+
right: 8,
|
|
175
|
+
width: 10,
|
|
176
|
+
height: 10,
|
|
177
|
+
borderRadius: 5,
|
|
178
|
+
borderWidth: 2,
|
|
179
|
+
},
|
|
180
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExportDialog Component
|
|
3
|
+
* Main component for video export dialog
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useCallback } from "react";
|
|
7
|
+
import { View, ScrollView, StyleSheet } from "react-native";
|
|
8
|
+
import { useLocalization } from "@umituz/react-native-localization";
|
|
9
|
+
import type { ExportSettings, VideoProject } from "@domains/video";
|
|
10
|
+
import { useExportForm } from "../../hooks/useExportForm";
|
|
11
|
+
import { useExport } from "../../hooks/useExport";
|
|
12
|
+
import {
|
|
13
|
+
RESOLUTIONS,
|
|
14
|
+
QUALITIES,
|
|
15
|
+
FORMATS,
|
|
16
|
+
} from "../../constants/export.constants";
|
|
17
|
+
import {
|
|
18
|
+
ProjectInfoBox,
|
|
19
|
+
OptionSelectorRow,
|
|
20
|
+
WatermarkToggle,
|
|
21
|
+
ExportProgress,
|
|
22
|
+
ExportInfoBanner,
|
|
23
|
+
ExportActions,
|
|
24
|
+
} from "./export";
|
|
25
|
+
|
|
26
|
+
interface ExportDialogProps {
|
|
27
|
+
project: VideoProject;
|
|
28
|
+
onExport: (settings: ExportSettings, uri?: string) => void;
|
|
29
|
+
onCancel: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const ExportDialog: React.FC<ExportDialogProps> = ({
|
|
33
|
+
project,
|
|
34
|
+
onExport,
|
|
35
|
+
onCancel,
|
|
36
|
+
}) => {
|
|
37
|
+
const { t } = useLocalization();
|
|
38
|
+
const {
|
|
39
|
+
formState,
|
|
40
|
+
setResolution,
|
|
41
|
+
setQuality,
|
|
42
|
+
setFormat,
|
|
43
|
+
setIncludeWatermark,
|
|
44
|
+
buildExportSettings,
|
|
45
|
+
estimatedSize,
|
|
46
|
+
projectDuration,
|
|
47
|
+
} = useExportForm(project);
|
|
48
|
+
|
|
49
|
+
const { isExporting, exportProgress, exportVideo, resetExport } = useExport();
|
|
50
|
+
|
|
51
|
+
const handleExport = useCallback(async () => {
|
|
52
|
+
const settings = buildExportSettings();
|
|
53
|
+
const result = await exportVideo(project, settings);
|
|
54
|
+
|
|
55
|
+
if (result.success) {
|
|
56
|
+
onExport(settings, result.uri);
|
|
57
|
+
} else {
|
|
58
|
+
resetExport();
|
|
59
|
+
}
|
|
60
|
+
}, [project, buildExportSettings, exportVideo, onExport, resetExport]);
|
|
61
|
+
|
|
62
|
+
const resolutionOptions = RESOLUTIONS.map((res) => ({
|
|
63
|
+
value: res,
|
|
64
|
+
label: res,
|
|
65
|
+
textTransform: "none" as const,
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
const qualityOptions = QUALITIES.map((qual) => ({
|
|
69
|
+
value: qual,
|
|
70
|
+
label: qual,
|
|
71
|
+
textTransform: "capitalize" as const,
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
const formatOptions = FORMATS.map((fmt) => ({
|
|
75
|
+
value: fmt,
|
|
76
|
+
label: fmt,
|
|
77
|
+
textTransform: "uppercase" as const,
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<View style={styles.container}>
|
|
82
|
+
<ScrollView showsVerticalScrollIndicator={false}>
|
|
83
|
+
<ProjectInfoBox
|
|
84
|
+
project={project}
|
|
85
|
+
duration={projectDuration}
|
|
86
|
+
estimatedSize={estimatedSize}
|
|
87
|
+
/>
|
|
88
|
+
|
|
89
|
+
<OptionSelectorRow
|
|
90
|
+
title={t("editor.export.resolution")}
|
|
91
|
+
options={resolutionOptions}
|
|
92
|
+
selectedValue={formState.resolution}
|
|
93
|
+
onValueChange={setResolution}
|
|
94
|
+
/>
|
|
95
|
+
|
|
96
|
+
<OptionSelectorRow
|
|
97
|
+
title={t("editor.export.quality")}
|
|
98
|
+
options={qualityOptions}
|
|
99
|
+
selectedValue={formState.quality}
|
|
100
|
+
onValueChange={setQuality}
|
|
101
|
+
/>
|
|
102
|
+
|
|
103
|
+
<OptionSelectorRow
|
|
104
|
+
title={t("editor.export.format")}
|
|
105
|
+
options={formatOptions}
|
|
106
|
+
selectedValue={formState.format}
|
|
107
|
+
onValueChange={setFormat}
|
|
108
|
+
/>
|
|
109
|
+
|
|
110
|
+
<WatermarkToggle
|
|
111
|
+
includeWatermark={formState.includeWatermark}
|
|
112
|
+
onToggle={setIncludeWatermark}
|
|
113
|
+
/>
|
|
114
|
+
|
|
115
|
+
{isExporting && exportProgress && (
|
|
116
|
+
<ExportProgress progress={exportProgress} />
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
{!isExporting && <ExportInfoBanner />}
|
|
120
|
+
</ScrollView>
|
|
121
|
+
|
|
122
|
+
<ExportActions
|
|
123
|
+
isExporting={isExporting}
|
|
124
|
+
onCancel={onCancel}
|
|
125
|
+
onExport={handleExport}
|
|
126
|
+
/>
|
|
127
|
+
</View>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const styles = StyleSheet.create({
|
|
132
|
+
container: {
|
|
133
|
+
paddingVertical: 16,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImageLayerEditor Component
|
|
3
|
+
* Main component for editing image layers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useCallback } from "react";
|
|
7
|
+
import { View, ScrollView, StyleSheet, Alert } from "react-native";
|
|
8
|
+
import { useImagePicker } from "@/domains/media";
|
|
9
|
+
import type { ImageLayer } from "@domains/video";
|
|
10
|
+
import { useImageLayerForm } from "../../hooks/useImageLayerForm";
|
|
11
|
+
import { IMAGE_PICKER_OPTIONS } from "../../constants/image-layer.constants";
|
|
12
|
+
import {
|
|
13
|
+
ImagePreview,
|
|
14
|
+
ImageSelectionButtons,
|
|
15
|
+
OpacitySelector,
|
|
16
|
+
} from "./image-layer";
|
|
17
|
+
import { EditorActions } from "./text-layer/EditorActions";
|
|
18
|
+
|
|
19
|
+
interface ImageLayerEditorProps {
|
|
20
|
+
layer?: ImageLayer;
|
|
21
|
+
onSave: (layerData: Partial<ImageLayer>) => void;
|
|
22
|
+
onCancel: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const ImageLayerEditor: React.FC<ImageLayerEditorProps> = ({
|
|
26
|
+
layer,
|
|
27
|
+
onSave,
|
|
28
|
+
onCancel,
|
|
29
|
+
}) => {
|
|
30
|
+
const { pickFromLibrary, pickFromCamera } = useImagePicker();
|
|
31
|
+
const { formState, setImageUri, setOpacity, buildLayerData, isValid } =
|
|
32
|
+
useImageLayerForm(layer);
|
|
33
|
+
|
|
34
|
+
const handlePickImage = useCallback(async () => {
|
|
35
|
+
const result = await pickFromLibrary(IMAGE_PICKER_OPTIONS);
|
|
36
|
+
|
|
37
|
+
if (!result.canceled && result.assets?.[0]) {
|
|
38
|
+
setImageUri(result.assets[0].uri);
|
|
39
|
+
}
|
|
40
|
+
}, [pickFromLibrary, setImageUri]);
|
|
41
|
+
|
|
42
|
+
const handleTakePhoto = useCallback(async () => {
|
|
43
|
+
const result = await pickFromCamera(IMAGE_PICKER_OPTIONS);
|
|
44
|
+
|
|
45
|
+
if (!result.canceled && result.assets?.[0]) {
|
|
46
|
+
setImageUri(result.assets[0].uri);
|
|
47
|
+
}
|
|
48
|
+
}, [pickFromCamera, setImageUri]);
|
|
49
|
+
|
|
50
|
+
const handleSave = useCallback(() => {
|
|
51
|
+
if (!isValid) {
|
|
52
|
+
Alert.alert("No Image", "Please select an image first.");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
onSave(buildLayerData());
|
|
56
|
+
}, [isValid, buildLayerData, onSave]);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<View style={styles.container}>
|
|
60
|
+
<ScrollView showsVerticalScrollIndicator={false}>
|
|
61
|
+
<View style={{ marginBottom: 24 }}>
|
|
62
|
+
<ImagePreview
|
|
63
|
+
imageUri={formState.imageUri}
|
|
64
|
+
opacity={formState.opacity}
|
|
65
|
+
/>
|
|
66
|
+
</View>
|
|
67
|
+
|
|
68
|
+
<ImageSelectionButtons
|
|
69
|
+
onPickFromGallery={handlePickImage}
|
|
70
|
+
onTakePhoto={handleTakePhoto}
|
|
71
|
+
/>
|
|
72
|
+
|
|
73
|
+
{formState.imageUri && (
|
|
74
|
+
<OpacitySelector
|
|
75
|
+
opacity={formState.opacity}
|
|
76
|
+
onOpacityChange={setOpacity}
|
|
77
|
+
/>
|
|
78
|
+
)}
|
|
79
|
+
</ScrollView>
|
|
80
|
+
|
|
81
|
+
<EditorActions
|
|
82
|
+
onCancel={onCancel}
|
|
83
|
+
onSave={handleSave}
|
|
84
|
+
saveLabel={layer ? "Update Image" : "Add Image"}
|
|
85
|
+
isValid={isValid}
|
|
86
|
+
/>
|
|
87
|
+
</View>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const styles = StyleSheet.create({
|
|
92
|
+
container: {
|
|
93
|
+
paddingVertical: 16,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer Actions Menu Component
|
|
3
|
+
* Single Responsibility: Display layer action menu
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
AtomicIcon,
|
|
11
|
+
useAppDesignTokens,
|
|
12
|
+
} from "@umituz/react-native-design-system";
|
|
13
|
+
import type { Layer, ImageLayer } from "@domains/video";
|
|
14
|
+
|
|
15
|
+
export interface LayerActionsMenuProps {
|
|
16
|
+
layer: Layer;
|
|
17
|
+
onEditText: () => void;
|
|
18
|
+
onEditImage: () => void;
|
|
19
|
+
onAnimate: () => void;
|
|
20
|
+
onDuplicate: () => void;
|
|
21
|
+
onMoveFront: () => void;
|
|
22
|
+
onMoveUp: () => void;
|
|
23
|
+
onMoveDown: () => void;
|
|
24
|
+
onMoveBack: () => void;
|
|
25
|
+
onDelete: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const LayerActionsMenu: React.FC<LayerActionsMenuProps> = ({
|
|
29
|
+
layer,
|
|
30
|
+
onEditText,
|
|
31
|
+
onEditImage,
|
|
32
|
+
onAnimate,
|
|
33
|
+
onDuplicate,
|
|
34
|
+
onMoveFront,
|
|
35
|
+
onMoveUp,
|
|
36
|
+
onMoveDown,
|
|
37
|
+
onMoveBack,
|
|
38
|
+
onDelete,
|
|
39
|
+
}) => {
|
|
40
|
+
const tokens = useAppDesignTokens();
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<View style={{ paddingVertical: 8 }}>
|
|
44
|
+
{layer.type === "text" && (
|
|
45
|
+
<TouchableOpacity style={styles.actionMenuItem} onPress={onEditText}>
|
|
46
|
+
<AtomicIcon name="Edit" size="md" color="primary" />
|
|
47
|
+
<AtomicText
|
|
48
|
+
type="bodyMedium"
|
|
49
|
+
style={{
|
|
50
|
+
color: tokens.colors.textPrimary,
|
|
51
|
+
marginLeft: 12,
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
Edit Text
|
|
55
|
+
</AtomicText>
|
|
56
|
+
</TouchableOpacity>
|
|
57
|
+
)}
|
|
58
|
+
{layer.type === "image" && (
|
|
59
|
+
<TouchableOpacity style={styles.actionMenuItem} onPress={onEditImage}>
|
|
60
|
+
<AtomicIcon name="Edit" size="md" color="primary" />
|
|
61
|
+
<AtomicText
|
|
62
|
+
type="bodyMedium"
|
|
63
|
+
style={{
|
|
64
|
+
color: tokens.colors.textPrimary,
|
|
65
|
+
marginLeft: 12,
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
Edit Image
|
|
69
|
+
</AtomicText>
|
|
70
|
+
</TouchableOpacity>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
<TouchableOpacity style={styles.actionMenuItem} onPress={onAnimate}>
|
|
74
|
+
<AtomicIcon name="sparkles-outline" size="md" color="primary" />
|
|
75
|
+
<AtomicText
|
|
76
|
+
type="bodyMedium"
|
|
77
|
+
style={{
|
|
78
|
+
color: tokens.colors.textPrimary,
|
|
79
|
+
marginLeft: 12,
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
{layer.animation ? "Edit Animation" : "Add Animation"}
|
|
83
|
+
</AtomicText>
|
|
84
|
+
{layer.animation && (
|
|
85
|
+
<View
|
|
86
|
+
style={[
|
|
87
|
+
styles.animationBadge,
|
|
88
|
+
{ backgroundColor: tokens.colors.success },
|
|
89
|
+
]}
|
|
90
|
+
/>
|
|
91
|
+
)}
|
|
92
|
+
</TouchableOpacity>
|
|
93
|
+
|
|
94
|
+
<TouchableOpacity style={styles.actionMenuItem} onPress={onDuplicate}>
|
|
95
|
+
<AtomicIcon name="copy" size="md" color="primary" />
|
|
96
|
+
<AtomicText
|
|
97
|
+
type="bodyMedium"
|
|
98
|
+
style={{
|
|
99
|
+
color: tokens.colors.textPrimary,
|
|
100
|
+
marginLeft: 12,
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
Duplicate Layer
|
|
104
|
+
</AtomicText>
|
|
105
|
+
</TouchableOpacity>
|
|
106
|
+
|
|
107
|
+
<View style={styles.divider} />
|
|
108
|
+
|
|
109
|
+
<TouchableOpacity style={styles.actionMenuItem} onPress={onMoveFront}>
|
|
110
|
+
<AtomicIcon name="ChevronsUp" size="md" color="secondary" />
|
|
111
|
+
<AtomicText
|
|
112
|
+
type="bodyMedium"
|
|
113
|
+
style={{
|
|
114
|
+
color: tokens.colors.textSecondary,
|
|
115
|
+
marginLeft: 12,
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
Bring to Front
|
|
119
|
+
</AtomicText>
|
|
120
|
+
</TouchableOpacity>
|
|
121
|
+
|
|
122
|
+
<TouchableOpacity style={styles.actionMenuItem} onPress={onMoveUp}>
|
|
123
|
+
<AtomicIcon name="ChevronUp" size="md" color="secondary" />
|
|
124
|
+
<AtomicText
|
|
125
|
+
type="bodyMedium"
|
|
126
|
+
style={{
|
|
127
|
+
color: tokens.colors.textSecondary,
|
|
128
|
+
marginLeft: 12,
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
Move Up
|
|
132
|
+
</AtomicText>
|
|
133
|
+
</TouchableOpacity>
|
|
134
|
+
|
|
135
|
+
<TouchableOpacity style={styles.actionMenuItem} onPress={onMoveDown}>
|
|
136
|
+
<AtomicIcon name="chevron-down" size="md" color="secondary" />
|
|
137
|
+
<AtomicText
|
|
138
|
+
type="bodyMedium"
|
|
139
|
+
style={{
|
|
140
|
+
color: tokens.colors.textSecondary,
|
|
141
|
+
marginLeft: 12,
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
Move Down
|
|
145
|
+
</AtomicText>
|
|
146
|
+
</TouchableOpacity>
|
|
147
|
+
|
|
148
|
+
<TouchableOpacity style={styles.actionMenuItem} onPress={onMoveBack}>
|
|
149
|
+
<AtomicIcon name="ChevronsDown" size="md" color="secondary" />
|
|
150
|
+
<AtomicText
|
|
151
|
+
type="bodyMedium"
|
|
152
|
+
style={{
|
|
153
|
+
color: tokens.colors.textSecondary,
|
|
154
|
+
marginLeft: 12,
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
Send to Back
|
|
158
|
+
</AtomicText>
|
|
159
|
+
</TouchableOpacity>
|
|
160
|
+
|
|
161
|
+
<View style={styles.divider} />
|
|
162
|
+
|
|
163
|
+
<TouchableOpacity style={styles.actionMenuItem} onPress={onDelete}>
|
|
164
|
+
<AtomicIcon name="trash-outline" size="md" color="error" />
|
|
165
|
+
<AtomicText
|
|
166
|
+
type="bodyMedium"
|
|
167
|
+
style={{
|
|
168
|
+
color: tokens.colors.error,
|
|
169
|
+
marginLeft: 12,
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
Delete Layer
|
|
173
|
+
</AtomicText>
|
|
174
|
+
</TouchableOpacity>
|
|
175
|
+
</View>
|
|
176
|
+
);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const styles = StyleSheet.create({
|
|
180
|
+
actionMenuItem: {
|
|
181
|
+
flexDirection: "row",
|
|
182
|
+
alignItems: "center",
|
|
183
|
+
paddingVertical: 16,
|
|
184
|
+
paddingHorizontal: 16,
|
|
185
|
+
},
|
|
186
|
+
divider: {
|
|
187
|
+
height: 1,
|
|
188
|
+
backgroundColor: "#E5E7EB",
|
|
189
|
+
marginVertical: 8,
|
|
190
|
+
},
|
|
191
|
+
animationBadge: {
|
|
192
|
+
width: 8,
|
|
193
|
+
height: 8,
|
|
194
|
+
borderRadius: 4,
|
|
195
|
+
marginLeft: 8,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scene Actions Menu Component
|
|
3
|
+
* Single Responsibility: Display scene action menu
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
AtomicIcon,
|
|
11
|
+
useAppDesignTokens,
|
|
12
|
+
} from "@umituz/react-native-design-system";
|
|
13
|
+
|
|
14
|
+
export interface SceneActionsMenuProps {
|
|
15
|
+
sceneIndex: number;
|
|
16
|
+
canDelete: boolean;
|
|
17
|
+
onDuplicate: () => void;
|
|
18
|
+
onDelete: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const SceneActionsMenu: React.FC<SceneActionsMenuProps> = ({
|
|
22
|
+
sceneIndex,
|
|
23
|
+
canDelete,
|
|
24
|
+
onDuplicate,
|
|
25
|
+
onDelete,
|
|
26
|
+
}) => {
|
|
27
|
+
const tokens = useAppDesignTokens();
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<View style={{ paddingVertical: 8 }}>
|
|
31
|
+
<TouchableOpacity style={styles.actionMenuItem} onPress={onDuplicate}>
|
|
32
|
+
<AtomicIcon name="copy" size="md" color="primary" />
|
|
33
|
+
<AtomicText
|
|
34
|
+
type="bodyMedium"
|
|
35
|
+
style={{
|
|
36
|
+
color: tokens.colors.textPrimary,
|
|
37
|
+
marginLeft: 12,
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
Duplicate Scene
|
|
41
|
+
</AtomicText>
|
|
42
|
+
</TouchableOpacity>
|
|
43
|
+
|
|
44
|
+
{canDelete && (
|
|
45
|
+
<TouchableOpacity style={styles.actionMenuItem} onPress={onDelete}>
|
|
46
|
+
<AtomicIcon name="trash-outline" size="md" color="error" />
|
|
47
|
+
<AtomicText
|
|
48
|
+
type="bodyMedium"
|
|
49
|
+
style={{
|
|
50
|
+
color: tokens.colors.error,
|
|
51
|
+
marginLeft: 12,
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
Delete Scene
|
|
55
|
+
</AtomicText>
|
|
56
|
+
</TouchableOpacity>
|
|
57
|
+
)}
|
|
58
|
+
</View>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const styles = StyleSheet.create({
|
|
63
|
+
actionMenuItem: {
|
|
64
|
+
flexDirection: "row",
|
|
65
|
+
alignItems: "center",
|
|
66
|
+
paddingVertical: 16,
|
|
67
|
+
paddingHorizontal: 16,
|
|
68
|
+
},
|
|
69
|
+
});
|