@umituz/react-native-video-editor 1.1.39 → 1.1.40
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/VideoEditor.tsx +210 -0
- package/src/index.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-video-editor",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.40",
|
|
4
4
|
"description": "Professional video editor with layer-based timeline, text/image/shape/audio/animation layers, and export functionality",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VideoEditor Component
|
|
3
|
+
* Self-contained full-screen video editor.
|
|
4
|
+
* Mirrors PhotoEditor API: accepts videoUri, onClose, onSave.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useState, useMemo, useRef, useCallback } from "react";
|
|
8
|
+
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
9
|
+
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
|
|
10
|
+
import { BottomSheetModal } from "@umituz/react-native-design-system/molecules";
|
|
11
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
12
|
+
import { useSafeAreaInsets } from "@umituz/react-native-design-system/safe-area";
|
|
13
|
+
|
|
14
|
+
import { VideoPlayer } from "./player/presentation/components/VideoPlayer";
|
|
15
|
+
import { VideoFilterPicker } from "./presentation/components/VideoFilterPicker";
|
|
16
|
+
import { SpeedControlPanel } from "./presentation/components/SpeedControlPanel";
|
|
17
|
+
import { FILTER_PRESETS, DEFAULT_FILTER } from "./infrastructure/constants/filter.constants";
|
|
18
|
+
import { DEFAULT_PLAYBACK_RATE } from "./infrastructure/constants/speed.constants";
|
|
19
|
+
import type { FilterPreset } from "./domain/entities/video-project.types";
|
|
20
|
+
|
|
21
|
+
export interface VideoEditorProps {
|
|
22
|
+
videoUri: string;
|
|
23
|
+
onClose: () => void;
|
|
24
|
+
onSave?: (uri: string, filter: FilterPreset, playbackRate: number) => void;
|
|
25
|
+
title?: string;
|
|
26
|
+
t: (key: string) => string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type ActiveTool = "filters" | "speed" | null;
|
|
30
|
+
|
|
31
|
+
export const VideoEditor: React.FC<VideoEditorProps> = ({
|
|
32
|
+
videoUri,
|
|
33
|
+
onClose,
|
|
34
|
+
onSave,
|
|
35
|
+
title,
|
|
36
|
+
t,
|
|
37
|
+
}) => {
|
|
38
|
+
const tokens = useAppDesignTokens();
|
|
39
|
+
const insets = useSafeAreaInsets();
|
|
40
|
+
|
|
41
|
+
const [activeFilter, setActiveFilter] = useState<FilterPreset>(DEFAULT_FILTER);
|
|
42
|
+
const [playbackRate, setPlaybackRate] = useState(DEFAULT_PLAYBACK_RATE);
|
|
43
|
+
const [activeTool, setActiveTool] = useState<ActiveTool>(null);
|
|
44
|
+
|
|
45
|
+
const filterSheetRef = useRef<{ present: () => void; dismiss: () => void }>(null);
|
|
46
|
+
const speedSheetRef = useRef<{ present: () => void; dismiss: () => void }>(null);
|
|
47
|
+
|
|
48
|
+
const handleToggleTool = useCallback((tool: Exclude<ActiveTool, null>) => {
|
|
49
|
+
if (activeTool === tool) {
|
|
50
|
+
setActiveTool(null);
|
|
51
|
+
if (tool === "filters") filterSheetRef.current?.dismiss();
|
|
52
|
+
else speedSheetRef.current?.dismiss();
|
|
53
|
+
} else {
|
|
54
|
+
setActiveTool(tool);
|
|
55
|
+
if (tool === "filters") filterSheetRef.current?.present();
|
|
56
|
+
else speedSheetRef.current?.present();
|
|
57
|
+
}
|
|
58
|
+
}, [activeTool]);
|
|
59
|
+
|
|
60
|
+
const handleSave = useCallback(() => {
|
|
61
|
+
onSave?.(videoUri, activeFilter, playbackRate);
|
|
62
|
+
onClose();
|
|
63
|
+
}, [onSave, onClose, videoUri, activeFilter, playbackRate]);
|
|
64
|
+
|
|
65
|
+
const styles = useMemo(() => StyleSheet.create({
|
|
66
|
+
container: {
|
|
67
|
+
flex: 1,
|
|
68
|
+
backgroundColor: tokens.colors.backgroundPrimary,
|
|
69
|
+
},
|
|
70
|
+
header: {
|
|
71
|
+
flexDirection: "row",
|
|
72
|
+
alignItems: "center",
|
|
73
|
+
justifyContent: "space-between",
|
|
74
|
+
paddingTop: insets.top + tokens.spacing.sm,
|
|
75
|
+
paddingBottom: tokens.spacing.sm,
|
|
76
|
+
paddingHorizontal: tokens.spacing.md,
|
|
77
|
+
backgroundColor: tokens.colors.surface,
|
|
78
|
+
},
|
|
79
|
+
headerTitle: {
|
|
80
|
+
flex: 1,
|
|
81
|
+
textAlign: "center",
|
|
82
|
+
},
|
|
83
|
+
headerBtn: {
|
|
84
|
+
width: 40,
|
|
85
|
+
height: 40,
|
|
86
|
+
alignItems: "center",
|
|
87
|
+
justifyContent: "center",
|
|
88
|
+
},
|
|
89
|
+
videoArea: {
|
|
90
|
+
flex: 1,
|
|
91
|
+
justifyContent: "center",
|
|
92
|
+
alignItems: "center",
|
|
93
|
+
backgroundColor: tokens.colors.backgroundPrimary,
|
|
94
|
+
},
|
|
95
|
+
toolbar: {
|
|
96
|
+
flexDirection: "row",
|
|
97
|
+
justifyContent: "space-around",
|
|
98
|
+
paddingVertical: tokens.spacing.md,
|
|
99
|
+
paddingHorizontal: tokens.spacing.md,
|
|
100
|
+
paddingBottom: insets.bottom + tokens.spacing.md,
|
|
101
|
+
backgroundColor: tokens.colors.surface,
|
|
102
|
+
borderTopWidth: 1,
|
|
103
|
+
borderTopColor: tokens.colors.border,
|
|
104
|
+
},
|
|
105
|
+
toolBtn: {
|
|
106
|
+
alignItems: "center",
|
|
107
|
+
gap: tokens.spacing.xs,
|
|
108
|
+
paddingHorizontal: tokens.spacing.lg,
|
|
109
|
+
},
|
|
110
|
+
toolBtnActive: {
|
|
111
|
+
opacity: 1,
|
|
112
|
+
},
|
|
113
|
+
}), [tokens, insets]);
|
|
114
|
+
|
|
115
|
+
const TOOLS: { id: Exclude<ActiveTool, null>; icon: string; labelKey: string }[] = [
|
|
116
|
+
{ id: "filters", icon: "sparkles", labelKey: "editor.tools.filters" },
|
|
117
|
+
{ id: "speed", icon: "flash", labelKey: "editor.tools.speed" },
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<View style={styles.container}>
|
|
122
|
+
{/* Header */}
|
|
123
|
+
<View style={styles.header}>
|
|
124
|
+
<TouchableOpacity
|
|
125
|
+
style={styles.headerBtn}
|
|
126
|
+
onPress={onClose}
|
|
127
|
+
accessibilityLabel="Close"
|
|
128
|
+
accessibilityRole="button"
|
|
129
|
+
>
|
|
130
|
+
<AtomicIcon name="close" size="md" color="textPrimary" />
|
|
131
|
+
</TouchableOpacity>
|
|
132
|
+
|
|
133
|
+
<AtomicText type="headlineSmall" style={styles.headerTitle}>
|
|
134
|
+
{title || t("editor.video.title") || "Edit Video"}
|
|
135
|
+
</AtomicText>
|
|
136
|
+
|
|
137
|
+
<TouchableOpacity
|
|
138
|
+
style={styles.headerBtn}
|
|
139
|
+
onPress={handleSave}
|
|
140
|
+
accessibilityLabel="Save"
|
|
141
|
+
accessibilityRole="button"
|
|
142
|
+
>
|
|
143
|
+
<AtomicText fontWeight="bold" color="primary">
|
|
144
|
+
{t("common.save") || "Save"}
|
|
145
|
+
</AtomicText>
|
|
146
|
+
</TouchableOpacity>
|
|
147
|
+
</View>
|
|
148
|
+
|
|
149
|
+
{/* Video Preview */}
|
|
150
|
+
<View style={styles.videoArea}>
|
|
151
|
+
<VideoPlayer
|
|
152
|
+
source={videoUri}
|
|
153
|
+
autoPlay
|
|
154
|
+
loop
|
|
155
|
+
nativeControls={false}
|
|
156
|
+
contentFit="contain"
|
|
157
|
+
playbackRate={playbackRate}
|
|
158
|
+
filterOverlay={activeFilter.id !== "none" ? activeFilter : undefined}
|
|
159
|
+
/>
|
|
160
|
+
</View>
|
|
161
|
+
|
|
162
|
+
{/* Toolbar */}
|
|
163
|
+
<View style={styles.toolbar}>
|
|
164
|
+
{TOOLS.map((tool) => {
|
|
165
|
+
const isActive = activeTool === tool.id;
|
|
166
|
+
return (
|
|
167
|
+
<TouchableOpacity
|
|
168
|
+
key={tool.id}
|
|
169
|
+
style={styles.toolBtn}
|
|
170
|
+
onPress={() => handleToggleTool(tool.id)}
|
|
171
|
+
accessibilityLabel={t(tool.labelKey) || tool.id}
|
|
172
|
+
accessibilityRole="button"
|
|
173
|
+
accessibilityState={{ selected: isActive }}
|
|
174
|
+
>
|
|
175
|
+
<AtomicIcon
|
|
176
|
+
name={tool.icon}
|
|
177
|
+
size="md"
|
|
178
|
+
color={isActive ? "primary" : "textSecondary"}
|
|
179
|
+
/>
|
|
180
|
+
<AtomicText
|
|
181
|
+
type="labelSmall"
|
|
182
|
+
color={isActive ? "primary" : "textSecondary"}
|
|
183
|
+
>
|
|
184
|
+
{t(tool.labelKey) || tool.id}
|
|
185
|
+
</AtomicText>
|
|
186
|
+
</TouchableOpacity>
|
|
187
|
+
);
|
|
188
|
+
})}
|
|
189
|
+
</View>
|
|
190
|
+
|
|
191
|
+
{/* Filter Bottom Sheet */}
|
|
192
|
+
<BottomSheetModal ref={filterSheetRef} snapPoints={["40%"]}>
|
|
193
|
+
<VideoFilterPicker
|
|
194
|
+
activeFilter={activeFilter}
|
|
195
|
+
onSelectFilter={setActiveFilter}
|
|
196
|
+
t={t}
|
|
197
|
+
/>
|
|
198
|
+
</BottomSheetModal>
|
|
199
|
+
|
|
200
|
+
{/* Speed Bottom Sheet */}
|
|
201
|
+
<BottomSheetModal ref={speedSheetRef} snapPoints={["30%"]}>
|
|
202
|
+
<SpeedControlPanel
|
|
203
|
+
playbackRate={playbackRate}
|
|
204
|
+
onChangeRate={setPlaybackRate}
|
|
205
|
+
t={t}
|
|
206
|
+
/>
|
|
207
|
+
</BottomSheetModal>
|
|
208
|
+
</View>
|
|
209
|
+
);
|
|
210
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -60,6 +60,9 @@ export {
|
|
|
60
60
|
// PRESENTATION LAYER - Components & Hooks
|
|
61
61
|
// =============================================================================
|
|
62
62
|
|
|
63
|
+
export { VideoEditor } from "./VideoEditor";
|
|
64
|
+
export type { VideoEditorProps } from "./VideoEditor";
|
|
65
|
+
|
|
63
66
|
export {
|
|
64
67
|
EditorHeader,
|
|
65
68
|
EditorPreviewArea,
|