@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.
Files changed (97) hide show
  1. package/README.md +92 -0
  2. package/package.json +48 -0
  3. package/src/domain/entities/index.ts +50 -0
  4. package/src/domain/entities/video-project.types.ts +153 -0
  5. package/src/index.ts +100 -0
  6. package/src/infrastructure/constants/animation-layer.constants.ts +32 -0
  7. package/src/infrastructure/constants/audio-layer.constants.ts +14 -0
  8. package/src/infrastructure/constants/export.constants.ts +28 -0
  9. package/src/infrastructure/constants/image-layer.constants.ts +12 -0
  10. package/src/infrastructure/constants/index.ts +11 -0
  11. package/src/infrastructure/constants/shape-layer.constants.ts +29 -0
  12. package/src/infrastructure/constants/text-layer.constants.ts +40 -0
  13. package/src/infrastructure/services/export-orchestrator.service.ts +122 -0
  14. package/src/infrastructure/services/image-layer-operations.service.ts +108 -0
  15. package/src/infrastructure/services/layer-manipulation.service.ts +93 -0
  16. package/src/infrastructure/services/layer-operations/index.ts +9 -0
  17. package/src/infrastructure/services/layer-operations/layer-delete.service.ts +47 -0
  18. package/src/infrastructure/services/layer-operations/layer-duplicate.service.ts +66 -0
  19. package/src/infrastructure/services/layer-operations/layer-order.service.ts +82 -0
  20. package/src/infrastructure/services/layer-operations/layer-transform.service.ts +160 -0
  21. package/src/infrastructure/services/layer-operations.service.ts +198 -0
  22. package/src/infrastructure/services/scene-operations.service.ts +166 -0
  23. package/src/infrastructure/services/shape-layer-operations.service.ts +65 -0
  24. package/src/infrastructure/services/text-layer-operations.service.ts +114 -0
  25. package/src/presentation/components/AnimationEditor.tsx +103 -0
  26. package/src/presentation/components/AudioEditor.tsx +144 -0
  27. package/src/presentation/components/DraggableLayer.tsx +110 -0
  28. package/src/presentation/components/EditorHeader.tsx +107 -0
  29. package/src/presentation/components/EditorPreviewArea.tsx +221 -0
  30. package/src/presentation/components/EditorTimeline.tsx +136 -0
  31. package/src/presentation/components/EditorToolPanel.tsx +180 -0
  32. package/src/presentation/components/ExportDialog.tsx +135 -0
  33. package/src/presentation/components/ImageLayerEditor.tsx +95 -0
  34. package/src/presentation/components/LayerActionsMenu.tsx +197 -0
  35. package/src/presentation/components/SceneActionsMenu.tsx +69 -0
  36. package/src/presentation/components/ShapeLayerEditor.tsx +108 -0
  37. package/src/presentation/components/TextLayerEditor.tsx +104 -0
  38. package/src/presentation/components/animation-layer/AnimationEditorActions.tsx +104 -0
  39. package/src/presentation/components/animation-layer/AnimationInfoBanner.tsx +43 -0
  40. package/src/presentation/components/animation-layer/AnimationTypeSelector.tsx +105 -0
  41. package/src/presentation/components/animation-layer/index.ts +8 -0
  42. package/src/presentation/components/audio-layer/AudioEditorActions.tsx +115 -0
  43. package/src/presentation/components/audio-layer/AudioFileSelector.tsx +126 -0
  44. package/src/presentation/components/audio-layer/FadeEffectsSelector.tsx +151 -0
  45. package/src/presentation/components/audio-layer/InfoBanner.tsx +43 -0
  46. package/src/presentation/components/audio-layer/VolumeSelector.tsx +98 -0
  47. package/src/presentation/components/audio-layer/index.ts +10 -0
  48. package/src/presentation/components/draggable-layer/LayerContent.tsx +106 -0
  49. package/src/presentation/components/draggable-layer/ResizeHandles.tsx +97 -0
  50. package/src/presentation/components/draggable-layer/index.ts +7 -0
  51. package/src/presentation/components/export/ExportActions.tsx +101 -0
  52. package/src/presentation/components/export/ExportInfoBanner.tsx +44 -0
  53. package/src/presentation/components/export/ExportProgress.tsx +114 -0
  54. package/src/presentation/components/export/OptionSelectorRow.tsx +101 -0
  55. package/src/presentation/components/export/ProjectInfoBox.tsx +61 -0
  56. package/src/presentation/components/export/WatermarkToggle.tsx +87 -0
  57. package/src/presentation/components/export/index.ts +11 -0
  58. package/src/presentation/components/image-layer/ImagePreview.tsx +70 -0
  59. package/src/presentation/components/image-layer/ImageSelectionButtons.tsx +82 -0
  60. package/src/presentation/components/image-layer/OpacitySelector.tsx +91 -0
  61. package/src/presentation/components/image-layer/index.ts +8 -0
  62. package/src/presentation/components/index.ts +17 -0
  63. package/src/presentation/components/shape-layer/ColorPickerHorizontal.tsx +92 -0
  64. package/src/presentation/components/shape-layer/ShapePreview.tsx +57 -0
  65. package/src/presentation/components/shape-layer/ShapeTypeSelector.tsx +102 -0
  66. package/src/presentation/components/shape-layer/ValueSelector.tsx +106 -0
  67. package/src/presentation/components/shape-layer/index.ts +9 -0
  68. package/src/presentation/components/text-layer/ColorPicker.tsx +91 -0
  69. package/src/presentation/components/text-layer/EditorActions.tsx +95 -0
  70. package/src/presentation/components/text-layer/FontSizeSelector.tsx +86 -0
  71. package/src/presentation/components/text-layer/OptionSelector.tsx +98 -0
  72. package/src/presentation/components/text-layer/TextAlignSelector.tsx +87 -0
  73. package/src/presentation/components/text-layer/TextInputSection.tsx +70 -0
  74. package/src/presentation/components/text-layer/TextPreview.tsx +71 -0
  75. package/src/presentation/components/text-layer/index.ts +12 -0
  76. package/src/presentation/hooks/useAnimationLayerForm.ts +72 -0
  77. package/src/presentation/hooks/useAudioLayerForm.ts +76 -0
  78. package/src/presentation/hooks/useDraggableLayerGestures.ts +166 -0
  79. package/src/presentation/hooks/useEditorActions.tsx +93 -0
  80. package/src/presentation/hooks/useEditorBottomSheet.ts +43 -0
  81. package/src/presentation/hooks/useEditorHistory.ts +80 -0
  82. package/src/presentation/hooks/useEditorLayers.ts +97 -0
  83. package/src/presentation/hooks/useEditorPlayback.ts +90 -0
  84. package/src/presentation/hooks/useEditorScenes.ts +106 -0
  85. package/src/presentation/hooks/useExport.ts +67 -0
  86. package/src/presentation/hooks/useExportActions.tsx +51 -0
  87. package/src/presentation/hooks/useExportForm.ts +96 -0
  88. package/src/presentation/hooks/useImageLayerForm.ts +57 -0
  89. package/src/presentation/hooks/useImageLayerOperations.ts +71 -0
  90. package/src/presentation/hooks/useLayerActions.tsx +162 -0
  91. package/src/presentation/hooks/useLayerManipulation.ts +178 -0
  92. package/src/presentation/hooks/useMenuActions.tsx +92 -0
  93. package/src/presentation/hooks/useSceneActions.tsx +81 -0
  94. package/src/presentation/hooks/useShapeLayerForm.ts +84 -0
  95. package/src/presentation/hooks/useShapeLayerOperations.ts +52 -0
  96. package/src/presentation/hooks/useTextLayerForm.ts +100 -0
  97. package/src/presentation/hooks/useTextLayerOperations.ts +74 -0
