@umituz/react-native-video-editor 1.1.53 → 1.1.55
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/infrastructure/services/layer-operations/layer-duplicate.service.ts +4 -2
- package/src/infrastructure/services/scene-operations.service.ts +5 -8
- package/src/infrastructure/utils/data-clone.utils.ts +141 -0
- package/src/infrastructure/utils/position-calculations.utils.ts +182 -0
- package/src/infrastructure/utils/srt.utils.ts +4 -20
- package/src/infrastructure/utils/time-calculations.utils.ts +107 -0
- package/src/infrastructure/utils/video-calculations.utils.ts +169 -0
- package/src/presentation/components/DraggableLayer.tsx +36 -16
- package/src/presentation/components/EditorPreviewArea.tsx +92 -57
- package/src/presentation/components/EditorTimeline.tsx +105 -47
- package/src/presentation/components/EditorToolPanel.tsx +141 -115
- package/src/presentation/hooks/useEditorHistory.ts +20 -9
- package/src/presentation/hooks/useEditorPlayback.ts +62 -25
- package/src/presentation/hooks/useExportForm.ts +10 -12
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Draggable and resizable layer component
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React from "react";
|
|
6
|
+
import React, { useMemo, useCallback } from "react";
|
|
7
7
|
import { View, StyleSheet } from "react-native";
|
|
8
8
|
import { GestureDetector } from "react-native-gesture-handler";
|
|
9
9
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
@@ -11,6 +11,7 @@ import type { Layer } from "../../domain/entities/video-project.types";
|
|
|
11
11
|
import { useDraggableLayerGestures } from "../hooks/useDraggableLayerGestures";
|
|
12
12
|
import { LayerContent } from "./draggable-layer/LayerContent";
|
|
13
13
|
import { ResizeHandles } from "./draggable-layer/ResizeHandles";
|
|
14
|
+
import { percentageToPixels } from "../../infrastructure/utils/position-calculations.utils";
|
|
14
15
|
|
|
15
16
|
interface DraggableLayerProps {
|
|
16
17
|
layer: Layer;
|
|
@@ -22,7 +23,7 @@ interface DraggableLayerProps {
|
|
|
22
23
|
onSizeChange: (width: number, height: number) => void;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
export const DraggableLayer: React.FC<DraggableLayerProps> = ({
|
|
26
|
+
export const DraggableLayer: React.FC<DraggableLayerProps> = React.memo(({
|
|
26
27
|
layer,
|
|
27
28
|
canvasWidth,
|
|
28
29
|
canvasHeight,
|
|
@@ -36,10 +37,23 @@ export const DraggableLayer: React.FC<DraggableLayerProps> = ({
|
|
|
36
37
|
const safeCanvasWidth = canvasWidth > 0 ? canvasWidth : 1;
|
|
37
38
|
const safeCanvasHeight = canvasHeight > 0 ? canvasHeight : 1;
|
|
38
39
|
|
|
39
|
-
const initialX = (layer.position.x
|
|
40
|
-
const initialY = (layer.position.y
|
|
41
|
-
const initialWidth = (layer.size.width
|
|
42
|
-
const initialHeight = (layer.size.height
|
|
40
|
+
const initialX = percentageToPixels(layer.position.x, safeCanvasWidth);
|
|
41
|
+
const initialY = percentageToPixels(layer.position.y, safeCanvasHeight);
|
|
42
|
+
const initialWidth = percentageToPixels(layer.size.width, safeCanvasWidth);
|
|
43
|
+
const initialHeight = percentageToPixels(layer.size.height, safeCanvasHeight);
|
|
44
|
+
|
|
45
|
+
// Stable callbacks to prevent gesture re-creation
|
|
46
|
+
const handlePositionChange = useCallback((x: number, y: number) => {
|
|
47
|
+
onPositionChange(x, y);
|
|
48
|
+
}, [onPositionChange]);
|
|
49
|
+
|
|
50
|
+
const handleSizeChange = useCallback((width: number, height: number) => {
|
|
51
|
+
onSizeChange(width, height);
|
|
52
|
+
}, [onSizeChange]);
|
|
53
|
+
|
|
54
|
+
const handleSelect = useCallback(() => {
|
|
55
|
+
onSelect();
|
|
56
|
+
}, [onSelect]);
|
|
43
57
|
|
|
44
58
|
const {
|
|
45
59
|
state,
|
|
@@ -55,12 +69,13 @@ export const DraggableLayer: React.FC<DraggableLayerProps> = ({
|
|
|
55
69
|
initialHeight,
|
|
56
70
|
canvasWidth: safeCanvasWidth,
|
|
57
71
|
canvasHeight: safeCanvasHeight,
|
|
58
|
-
onSelect,
|
|
59
|
-
onPositionChange,
|
|
60
|
-
onSizeChange,
|
|
72
|
+
onSelect: handleSelect,
|
|
73
|
+
onPositionChange: handlePositionChange,
|
|
74
|
+
onSizeChange: handleSizeChange,
|
|
61
75
|
});
|
|
62
76
|
|
|
63
|
-
|
|
77
|
+
// Memoize layer style to prevent new object on every render
|
|
78
|
+
const layerStyle = useMemo(() => ({
|
|
64
79
|
transform: [
|
|
65
80
|
{ translateX: state.x },
|
|
66
81
|
{ translateY: state.y },
|
|
@@ -69,17 +84,20 @@ export const DraggableLayer: React.FC<DraggableLayerProps> = ({
|
|
|
69
84
|
opacity: layer.opacity,
|
|
70
85
|
width: state.width,
|
|
71
86
|
height: state.height,
|
|
72
|
-
};
|
|
87
|
+
}), [state.x, state.y, state.width, state.height, layer.rotation, layer.opacity]);
|
|
88
|
+
|
|
89
|
+
// Memoize border style
|
|
90
|
+
const borderStyle = useMemo(() => ({
|
|
91
|
+
borderColor: isSelected ? tokens.colors.primary : "transparent",
|
|
92
|
+
borderWidth: isSelected ? 2 : 0,
|
|
93
|
+
}), [isSelected, tokens.colors.primary]);
|
|
73
94
|
|
|
74
95
|
return (
|
|
75
96
|
<GestureDetector gesture={composedGesture}>
|
|
76
97
|
<View
|
|
77
98
|
style={[
|
|
78
99
|
styles.layer,
|
|
79
|
-
|
|
80
|
-
borderColor: isSelected ? tokens.colors.primary : "transparent",
|
|
81
|
-
borderWidth: isSelected ? 2 : 0,
|
|
82
|
-
},
|
|
100
|
+
borderStyle,
|
|
83
101
|
layerStyle,
|
|
84
102
|
]}
|
|
85
103
|
>
|
|
@@ -96,7 +114,9 @@ export const DraggableLayer: React.FC<DraggableLayerProps> = ({
|
|
|
96
114
|
</View>
|
|
97
115
|
</GestureDetector>
|
|
98
116
|
);
|
|
99
|
-
};
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
DraggableLayer.displayName = 'DraggableLayer';
|
|
100
120
|
|
|
101
121
|
const styles = StyleSheet.create({
|
|
102
122
|
layer: {
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Editor Preview Area Component
|
|
3
3
|
* Single Responsibility: Display video preview canvas and playback controls
|
|
4
|
+
* Optimized to prevent unnecessary re-renders
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import React, { useMemo } from "react";
|
|
7
|
+
import React, { useMemo, useCallback } from "react";
|
|
7
8
|
import { View, TouchableOpacity, Dimensions } from "react-native";
|
|
8
9
|
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
|
|
9
10
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
10
11
|
import { DraggableLayer } from "./DraggableLayer";
|
|
11
12
|
import type { Scene, Layer } from "../../domain/entities/video-project.types";
|
|
12
13
|
import { createPreviewStyles } from "./EditorPreviewArea.styles";
|
|
14
|
+
import { formatTimeDisplay, calculateProgressPercent } from "../../infrastructure/utils/time-calculations.utils";
|
|
13
15
|
|
|
14
16
|
const { width } = Dimensions.get("window");
|
|
15
17
|
const PREVIEW_ASPECT_RATIO = 16 / 9;
|
|
@@ -28,7 +30,7 @@ interface EditorPreviewAreaProps {
|
|
|
28
30
|
onReset: () => void;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
export const EditorPreviewArea: React.FC<EditorPreviewAreaProps> = ({
|
|
33
|
+
export const EditorPreviewArea: React.FC<EditorPreviewAreaProps> = React.memo(({
|
|
32
34
|
scene,
|
|
33
35
|
selectedLayerId,
|
|
34
36
|
isPlaying,
|
|
@@ -43,26 +45,88 @@ export const EditorPreviewArea: React.FC<EditorPreviewAreaProps> = ({
|
|
|
43
45
|
const tokens = useAppDesignTokens();
|
|
44
46
|
const styles = useMemo(() => createPreviewStyles(tokens), [tokens]);
|
|
45
47
|
|
|
48
|
+
// Memoize canvas style
|
|
49
|
+
const canvasStyle = useMemo(() => [
|
|
50
|
+
styles.previewCanvas,
|
|
51
|
+
{
|
|
52
|
+
backgroundColor:
|
|
53
|
+
scene.background.type === "color"
|
|
54
|
+
? scene.background.value
|
|
55
|
+
: tokens.colors.surfaceSecondary,
|
|
56
|
+
height: PREVIEW_HEIGHT,
|
|
57
|
+
},
|
|
58
|
+
], [styles.previewCanvas, scene.background.type, scene.background.value, tokens.colors.surfaceSecondary]);
|
|
59
|
+
|
|
60
|
+
// Memoize empty preview text style
|
|
61
|
+
const emptyTextStyle = useMemo(() => ({
|
|
62
|
+
color: tokens.colors.textSecondary,
|
|
63
|
+
marginTop: 12,
|
|
64
|
+
}), [tokens.colors.textSecondary]);
|
|
65
|
+
|
|
66
|
+
// Memoize layer actions button style
|
|
67
|
+
const layerActionsButtonStyle = useMemo(() => [
|
|
68
|
+
styles.layerActionsButton,
|
|
69
|
+
{ backgroundColor: tokens.colors.primary },
|
|
70
|
+
], [styles.layerActionsButton, tokens.colors.primary]);
|
|
71
|
+
|
|
72
|
+
// Stable callbacks for layer operations
|
|
73
|
+
const handleLayerSelect = useCallback((layerId: string) => {
|
|
74
|
+
onLayerSelect(layerId);
|
|
75
|
+
}, [onLayerSelect]);
|
|
76
|
+
|
|
77
|
+
const createLayerPositionHandler = useCallback((layerId: string) => {
|
|
78
|
+
return (x: number, y: number) => {
|
|
79
|
+
onLayerPositionChange(layerId, x, y);
|
|
80
|
+
};
|
|
81
|
+
}, [onLayerPositionChange]);
|
|
82
|
+
|
|
83
|
+
const createLayerSizeHandler = useCallback((layerId: string) => {
|
|
84
|
+
return (width: number, height: number) => {
|
|
85
|
+
onLayerSizeChange(layerId, width, height);
|
|
86
|
+
};
|
|
87
|
+
}, [onLayerSizeChange]);
|
|
88
|
+
|
|
89
|
+
// Stable callback for layer actions button
|
|
90
|
+
const handleLayerActionsPress = useCallback(() => {
|
|
91
|
+
const layer = scene.layers.find((l) => l.id === selectedLayerId);
|
|
92
|
+
if (layer) {
|
|
93
|
+
onLayerActionsPress(layer);
|
|
94
|
+
}
|
|
95
|
+
}, [scene.layers, selectedLayerId, onLayerActionsPress]);
|
|
96
|
+
|
|
97
|
+
// Memoize playback controls style
|
|
98
|
+
const playbackControlsStyle = useMemo(() => [
|
|
99
|
+
styles.playbackControls,
|
|
100
|
+
{ backgroundColor: tokens.colors.surface },
|
|
101
|
+
], [styles.playbackControls, tokens.colors.surface]);
|
|
102
|
+
|
|
103
|
+
// Memoize progress bar container style
|
|
104
|
+
const progressBarContainerStyle = useMemo(() => [
|
|
105
|
+
styles.progressBarContainer,
|
|
106
|
+
{ backgroundColor: tokens.colors.borderLight },
|
|
107
|
+
], [styles.progressBarContainer, tokens.colors.borderLight]);
|
|
108
|
+
|
|
109
|
+
// Memoize progress bar style
|
|
110
|
+
const progressBarStyle = useMemo(() => ({
|
|
111
|
+
...styles.progressBar,
|
|
112
|
+
width: `${calculateProgressPercent(currentTime, scene.duration)}%` as any,
|
|
113
|
+
backgroundColor: tokens.colors.primary,
|
|
114
|
+
}), [styles.progressBar, currentTime, scene.duration, tokens.colors.primary]);
|
|
115
|
+
|
|
116
|
+
// Memoize time text style
|
|
117
|
+
const timeTextStyle = useMemo(() => ({
|
|
118
|
+
color: tokens.colors.textSecondary,
|
|
119
|
+
}), [tokens.colors.textSecondary]);
|
|
120
|
+
|
|
46
121
|
return (
|
|
47
122
|
<View style={styles.previewSection}>
|
|
48
|
-
<View
|
|
49
|
-
style={[
|
|
50
|
-
styles.previewCanvas,
|
|
51
|
-
{
|
|
52
|
-
backgroundColor:
|
|
53
|
-
scene.background.type === "color"
|
|
54
|
-
? scene.background.value
|
|
55
|
-
: tokens.colors.surfaceSecondary,
|
|
56
|
-
height: PREVIEW_HEIGHT,
|
|
57
|
-
},
|
|
58
|
-
]}
|
|
59
|
-
>
|
|
123
|
+
<View style={canvasStyle}>
|
|
60
124
|
{scene.layers.length === 0 ? (
|
|
61
125
|
<View style={styles.emptyPreview}>
|
|
62
126
|
<AtomicIcon name="film-outline" size="xl" color="secondary" />
|
|
63
127
|
<AtomicText
|
|
64
128
|
type="bodyMedium"
|
|
65
|
-
style={
|
|
129
|
+
style={emptyTextStyle}
|
|
66
130
|
>
|
|
67
131
|
Canvas is empty. Add layers to get started.
|
|
68
132
|
</AtomicText>
|
|
@@ -76,28 +140,16 @@ export const EditorPreviewArea: React.FC<EditorPreviewAreaProps> = ({
|
|
|
76
140
|
canvasWidth={width}
|
|
77
141
|
canvasHeight={PREVIEW_HEIGHT}
|
|
78
142
|
isSelected={selectedLayerId === layer.id}
|
|
79
|
-
onSelect={() =>
|
|
80
|
-
onPositionChange={(
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
onSizeChange={(w, h) => onLayerSizeChange(layer.id, w, h)}
|
|
143
|
+
onSelect={() => handleLayerSelect(layer.id)}
|
|
144
|
+
onPositionChange={createLayerPositionHandler(layer.id)}
|
|
145
|
+
onSizeChange={createLayerSizeHandler(layer.id)}
|
|
84
146
|
/>
|
|
85
147
|
))}
|
|
86
148
|
|
|
87
149
|
{selectedLayerId && (
|
|
88
150
|
<TouchableOpacity
|
|
89
|
-
style={
|
|
90
|
-
|
|
91
|
-
{ backgroundColor: tokens.colors.primary },
|
|
92
|
-
]}
|
|
93
|
-
onPress={() => {
|
|
94
|
-
const layer = scene.layers.find(
|
|
95
|
-
(l) => l.id === selectedLayerId,
|
|
96
|
-
);
|
|
97
|
-
if (layer) {
|
|
98
|
-
onLayerActionsPress(layer);
|
|
99
|
-
}
|
|
100
|
-
}}
|
|
151
|
+
style={layerActionsButtonStyle}
|
|
152
|
+
onPress={handleLayerActionsPress}
|
|
101
153
|
>
|
|
102
154
|
<AtomicIcon
|
|
103
155
|
name="ellipsis-vertical"
|
|
@@ -110,12 +162,7 @@ export const EditorPreviewArea: React.FC<EditorPreviewAreaProps> = ({
|
|
|
110
162
|
)}
|
|
111
163
|
</View>
|
|
112
164
|
|
|
113
|
-
<View
|
|
114
|
-
style={[
|
|
115
|
-
styles.playbackControls,
|
|
116
|
-
{ backgroundColor: tokens.colors.surface },
|
|
117
|
-
]}
|
|
118
|
-
>
|
|
165
|
+
<View style={playbackControlsStyle}>
|
|
119
166
|
<View style={styles.playbackRow}>
|
|
120
167
|
<TouchableOpacity onPress={onPlayPause} style={styles.playButton}>
|
|
121
168
|
<AtomicIcon
|
|
@@ -128,10 +175,9 @@ export const EditorPreviewArea: React.FC<EditorPreviewAreaProps> = ({
|
|
|
128
175
|
<View style={styles.timeDisplay}>
|
|
129
176
|
<AtomicText
|
|
130
177
|
type="labelSmall"
|
|
131
|
-
style={
|
|
178
|
+
style={timeTextStyle}
|
|
132
179
|
>
|
|
133
|
-
{
|
|
134
|
-
{Math.floor(scene.duration / 1000)}s
|
|
180
|
+
{formatTimeDisplay(currentTime, true)} / {formatTimeDisplay(scene.duration, true)}
|
|
135
181
|
</AtomicText>
|
|
136
182
|
</View>
|
|
137
183
|
|
|
@@ -140,23 +186,12 @@ export const EditorPreviewArea: React.FC<EditorPreviewAreaProps> = ({
|
|
|
140
186
|
</TouchableOpacity>
|
|
141
187
|
</View>
|
|
142
188
|
|
|
143
|
-
<View
|
|
144
|
-
style={
|
|
145
|
-
styles.progressBarContainer,
|
|
146
|
-
{ backgroundColor: tokens.colors.borderLight },
|
|
147
|
-
]}
|
|
148
|
-
>
|
|
149
|
-
<View
|
|
150
|
-
style={[
|
|
151
|
-
styles.progressBar,
|
|
152
|
-
{
|
|
153
|
-
width: `${(currentTime / scene.duration) * 100}%`,
|
|
154
|
-
backgroundColor: tokens.colors.primary,
|
|
155
|
-
},
|
|
156
|
-
]}
|
|
157
|
-
/>
|
|
189
|
+
<View style={progressBarContainerStyle}>
|
|
190
|
+
<View style={progressBarStyle} />
|
|
158
191
|
</View>
|
|
159
192
|
</View>
|
|
160
193
|
</View>
|
|
161
194
|
);
|
|
162
|
-
};
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
EditorPreviewArea.displayName = 'EditorPreviewArea';
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Single Responsibility: Display scene timeline
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React from "react";
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
7
|
import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
8
|
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
|
|
9
9
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
@@ -18,6 +18,84 @@ interface EditorTimelineProps {
|
|
|
18
18
|
onAddScene: () => void;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
interface SceneCardProps {
|
|
22
|
+
scene: Scene;
|
|
23
|
+
index: number;
|
|
24
|
+
isSelected: boolean;
|
|
25
|
+
onSelect: (index: number) => void;
|
|
26
|
+
onLongPress: (index: number) => void;
|
|
27
|
+
primaryColor: string;
|
|
28
|
+
backgroundColor: string;
|
|
29
|
+
borderLightColor: string;
|
|
30
|
+
surfaceSecondaryColor: string;
|
|
31
|
+
onPrimaryColor: string;
|
|
32
|
+
textPrimaryColor: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Memoized SceneCard component to prevent unnecessary re-renders
|
|
37
|
+
* Only re-renders when its own props change, not when other scenes change
|
|
38
|
+
*/
|
|
39
|
+
const SceneCard = React.memo<SceneCardProps>(({
|
|
40
|
+
scene,
|
|
41
|
+
index,
|
|
42
|
+
isSelected,
|
|
43
|
+
onSelect,
|
|
44
|
+
onLongPress,
|
|
45
|
+
primaryColor,
|
|
46
|
+
backgroundColor,
|
|
47
|
+
borderLightColor,
|
|
48
|
+
surfaceSecondaryColor,
|
|
49
|
+
onPrimaryColor,
|
|
50
|
+
textPrimaryColor,
|
|
51
|
+
}) => {
|
|
52
|
+
const cardStyle = useMemo(() => [
|
|
53
|
+
styles.sceneCard,
|
|
54
|
+
{
|
|
55
|
+
backgroundColor: isSelected ? `${primaryColor}20` : backgroundColor,
|
|
56
|
+
borderColor: isSelected ? primaryColor : borderLightColor,
|
|
57
|
+
},
|
|
58
|
+
], [isSelected, primaryColor, backgroundColor, borderLightColor]);
|
|
59
|
+
|
|
60
|
+
const thumbnailStyle = useMemo(() => [
|
|
61
|
+
styles.sceneThumbnail,
|
|
62
|
+
{
|
|
63
|
+
backgroundColor: scene.background.value || surfaceSecondaryColor,
|
|
64
|
+
},
|
|
65
|
+
], [scene.background.value, surfaceSecondaryColor]);
|
|
66
|
+
|
|
67
|
+
const textStyle = useMemo(() => ({
|
|
68
|
+
color: textPrimaryColor,
|
|
69
|
+
marginTop: 4,
|
|
70
|
+
fontWeight: isSelected ? "600" as const : "400" as const,
|
|
71
|
+
}), [isSelected, textPrimaryColor]);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<TouchableOpacity
|
|
75
|
+
style={cardStyle}
|
|
76
|
+
onPress={() => onSelect(index)}
|
|
77
|
+
onLongPress={() => onLongPress(index)}
|
|
78
|
+
>
|
|
79
|
+
<View style={thumbnailStyle}>
|
|
80
|
+
<AtomicText
|
|
81
|
+
type="labelSmall"
|
|
82
|
+
style={{ color: onPrimaryColor, fontWeight: "600" }}
|
|
83
|
+
>
|
|
84
|
+
{index + 1}
|
|
85
|
+
</AtomicText>
|
|
86
|
+
</View>
|
|
87
|
+
<AtomicText
|
|
88
|
+
type="labelSmall"
|
|
89
|
+
style={textStyle}
|
|
90
|
+
>
|
|
91
|
+
{scene.duration / 1000}s
|
|
92
|
+
</AtomicText>
|
|
93
|
+
</TouchableOpacity>
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
SceneCard.displayName = 'SceneCard';
|
|
98
|
+
|
|
21
99
|
export const EditorTimeline: React.FC<EditorTimelineProps> = ({
|
|
22
100
|
project,
|
|
23
101
|
currentSceneIndex,
|
|
@@ -28,12 +106,23 @@ export const EditorTimeline: React.FC<EditorTimelineProps> = ({
|
|
|
28
106
|
const { t } = useLocalization();
|
|
29
107
|
const tokens = useAppDesignTokens();
|
|
30
108
|
|
|
109
|
+
// Memoize styles that don't change
|
|
110
|
+
const containerStyle = useMemo(() => [
|
|
111
|
+
styles.timeline,
|
|
112
|
+
{ backgroundColor: tokens.colors.surface }
|
|
113
|
+
], [tokens.colors.surface]);
|
|
114
|
+
|
|
115
|
+
const headerTextStyle = useMemo(() => ({
|
|
116
|
+
color: tokens.colors.textPrimary,
|
|
117
|
+
fontWeight: "600" as const,
|
|
118
|
+
}), [tokens.colors.textPrimary]);
|
|
119
|
+
|
|
31
120
|
return (
|
|
32
|
-
<View style={
|
|
121
|
+
<View style={containerStyle}>
|
|
33
122
|
<View style={styles.timelineHeader}>
|
|
34
123
|
<AtomicText
|
|
35
124
|
type="bodyMedium"
|
|
36
|
-
style={
|
|
125
|
+
style={headerTextStyle}
|
|
37
126
|
>
|
|
38
127
|
{t("editor.timeline.title")}
|
|
39
128
|
</AtomicText>
|
|
@@ -48,51 +137,20 @@ export const EditorTimeline: React.FC<EditorTimelineProps> = ({
|
|
|
48
137
|
style={styles.scenesScroll}
|
|
49
138
|
>
|
|
50
139
|
{project.scenes.map((scene: Scene, index: number) => (
|
|
51
|
-
<
|
|
140
|
+
<SceneCard
|
|
52
141
|
key={scene.id}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
]}
|
|
66
|
-
onPress={() => onSceneSelect(index)}
|
|
67
|
-
onLongPress={() => onSceneLongPress(index)}
|
|
68
|
-
>
|
|
69
|
-
<View
|
|
70
|
-
style={[
|
|
71
|
-
styles.sceneThumbnail,
|
|
72
|
-
{
|
|
73
|
-
backgroundColor:
|
|
74
|
-
scene.background.value || tokens.colors.surfaceSecondary,
|
|
75
|
-
},
|
|
76
|
-
]}
|
|
77
|
-
>
|
|
78
|
-
<AtomicText
|
|
79
|
-
type="labelSmall"
|
|
80
|
-
style={{ color: tokens.colors.onPrimary, fontWeight: "600" }}
|
|
81
|
-
>
|
|
82
|
-
{index + 1}
|
|
83
|
-
</AtomicText>
|
|
84
|
-
</View>
|
|
85
|
-
<AtomicText
|
|
86
|
-
type="labelSmall"
|
|
87
|
-
style={{
|
|
88
|
-
color: tokens.colors.textPrimary,
|
|
89
|
-
marginTop: 4,
|
|
90
|
-
fontWeight: currentSceneIndex === index ? "600" : "400",
|
|
91
|
-
}}
|
|
92
|
-
>
|
|
93
|
-
{scene.duration / 1000}s
|
|
94
|
-
</AtomicText>
|
|
95
|
-
</TouchableOpacity>
|
|
142
|
+
scene={scene}
|
|
143
|
+
index={index}
|
|
144
|
+
isSelected={currentSceneIndex === index}
|
|
145
|
+
onSelect={onSceneSelect}
|
|
146
|
+
onLongPress={onSceneLongPress}
|
|
147
|
+
primaryColor={tokens.colors.primary}
|
|
148
|
+
backgroundColor={tokens.colors.backgroundPrimary}
|
|
149
|
+
borderLightColor={tokens.colors.borderLight}
|
|
150
|
+
surfaceSecondaryColor={tokens.colors.surfaceSecondary}
|
|
151
|
+
onPrimaryColor={tokens.colors.onPrimary}
|
|
152
|
+
textPrimaryColor={tokens.colors.textPrimary}
|
|
153
|
+
/>
|
|
96
154
|
))}
|
|
97
155
|
</ScrollView>
|
|
98
156
|
</View>
|