@@ -0,0 +1,144 @@
1
+ /**
2
+ * AudioEditor Component
3
+ * Main component for editing audio layers
4
+ */
5
+
6
+ import React, { useCallback } from "react";
7
+ import { View, ScrollView, StyleSheet, Alert } from "react-native";
8
+ import * as DocumentPicker from "expo-document-picker";
9
+ import {
10
+ AtomicText,
11
+ useAppDesignTokens,
12
+ } from "@umituz/react-native-design-system";
13
+ import type { Audio } from "@domains/video";
14
+ import { useAudioLayerForm } from "../../hooks/useAudioLayerForm";
15
+ import { AUDIO_FILE_TYPES } from "../../constants/audio-layer.constants";
16
+ import {
17
+ AudioFileSelector,
18
+ VolumeSelector,
19
+ FadeEffectsSelector,
20
+ InfoBanner,
21
+ AudioEditorActions,
22
+ } from "./audio-layer";
23
+
24
+ interface AudioEditorProps {
25
+ audio?: Audio;
26
+ onSave: (audioData: Audio) => void;
27
+ onRemove?: () => void;
28
+ onCancel: () => void;
29
+ }
30
+
31
+ export const AudioEditor: React.FC<AudioEditorProps> = ({
32
+ audio,
33
+ onSave,
34
+ onRemove,
35
+ onCancel,
36
+ }) => {
37
+ const tokens = useAppDesignTokens();
38
+ const {
39
+ formState,
40
+ setAudioUri,
41
+ setVolume,
42
+ setFadeIn,
43
+ setFadeOut,
44
+ buildAudioData,
45
+ isValid,
46
+ } = useAudioLayerForm(audio);
47
+
48
+ const handlePickAudio = useCallback(async () => {
49
+ try {
50
+ const result = await DocumentPicker.getDocumentAsync({
51
+ type: AUDIO_FILE_TYPES[0],
52
+ copyToCacheDirectory: true,
53
+ });
54
+
55
+ if (!result.canceled && result.assets && result.assets.length > 0) {
56
+ setAudioUri(result.assets[0].uri);
57
+ }
58
+ } catch (error) {
59
+ Alert.alert("Error", "Failed to pick audio file");
60
+ }
61
+ }, [setAudioUri]);
62
+
63
+ const handleSave = useCallback(() => {
64
+ if (!isValid) {
65
+ Alert.alert("Error", "Please select an audio file");
66
+ return;
67
+ }
68
+ onSave(buildAudioData());
69
+ }, [isValid, buildAudioData, onSave]);
70
+
71
+ const handleRemoveAudio = useCallback(() => {
72
+ Alert.alert(
73
+ "Remove Audio",
74
+ "Are you sure you want to remove the audio from this scene?",
75
+ [
76
+ { text: "Cancel", style: "cancel" },
77
+ {
78
+ text: "Remove",
79
+ style: "destructive",
80
+ onPress: () => {
81
+ onRemove?.();
82
+ },
83
+ },
84
+ ],
85
+ );
86
+ }, [onRemove]);
87
+
88
+ const getFileName = useCallback((uri: string) => {
89
+ const parts = uri.split("/");
90
+ return parts[parts.length - 1] || "Unknown";
91
+ }, []);
92
+
93
+ return (
94
+ <View style={styles.container}>
95
+ <ScrollView showsVerticalScrollIndicator={false}>
96
+ <View style={styles.section}>
97
+ <AtomicText
98
+ type="bodyMedium"
99
+ style={{
100
+ color: tokens.colors.textPrimary,
101
+ fontWeight: "600",
102
+ marginBottom: 8,
103
+ }}
104
+ >
105
+ Audio File
106
+ </AtomicText>
107
+ <AudioFileSelector
108
+ audioUri={formState.audioUri}
109
+ onPickAudio={handlePickAudio}
110
+ getFileName={getFileName}
111
+ />
112
+ </View>
113
+
114
+ <VolumeSelector volume={formState.volume} onVolumeChange={setVolume} />
115
+
116
+ <FadeEffectsSelector
117
+ fadeIn={formState.fadeIn}
118
+ fadeOut={formState.fadeOut}
119
+ onFadeInChange={setFadeIn}
120
+ onFadeOutChange={setFadeOut}
121
+ />
122
+
123
+ <InfoBanner />
124
+ </ScrollView>
125
+
126
+ <AudioEditorActions
127
+ hasAudio={!!audio}
128
+ onRemove={handleRemoveAudio}
129
+ onCancel={onCancel}
130
+ onSave={handleSave}
131
+ isValid={isValid}
132
+ />
133
+ </View>
134
+ );
135
+ };
136
+
137
+ const styles = StyleSheet.create({
138
+ container: {
139
+ paddingVertical: 16,
140
+ },
141
+ section: {
142
+ marginBottom: 24,
143
+ },
144
+ });
@@ -0,0 +1,110 @@
1
+ /**
2
+ * DraggableLayer Component
3
+ * Draggable and resizable layer component
4
+ */
5
+
6
+ import React from "react";
7
+ import { StyleSheet } from "react-native";
8
+ import { GestureDetector } from "react-native-gesture-handler";
9
+ import Animated, { useAnimatedStyle } from "react-native-reanimated";
10
+ import { useAppDesignTokens } from "@umituz/react-native-design-system";
11
+ import type { Layer } from "@domains/video";
12
+ import { useDraggableLayerGestures } from "../../hooks/useDraggableLayerGestures";
13
+ import { LayerContent } from "./draggable-layer/LayerContent";
14
+ import { ResizeHandles } from "./draggable-layer/ResizeHandles";
15
+
16
+ interface DraggableLayerProps {
17
+ layer: Layer;
18
+ canvasWidth: number;
19
+ canvasHeight: number;
20
+ isSelected: boolean;
21
+ onSelect: () => void;
22
+ onPositionChange: (x: number, y: number) => void;
23
+ onSizeChange: (width: number, height: number) => void;
24
+ }
25
+
26
+ export const DraggableLayer: React.FC<DraggableLayerProps> = ({
27
+ layer,
28
+ canvasWidth,
29
+ canvasHeight,
30
+ isSelected,
31
+ onSelect,
32
+ onPositionChange,
33
+ onSizeChange,
34
+ }) => {
35
+ const tokens = useAppDesignTokens();
36
+
37
+ const initialX = (layer.position.x / 100) * canvasWidth;
38
+ const initialY = (layer.position.y / 100) * canvasHeight;
39
+ const initialWidth = (layer.size.width / 100) * canvasWidth;
40
+ const initialHeight = (layer.size.height / 100) * canvasHeight;
41
+
42
+ const {
43
+ translateX,
44
+ translateY,
45
+ width,
46
+ height,
47
+ composedGesture,
48
+ topLeftResizeHandler,
49
+ topRightResizeHandler,
50
+ bottomLeftResizeHandler,
51
+ bottomRightResizeHandler,
52
+ } = useDraggableLayerGestures({
53
+ initialX,
54
+ initialY,
55
+ initialWidth,
56
+ initialHeight,
57
+ canvasWidth,
58
+ canvasHeight,
59
+ onSelect,
60
+ onPositionChange,
61
+ onSizeChange,
62
+ });
63
+
64
+ const animatedStyle = useAnimatedStyle(() => ({
65
+ transform: [
66
+ { translateX: translateX.value },
67
+ { translateY: translateY.value },
68
+ { rotate: `${layer.rotation}deg` },
69
+ ],
70
+ opacity: layer.opacity,
71
+ width: width.value,
72
+ height: height.value,
73
+ }));
74
+
75
+ return (
76
+ <GestureDetector gesture={composedGesture}>
77
+ <Animated.View
78
+ style={[
79
+ styles.layer,
80
+ {
81
+ borderColor: isSelected ? tokens.colors.primary : "transparent",
82
+ borderWidth: isSelected ? 2 : 0,
83
+ },
84
+ animatedStyle,
85
+ ]}
86
+ >
87
+ <LayerContent layer={layer} />
88
+
89
+ {isSelected && (
90
+ <ResizeHandles
91
+ topLeftGesture={topLeftResizeHandler}
92
+ topRightGesture={topRightResizeHandler}
93
+ bottomLeftGesture={bottomLeftResizeHandler}
94
+ bottomRightGesture={bottomRightResizeHandler}
95
+ />
96
+ )}
97
+ </Animated.View>
98
+ </GestureDetector>
99
+ );
100
+ };
101
+
102
+ const styles = StyleSheet.create({
103
+ layer: {
104
+ position: "absolute",
105
+ justifyContent: "center",
106
+ alignItems: "center",
107
+ borderStyle: "dashed",
108
+ overflow: "hidden",
109
+ },
110
+ });
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Editor Header Component
3
+ * Single Responsibility: Display editor header with actions
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 { AppNavigation } from "@umituz/react-native-design-system";
14
+
15
+ export interface EditorHeaderProps {
16
+ projectTitle: string;
17
+ canUndo: boolean;
18
+ canRedo: boolean;
19
+ onUndo: () => void;
20
+ onRedo: () => void;
21
+ onSave: () => void;
22
+ onExport: () => void;
23
+ onBack: () => void;
24
+ }
25
+
26
+ export const EditorHeader: React.FC<EditorHeaderProps> = ({
27
+ projectTitle,
28
+ canUndo,
29
+ canRedo,
30
+ onUndo,
31
+ onRedo,
32
+ onSave,
33
+ onExport,
34
+ onBack,
35
+ }) => {
36
+ const tokens = useAppDesignTokens();
37
+
38
+ return (
39
+ <View
40
+ style={[
41
+ styles.header,
42
+ {
43
+ backgroundColor: tokens.colors.surface,
44
+ borderBottomColor: tokens.colors.borderLight,
45
+ },
46
+ ]}
47
+ >
48
+ <TouchableOpacity onPress={onBack} style={styles.backButton}>
49
+ <AtomicIcon name="chevron-back" size="md" color="primary" />
50
+ </TouchableOpacity>
51
+
52
+ <AtomicText
53
+ type="bodyLarge"
54
+ style={{ color: tokens.colors.textPrimary, fontWeight: "600" }}
55
+ >
56
+ {projectTitle}
57
+ </AtomicText>
58
+
59
+ <View style={styles.headerActions}>
60
+ <TouchableOpacity
61
+ onPress={onUndo}
62
+ style={styles.headerButton}
63
+ disabled={!canUndo}
64
+ >
65
+ <AtomicIcon name="arrow-undo" size="md" color="secondary" />
66
+ </TouchableOpacity>
67
+
68
+ <TouchableOpacity
69
+ onPress={onRedo}
70
+ style={styles.headerButton}
71
+ disabled={!canRedo}
72
+ >
73
+ <AtomicIcon name="arrow-redo" size="md" color="secondary" />
74
+ </TouchableOpacity>
75
+
76
+ <TouchableOpacity onPress={onSave} style={styles.headerButton}>
77
+ <AtomicIcon name="save" size="md" color="secondary" />
78
+ </TouchableOpacity>
79
+
80
+ <TouchableOpacity onPress={onExport} style={styles.headerButton}>
81
+ <AtomicIcon name="download" size="md" color="primary" />
82
+ </TouchableOpacity>
83
+ </View>
84
+ </View>
85
+ );
86
+ };
87
+
88
+ const styles = StyleSheet.create({
89
+ header: {
90
+ flexDirection: "row",
91
+ alignItems: "center",
92
+ justifyContent: "space-between",
93
+ paddingHorizontal: 16,
94
+ paddingVertical: 12,
95
+ borderBottomWidth: 1,
96
+ },
97
+ backButton: {
98
+ padding: 8,
99
+ },
100
+ headerActions: {
101
+ flexDirection: "row",
102
+ gap: 12,
103
+ },
104
+ headerButton: {
105
+ padding: 8,
106
+ },
107
+ });
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Editor Preview Area Component
3
+ * Single Responsibility: Display video preview canvas and playback controls
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, TouchableOpacity, StyleSheet, Dimensions } from "react-native";
8
+ import {
9
+ AtomicText,
10
+ AtomicIcon,
11
+ useAppDesignTokens,
12
+ } from "@umituz/react-native-design-system";
13
+ import { DraggableLayer } from "./DraggableLayer";
14
+ import type { Scene, Layer, ImageLayer } from "@domains/video";
15
+
16
+ const { width } = Dimensions.get("window");
17
+ const PREVIEW_ASPECT_RATIO = 16 / 9;
18
+ const PREVIEW_HEIGHT = width / PREVIEW_ASPECT_RATIO;
19
+
20
+ export interface EditorPreviewAreaProps {
21
+ scene: Scene;
22
+ selectedLayerId: string | null;
23
+ isPlaying: boolean;
24
+ currentTime: number;
25
+ onLayerSelect: (layerId: string) => void;
26
+ onLayerPositionChange: (layerId: string, x: number, y: number) => void;
27
+ onLayerSizeChange: (layerId: string, width: number, height: number) => void;
28
+ onLayerActionsPress: (layer: Layer) => void;
29
+ onPlayPause: () => void;
30
+ onReset: () => void;
31
+ }
32
+
33
+ export const EditorPreviewArea: React.FC<EditorPreviewAreaProps> = ({
34
+ scene,
35
+ selectedLayerId,
36
+ isPlaying,
37
+ currentTime,
38
+ onLayerSelect,
39
+ onLayerPositionChange,
40
+ onLayerSizeChange,
41
+ onLayerActionsPress,
42
+ onPlayPause,
43
+ onReset,
44
+ }) => {
45
+ const tokens = useAppDesignTokens();
46
+
47
+ return (
48
+ <View style={styles.previewSection}>
49
+ <View
50
+ style={[
51
+ styles.previewCanvas,
52
+ {
53
+ backgroundColor:
54
+ scene.background.type === "color"
55
+ ? scene.background.value
56
+ : tokens.colors.surfaceSecondary,
57
+ height: PREVIEW_HEIGHT,
58
+ },
59
+ ]}
60
+ >
61
+ {scene.layers.length === 0 ? (
62
+ <View style={styles.emptyPreview}>
63
+ <AtomicIcon name="film-outline" size="xl" color="secondary" />
64
+ <AtomicText
65
+ type="bodyMedium"
66
+ style={{ color: tokens.colors.textSecondary, marginTop: 12 }}
67
+ >
68
+ Canvas is empty. Add layers to get started.
69
+ </AtomicText>
70
+ </View>
71
+ ) : (
72
+ <>
73
+ {scene.layers.map((layer) => (
74
+ <DraggableLayer
75
+ key={layer.id}
76
+ layer={layer}
77
+ canvasWidth={width}
78
+ canvasHeight={PREVIEW_HEIGHT}
79
+ isSelected={selectedLayerId === layer.id}
80
+ onSelect={() => onLayerSelect(layer.id)}
81
+ onPositionChange={(x, y) =>
82
+ onLayerPositionChange(layer.id, x, y)
83
+ }
84
+ onSizeChange={(w, h) => onLayerSizeChange(layer.id, w, h)}
85
+ />
86
+ ))}
87
+
88
+ {selectedLayerId && (
89
+ <TouchableOpacity
90
+ style={[
91
+ styles.layerActionsButton,
92
+ { backgroundColor: tokens.colors.primary },
93
+ ]}
94
+ onPress={() => {
95
+ const layer = scene.layers.find(
96
+ (l) => l.id === selectedLayerId,
97
+ );
98
+ if (layer) {
99
+ onLayerActionsPress(layer);
100
+ }
101
+ }}
102
+ >
103
+ <AtomicIcon
104
+ name="ellipsis-vertical"
105
+ size="md"
106
+ color="onSurface"
107
+ />
108
+ </TouchableOpacity>
109
+ )}
110
+ </>
111
+ )}
112
+ </View>
113
+
114
+ <View
115
+ style={[
116
+ styles.playbackControls,
117
+ { backgroundColor: tokens.colors.surface },
118
+ ]}
119
+ >
120
+ <View style={styles.playbackRow}>
121
+ <TouchableOpacity onPress={onPlayPause} style={styles.playButton}>
122
+ <AtomicIcon
123
+ name={isPlaying ? "pause" : "play"}
124
+ size="lg"
125
+ color="primary"
126
+ />
127
+ </TouchableOpacity>
128
+
129
+ <View style={styles.timeDisplay}>
130
+ <AtomicText
131
+ type="labelSmall"
132
+ style={{ color: tokens.colors.textSecondary }}
133
+ >
134
+ {Math.floor(currentTime / 1000)}s /{" "}
135
+ {Math.floor(scene.duration / 1000)}s
136
+ </AtomicText>
137
+ </View>
138
+
139
+ <TouchableOpacity onPress={onReset} style={styles.resetButton}>
140
+ <AtomicIcon name="refresh" size="md" color="secondary" />
141
+ </TouchableOpacity>
142
+ </View>
143
+
144
+ <View
145
+ style={[
146
+ styles.progressBarContainer,
147
+ { backgroundColor: tokens.colors.borderLight },
148
+ ]}
149
+ >
150
+ <View
151
+ style={[
152
+ styles.progressBar,
153
+ {
154
+ width: `${(currentTime / scene.duration) * 100}%`,
155
+ backgroundColor: tokens.colors.primary,
156
+ },
157
+ ]}
158
+ />
159
+ </View>
160
+ </View>
161
+ </View>
162
+ );
163
+ };
164
+
165
+ const styles = StyleSheet.create({
166
+ previewSection: {
167
+ padding: 16,
168
+ },
169
+ previewCanvas: {
170
+ width: "100%",
171
+ borderRadius: 12,
172
+ overflow: "hidden",
173
+ position: "relative",
174
+ },
175
+ emptyPreview: {
176
+ flex: 1,
177
+ alignItems: "center",
178
+ justifyContent: "center",
179
+ paddingVertical: 60,
180
+ },
181
+ playbackControls: {
182
+ marginTop: 16,
183
+ padding: 16,
184
+ borderRadius: 12,
185
+ },
186
+ playbackRow: {
187
+ flexDirection: "row",
188
+ alignItems: "center",
189
+ justifyContent: "space-between",
190
+ },
191
+ progressBarContainer: {
192
+ height: 4,
193
+ borderRadius: 2,
194
+ marginTop: 12,
195
+ overflow: "hidden",
196
+ },
197
+ progressBar: {
198
+ height: "100%",
199
+ borderRadius: 2,
200
+ },
201
+ playButton: {
202
+ padding: 8,
203
+ },
204
+ timeDisplay: {
205
+ flex: 1,
206
+ alignItems: "center",
207
+ },
208
+ resetButton: {
209
+ padding: 8,
210
+ },
211
+ layerActionsButton: {
212
+ position: "absolute",
213
+ top: 16,
214
+ right: 16,
215
+ width: 44,
216
+ height: 44,
217
+ borderRadius: 22,
218
+ alignItems: "center",
219
+ justifyContent: "center",
220
+ },
221
+ });
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Editor Timeline Component
3
+ * Single Responsibility: Display scene timeline
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
8
+ import {
9
+ AtomicText,
10
+ AtomicIcon,
11
+ useAppDesignTokens,
12
+ } from "@umituz/react-native-design-system";
13
+ import { useLocalization } from "@umituz/react-native-localization";
14
+ import type { VideoProject, Scene } from "@domains/video";
15
+
16
+ export interface EditorTimelineProps {
17
+ project: VideoProject;
18
+ currentSceneIndex: number;
19
+ onSceneSelect: (index: number) => void;
20
+ onSceneLongPress: (index: number) => void;
21
+ onAddScene: () => void;
22
+ }
23
+
24
+ export const EditorTimeline: React.FC<EditorTimelineProps> = ({
25
+ project,
26
+ currentSceneIndex,
27
+ onSceneSelect,
28
+ onSceneLongPress,
29
+ onAddScene,
30
+ }) => {
31
+ const { t } = useLocalization();
32
+ const tokens = useAppDesignTokens();
33
+
34
+ return (
35
+ <View style={[styles.timeline, { backgroundColor: tokens.colors.surface }]}>
36
+ <View style={styles.timelineHeader}>
37
+ <AtomicText
38
+ type="bodyMedium"
39
+ style={{ color: tokens.colors.textPrimary, fontWeight: "600" }}
40
+ >
41
+ {t("editor.timeline.title")}
42
+ </AtomicText>
43
+ <TouchableOpacity onPress={onAddScene}>
44
+ <AtomicIcon name="add" size="md" color="primary" />
45
+ </TouchableOpacity>
46
+ </View>
47
+
48
+ <ScrollView
49
+ horizontal
50
+ showsHorizontalScrollIndicator={false}
51
+ style={styles.scenesScroll}
52
+ >
53
+ {project.scenes.map((scene: Scene, index: number) => (
54
+ <TouchableOpacity
55
+ key={scene.id}
56
+ style={[
57
+ styles.sceneCard,
58
+ {
59
+ backgroundColor:
60
+ currentSceneIndex === index
61
+ ? tokens.colors.primary + "20"
62
+ : tokens.colors.backgroundPrimary,
63
+ borderColor:
64
+ currentSceneIndex === index
65
+ ? tokens.colors.primary
66
+ : tokens.colors.borderLight,
67
+ },
68
+ ]}
69
+ onPress={() => onSceneSelect(index)}
70
+ onLongPress={() => onSceneLongPress(index)}
71
+ >
72
+ <View
73
+ style={[
74
+ styles.sceneThumbnail,
75
+ {
76
+ backgroundColor:
77
+ scene.background.value || tokens.colors.surfaceSecondary,
78
+ },
79
+ ]}
80
+ >
81
+ <AtomicText
82
+ type="labelSmall"
83
+ style={{ color: "#FFFFFF", fontWeight: "600" }}
84
+ >
85
+ {index + 1}
86
+ </AtomicText>
87
+ </View>
88
+ <AtomicText
89
+ type="labelSmall"
90
+ style={{
91
+ color: tokens.colors.textPrimary,
92
+ marginTop: 4,
93
+ fontWeight: currentSceneIndex === index ? "600" : "400",
94
+ }}
95
+ >
96
+ {scene.duration / 1000}s
97
+ </AtomicText>
98
+ </TouchableOpacity>
99
+ ))}
100
+ </ScrollView>
101
+ </View>
102
+ );
103
+ };
104
+
105
+ const styles = StyleSheet.create({
106
+ timeline: {
107
+ padding: 16,
108
+ marginHorizontal: 16,
109
+ marginBottom: 24,
110
+ borderRadius: 12,
111
+ },
112
+ timelineHeader: {
113
+ flexDirection: "row",
114
+ alignItems: "center",
115
+ justifyContent: "space-between",
116
+ marginBottom: 12,
117
+ },
118
+ scenesScroll: {
119
+ marginHorizontal: -16,
120
+ paddingHorizontal: 16,
121
+ },
122
+ sceneCard: {
123
+ padding: 8,
124
+ borderRadius: 8,
125
+ borderWidth: 2,
126
+ marginRight: 12,
127
+ alignItems: "center",
128
+ },
129
+ sceneThumbnail: {
130
+ width: 60,
131
+ height: 60,
132
+ borderRadius: 6,
133
+ alignItems: "center",
134
+ justifyContent: "center",
135
+ },
136
+ });