@umituz/react-native-video-editor 1.1.47 → 1.1.49
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 +5 -1
- package/src/VideoEditor.tsx +1 -1
- package/src/domain/entities/video-project.types.ts +49 -0
- package/src/index.ts +28 -27
- package/src/infrastructure/constants/animation-layer.constants.ts +1 -1
- package/src/infrastructure/constants/filter.constants.ts +1 -1
- package/src/infrastructure/services/image-layer-operations.service.ts +24 -6
- package/src/infrastructure/services/layer-manipulation.service.ts +6 -8
- package/src/infrastructure/services/layer-operations/layer-delete.service.ts +2 -2
- package/src/infrastructure/services/layer-operations/layer-duplicate.service.ts +2 -2
- package/src/infrastructure/services/layer-operations/layer-order.service.ts +16 -2
- package/src/infrastructure/services/layer-operations/layer-transform.service.ts +15 -6
- package/src/infrastructure/services/layer-operations.service.ts +2 -2
- package/src/infrastructure/services/scene-operations.service.ts +2 -2
- package/src/infrastructure/services/shape-layer-operations.service.ts +5 -5
- package/src/infrastructure/services/text-layer-operations.service.ts +13 -4
- package/src/infrastructure/utils/srt.utils.ts +8 -1
- package/src/player/index.ts +0 -3
- package/src/player/presentation/components/FullScreenVideoPlayer.tsx +0 -1
- package/src/player/presentation/components/VideoPlayer.tsx +8 -7
- package/src/player/presentation/components/VideoPlayerOverlay.tsx +1 -1
- package/src/player/presentation/hooks/useVideoPlaybackProgress.ts +0 -1
- package/src/player/presentation/hooks/useVideoPlayerControl.ts +6 -4
- package/src/player/{types/index.ts → types.ts} +3 -2
- package/src/presentation/components/AnimationEditor.tsx +4 -6
- package/src/presentation/components/AudioEditor.tsx +6 -8
- package/src/presentation/components/DraggableLayer.tsx +1 -1
- package/src/presentation/components/EditorPreviewArea.tsx +1 -1
- package/src/presentation/components/EditorTimeline.tsx +1 -1
- package/src/presentation/components/ExportDialog.tsx +7 -9
- package/src/presentation/components/ImageLayerEditor.tsx +4 -6
- package/src/presentation/components/LayerActionsMenu.tsx +1 -1
- package/src/presentation/components/ShapeLayerEditor.tsx +5 -7
- package/src/presentation/components/SubtitleListPanel.tsx +1 -1
- package/src/presentation/components/TextLayerEditor.tsx +8 -10
- package/src/presentation/components/VideoFilterPicker.tsx +1 -1
- package/src/presentation/components/animation-layer/AnimationTypeSelector.tsx +2 -2
- package/src/presentation/components/draggable-layer/LayerContent.tsx +1 -1
- package/src/presentation/components/export/ProjectInfoBox.tsx +1 -1
- package/src/presentation/components/shape-layer/ShapeTypeSelector.tsx +1 -1
- package/src/presentation/hooks/useAnimationLayerForm.ts +1 -1
- package/src/presentation/hooks/useAudioLayerForm.ts +1 -1
- package/src/presentation/hooks/useDraggableLayerGestures.ts +21 -4
- package/src/presentation/hooks/useEditorActions.tsx +1 -1
- package/src/presentation/hooks/useEditorHistory.ts +5 -4
- package/src/presentation/hooks/useEditorLayers.ts +2 -2
- package/src/presentation/hooks/useEditorPlayback.ts +1 -1
- package/src/presentation/hooks/useEditorScenes.ts +1 -1
- package/src/presentation/hooks/useExport.ts +1 -1
- package/src/presentation/hooks/useExportActions.tsx +1 -1
- package/src/presentation/hooks/useExportForm.ts +1 -1
- package/src/presentation/hooks/useImageLayerForm.ts +1 -1
- package/src/presentation/hooks/useImageLayerOperations.ts +1 -1
- package/src/presentation/hooks/useLayerActions.tsx +3 -5
- package/src/presentation/hooks/useLayerManipulation.ts +1 -1
- package/src/presentation/hooks/useMenuActions.tsx +1 -1
- package/src/presentation/hooks/useSceneActions.tsx +1 -1
- package/src/presentation/hooks/useShapeLayerForm.ts +1 -1
- package/src/presentation/hooks/useShapeLayerOperations.ts +1 -1
- package/src/presentation/hooks/useTextLayerForm.ts +1 -1
- package/src/presentation/hooks/useTextLayerOperations.ts +1 -1
- package/src/domain/entities/index.ts +0 -52
- package/src/infrastructure/constants/index.ts +0 -15
- package/src/infrastructure/services/layer-operations/index.ts +0 -9
- package/src/presentation/components/animation-layer/index.ts +0 -8
- package/src/presentation/components/audio-layer/index.ts +0 -10
- package/src/presentation/components/export/index.ts +0 -11
- package/src/presentation/components/image-layer/index.ts +0 -8
- package/src/presentation/components/index.ts +0 -24
- package/src/presentation/components/shape-layer/index.ts +0 -9
- package/src/presentation/components/text-layer/index.ts +0 -12
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.49",
|
|
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",
|
|
@@ -45,6 +45,10 @@
|
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@gorhom/bottom-sheet": "^5.2.8",
|
|
48
|
+
"@react-navigation/bottom-tabs": "^7.15.5",
|
|
49
|
+
"@react-navigation/elements": "^2.9.10",
|
|
50
|
+
"@react-navigation/native": "^7.1.33",
|
|
51
|
+
"@react-navigation/stack": "^7.8.5",
|
|
48
52
|
"@types/react": "~19.1.10",
|
|
49
53
|
"@types/react-native": "*",
|
|
50
54
|
"@typescript-eslint/eslint-plugin": "*",
|
package/src/VideoEditor.tsx
CHANGED
|
@@ -14,7 +14,7 @@ import { useSafeAreaInsets } from "@umituz/react-native-design-system/safe-area"
|
|
|
14
14
|
import { VideoPlayer } from "./player/presentation/components/VideoPlayer";
|
|
15
15
|
import { VideoFilterPicker } from "./presentation/components/VideoFilterPicker";
|
|
16
16
|
import { SpeedControlPanel } from "./presentation/components/SpeedControlPanel";
|
|
17
|
-
import {
|
|
17
|
+
import { DEFAULT_FILTER } from "./infrastructure/constants/filter.constants";
|
|
18
18
|
import { DEFAULT_PLAYBACK_RATE } from "./infrastructure/constants/speed.constants";
|
|
19
19
|
import type { FilterPreset } from "./domain/entities/video-project.types";
|
|
20
20
|
|
|
@@ -174,3 +174,52 @@ export interface VideoProject {
|
|
|
174
174
|
folderId?: string;
|
|
175
175
|
tags: string[];
|
|
176
176
|
}
|
|
177
|
+
|
|
178
|
+
// Editor State
|
|
179
|
+
export interface EditorState {
|
|
180
|
+
project: VideoProject | null;
|
|
181
|
+
currentSceneIndex: number;
|
|
182
|
+
selectedLayerId: string | null;
|
|
183
|
+
isPlaying: boolean;
|
|
184
|
+
currentTime: number;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Operation Results
|
|
188
|
+
export interface LayerOperationResult {
|
|
189
|
+
success: boolean;
|
|
190
|
+
updatedScenes: Scene[];
|
|
191
|
+
error?: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface SceneOperationResult {
|
|
195
|
+
success: boolean;
|
|
196
|
+
updatedScenes: Scene[];
|
|
197
|
+
newSceneIndex?: number;
|
|
198
|
+
error?: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Layer Actions
|
|
202
|
+
export type LayerOrderAction = "front" | "back" | "up" | "down";
|
|
203
|
+
|
|
204
|
+
// Layer Data
|
|
205
|
+
export interface AddTextLayerData {
|
|
206
|
+
content?: string;
|
|
207
|
+
fontSize?: number;
|
|
208
|
+
fontFamily?: string;
|
|
209
|
+
fontWeight?: string;
|
|
210
|
+
color?: string;
|
|
211
|
+
textAlign?: "left" | "center" | "right";
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface AddImageLayerData {
|
|
215
|
+
uri?: string;
|
|
216
|
+
opacity?: number;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface AddShapeLayerData {
|
|
220
|
+
shape?: string;
|
|
221
|
+
opacity?: number;
|
|
222
|
+
fillColor?: string;
|
|
223
|
+
borderColor?: string;
|
|
224
|
+
borderWidth?: number;
|
|
225
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -36,13 +36,14 @@ export type {
|
|
|
36
36
|
FilterPreset,
|
|
37
37
|
SubtitleStyle,
|
|
38
38
|
Subtitle,
|
|
39
|
-
} from "./domain/entities";
|
|
39
|
+
} from "./domain/entities/video-project.types";
|
|
40
40
|
|
|
41
41
|
// =============================================================================
|
|
42
42
|
// INFRASTRUCTURE LAYER - Services & Constants
|
|
43
43
|
// =============================================================================
|
|
44
44
|
|
|
45
|
-
export
|
|
45
|
+
export { FILTER_PRESETS, DEFAULT_FILTER } from "./infrastructure/constants/filter.constants";
|
|
46
|
+
export { DEFAULT_PLAYBACK_RATE, SPEED_PRESETS } from "./infrastructure/constants/speed.constants";
|
|
46
47
|
|
|
47
48
|
export { layerOperationsService } from "./infrastructure/services/layer-operations.service";
|
|
48
49
|
export { sceneOperationsService } from "./infrastructure/services/scene-operations.service";
|
|
@@ -51,12 +52,10 @@ export { imageLayerOperationsService } from "./infrastructure/services/image-lay
|
|
|
51
52
|
export { shapeLayerOperationsService } from "./infrastructure/services/shape-layer-operations.service";
|
|
52
53
|
export { layerManipulationService } from "./infrastructure/services/layer-manipulation.service";
|
|
53
54
|
|
|
54
|
-
export {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
layerTransformService,
|
|
59
|
-
} from "./infrastructure/services/layer-operations";
|
|
55
|
+
export { layerDeleteService } from "./infrastructure/services/layer-operations/layer-delete.service";
|
|
56
|
+
export { layerDuplicateService } from "./infrastructure/services/layer-operations/layer-duplicate.service";
|
|
57
|
+
export { layerOrderService } from "./infrastructure/services/layer-operations/layer-order.service";
|
|
58
|
+
export { layerTransformService } from "./infrastructure/services/layer-operations/layer-transform.service";
|
|
60
59
|
|
|
61
60
|
// =============================================================================
|
|
62
61
|
// PRESENTATION LAYER - Components & Hooks
|
|
@@ -65,24 +64,26 @@ export {
|
|
|
65
64
|
export { VideoEditor } from "./VideoEditor";
|
|
66
65
|
export type { VideoEditorProps } from "./VideoEditor";
|
|
67
66
|
|
|
68
|
-
export {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
} from "./presentation/components";
|
|
67
|
+
export { EditorHeader } from "./presentation/components/EditorHeader";
|
|
68
|
+
export { EditorPreviewArea } from "./presentation/components/EditorPreviewArea";
|
|
69
|
+
export { EditorToolPanel } from "./presentation/components/EditorToolPanel";
|
|
70
|
+
export { EditorTimeline } from "./presentation/components/EditorTimeline";
|
|
71
|
+
export { LayerActionsMenu } from "./presentation/components/LayerActionsMenu";
|
|
72
|
+
export { SceneActionsMenu } from "./presentation/components/SceneActionsMenu";
|
|
73
|
+
export { TextLayerEditor } from "./presentation/components/TextLayerEditor";
|
|
74
|
+
export { AudioEditor } from "./presentation/components/AudioEditor";
|
|
75
|
+
export { ShapeLayerEditor } from "./presentation/components/ShapeLayerEditor";
|
|
76
|
+
export { AnimationEditor } from "./presentation/components/AnimationEditor";
|
|
77
|
+
export { DraggableLayer } from "./presentation/components/DraggableLayer";
|
|
78
|
+
export { ImageLayerEditor } from "./presentation/components/ImageLayerEditor";
|
|
79
|
+
export { ExportDialog } from "./presentation/components/ExportDialog";
|
|
80
|
+
export { SpeedControlPanel } from "./presentation/components/SpeedControlPanel";
|
|
81
|
+
export { VideoFilterPicker } from "./presentation/components/VideoFilterPicker";
|
|
82
|
+
export { CollageEditorCanvas } from "./presentation/components/CollageEditorCanvas";
|
|
83
|
+
export { SubtitleTimeInput } from "./presentation/components/SubtitleTimeInput";
|
|
84
|
+
export { SubtitleStylePicker } from "./presentation/components/SubtitleStylePicker";
|
|
85
|
+
export { SubtitleOverlay } from "./presentation/components/SubtitleOverlay";
|
|
86
|
+
export { SubtitleListPanel } from "./presentation/components/SubtitleListPanel";
|
|
86
87
|
|
|
87
88
|
export { useEditorLayers } from "./presentation/hooks/useEditorLayers";
|
|
88
89
|
export { useEditorScenes } from "./presentation/hooks/useEditorScenes";
|
|
@@ -131,7 +132,7 @@ export type {
|
|
|
131
132
|
VideoProgressBarProps,
|
|
132
133
|
VideoPlayerOverlayProps,
|
|
133
134
|
FullScreenVideoPlayerProps,
|
|
134
|
-
} from "./player";
|
|
135
|
+
} from "./player/index";
|
|
135
136
|
|
|
136
137
|
export {
|
|
137
138
|
safePlay,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Centralized constants for animation layer editor
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { AnimationType } from "../../domain/entities";
|
|
6
|
+
import type { AnimationType } from "../../domain/entities/video-project.types";
|
|
7
7
|
|
|
8
8
|
export type Easing = "linear" | "ease-in" | "ease-out" | "ease-in-out";
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Color overlay-based filters for video and photo editing
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { FilterPreset } from "../../domain/entities";
|
|
6
|
+
import type { FilterPreset } from "../../domain/entities/video-project.types";
|
|
7
7
|
|
|
8
8
|
export const FILTER_PRESETS: FilterPreset[] = [
|
|
9
9
|
{ id: "none", name: "Original", overlay: "transparent", opacity: 0 },
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { generateUUID } from "@umituz/react-native-design-system/uuid";
|
|
7
|
-
import type { Scene, ImageLayer } from "../../domain/entities";
|
|
8
|
-
import type { LayerOperationResult, AddImageLayerData } from "../../domain/entities";
|
|
7
|
+
import type { Scene, ImageLayer } from "../../domain/entities/video-project.types";
|
|
8
|
+
import type { LayerOperationResult, AddImageLayerData } from "../../domain/entities/video-project.types";
|
|
9
9
|
|
|
10
10
|
class ImageLayerOperationsService {
|
|
11
11
|
/**
|
|
@@ -25,14 +25,23 @@ class ImageLayerOperationsService {
|
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// Validate URI
|
|
29
|
+
if (!layerData.uri || layerData.uri.trim().length === 0) {
|
|
30
|
+
return {
|
|
31
|
+
success: false,
|
|
32
|
+
updatedScenes: scenes,
|
|
33
|
+
error: "Image URI is required",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
const newLayer: ImageLayer = {
|
|
29
38
|
id: generateUUID(),
|
|
30
39
|
type: "image",
|
|
31
|
-
uri: layerData.uri
|
|
40
|
+
uri: layerData.uri ?? "",
|
|
32
41
|
position: { x: 15, y: 30 },
|
|
33
42
|
size: { width: 70, height: 40 },
|
|
34
43
|
rotation: 0,
|
|
35
|
-
opacity: layerData.opacity
|
|
44
|
+
opacity: layerData.opacity ?? 1,
|
|
36
45
|
animation: {
|
|
37
46
|
type: "fade",
|
|
38
47
|
duration: 500,
|
|
@@ -88,10 +97,19 @@ class ImageLayerOperationsService {
|
|
|
88
97
|
};
|
|
89
98
|
}
|
|
90
99
|
|
|
100
|
+
const existingLayer = updatedScenes[sceneIndex].layers[layerIndex];
|
|
101
|
+
if (existingLayer.type !== "image") {
|
|
102
|
+
return {
|
|
103
|
+
success: false,
|
|
104
|
+
updatedScenes: scenes,
|
|
105
|
+
error: "Layer is not an image layer",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
91
109
|
updatedScenes[sceneIndex].layers[layerIndex] = {
|
|
92
|
-
...
|
|
110
|
+
...existingLayer,
|
|
93
111
|
...layerData,
|
|
94
|
-
}
|
|
112
|
+
};
|
|
95
113
|
|
|
96
114
|
return { success: true, updatedScenes };
|
|
97
115
|
} catch (error) {
|
|
@@ -3,14 +3,12 @@
|
|
|
3
3
|
* Orchestrator service that delegates to specialized layer operation services
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { Scene, Animation } from "../../domain/entities";
|
|
7
|
-
import type { LayerOperationResult, LayerOrderAction } from "../../domain/entities";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
layerTransformService,
|
|
13
|
-
} from "./layer-operations";
|
|
6
|
+
import type { Scene, Animation } from "../../domain/entities/video-project.types";
|
|
7
|
+
import type { LayerOperationResult, LayerOrderAction } from "../../domain/entities/video-project.types";
|
|
8
|
+
import { layerDeleteService } from "./layer-operations/layer-delete.service";
|
|
9
|
+
import { layerOrderService } from "./layer-operations/layer-order.service";
|
|
10
|
+
import { layerDuplicateService } from "./layer-operations/layer-duplicate.service";
|
|
11
|
+
import { layerTransformService } from "./layer-operations/layer-transform.service";
|
|
14
12
|
|
|
15
13
|
class LayerManipulationService {
|
|
16
14
|
deleteLayer(
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Single Responsibility: Handle layer deletion operations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { Scene } from "../../../domain/entities";
|
|
7
|
-
import type { LayerOperationResult } from "../../../domain/entities";
|
|
6
|
+
import type { Scene } from "../../../domain/entities/video-project.types";
|
|
7
|
+
import type { LayerOperationResult } from "../../../domain/entities/video-project.types";
|
|
8
8
|
|
|
9
9
|
class LayerDeleteService {
|
|
10
10
|
/**
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { generateUUID } from "@umituz/react-native-design-system/uuid";
|
|
7
|
-
import type { Scene } from "../../../domain/entities";
|
|
8
|
-
import type { LayerOperationResult } from "../../../domain/entities";
|
|
7
|
+
import type { Scene } from "../../../domain/entities/video-project.types";
|
|
8
|
+
import type { LayerOperationResult } from "../../../domain/entities/video-project.types";
|
|
9
9
|
|
|
10
10
|
class LayerDuplicateService {
|
|
11
11
|
/**
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Single Responsibility: Handle layer ordering operations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { Scene } from "../../../domain/entities";
|
|
7
|
-
import type { LayerOperationResult, LayerOrderAction } from "../../../domain/entities";
|
|
6
|
+
import type { Scene } from "../../../domain/entities/video-project.types";
|
|
7
|
+
import type { LayerOperationResult, LayerOrderAction } from "../../../domain/entities/video-project.types";
|
|
8
8
|
|
|
9
9
|
class LayerOrderService {
|
|
10
10
|
/**
|
|
@@ -48,9 +48,23 @@ class LayerOrderService {
|
|
|
48
48
|
newIndex = 0;
|
|
49
49
|
break;
|
|
50
50
|
case "up":
|
|
51
|
+
if (updatedLayers.length === 1) {
|
|
52
|
+
return {
|
|
53
|
+
success: false,
|
|
54
|
+
updatedScenes: scenes,
|
|
55
|
+
error: "Cannot move layer - only one layer exists",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
51
58
|
newIndex = Math.min(layerIndex + 1, updatedLayers.length - 1);
|
|
52
59
|
break;
|
|
53
60
|
case "down":
|
|
61
|
+
if (updatedLayers.length === 1) {
|
|
62
|
+
return {
|
|
63
|
+
success: false,
|
|
64
|
+
updatedScenes: scenes,
|
|
65
|
+
error: "Cannot move layer - only one layer exists",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
54
68
|
newIndex = Math.max(layerIndex - 1, 0);
|
|
55
69
|
break;
|
|
56
70
|
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Single Responsibility: Handle layer position, size, and animation updates
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { Scene, Animation
|
|
7
|
-
import type { LayerOperationResult } from "../../../domain/entities";
|
|
6
|
+
import type { Scene, Animation } from "../../../domain/entities/video-project.types";
|
|
7
|
+
import type { LayerOperationResult } from "../../../domain/entities/video-project.types";
|
|
8
8
|
|
|
9
9
|
class LayerTransformService {
|
|
10
10
|
/**
|
|
@@ -26,6 +26,10 @@ class LayerTransformService {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// Validate position values (percentage: 0-100)
|
|
30
|
+
const clampedX = Math.max(0, Math.min(100, x));
|
|
31
|
+
const clampedY = Math.max(0, Math.min(100, y));
|
|
32
|
+
|
|
29
33
|
const updatedScenes = [...scenes];
|
|
30
34
|
const layerIndex = updatedScenes[sceneIndex].layers.findIndex(
|
|
31
35
|
(l) => l.id === layerId,
|
|
@@ -41,7 +45,7 @@ class LayerTransformService {
|
|
|
41
45
|
|
|
42
46
|
updatedScenes[sceneIndex].layers[layerIndex] = {
|
|
43
47
|
...updatedScenes[sceneIndex].layers[layerIndex],
|
|
44
|
-
position: { x, y },
|
|
48
|
+
position: { x: clampedX, y: clampedY },
|
|
45
49
|
};
|
|
46
50
|
|
|
47
51
|
return { success: true, updatedScenes };
|
|
@@ -76,6 +80,10 @@ class LayerTransformService {
|
|
|
76
80
|
};
|
|
77
81
|
}
|
|
78
82
|
|
|
83
|
+
// Validate size values (percentage: 1-100, minimum 1% to prevent invisible layers)
|
|
84
|
+
const clampedWidth = Math.max(1, Math.min(100, width));
|
|
85
|
+
const clampedHeight = Math.max(1, Math.min(100, height));
|
|
86
|
+
|
|
79
87
|
const updatedScenes = [...scenes];
|
|
80
88
|
const layerIndex = updatedScenes[sceneIndex].layers.findIndex(
|
|
81
89
|
(l) => l.id === layerId,
|
|
@@ -91,7 +99,7 @@ class LayerTransformService {
|
|
|
91
99
|
|
|
92
100
|
updatedScenes[sceneIndex].layers[layerIndex] = {
|
|
93
101
|
...updatedScenes[sceneIndex].layers[layerIndex],
|
|
94
|
-
size: { width, height },
|
|
102
|
+
size: { width: clampedWidth, height: clampedHeight },
|
|
95
103
|
};
|
|
96
104
|
|
|
97
105
|
return { success: true, updatedScenes };
|
|
@@ -138,10 +146,11 @@ class LayerTransformService {
|
|
|
138
146
|
};
|
|
139
147
|
}
|
|
140
148
|
|
|
149
|
+
const existingLayer = updatedScenes[sceneIndex].layers[layerIndex];
|
|
141
150
|
updatedScenes[sceneIndex].layers[layerIndex] = {
|
|
142
|
-
...
|
|
151
|
+
...existingLayer,
|
|
143
152
|
animation,
|
|
144
|
-
}
|
|
153
|
+
};
|
|
145
154
|
|
|
146
155
|
return { success: true, updatedScenes };
|
|
147
156
|
} catch (error) {
|
|
@@ -7,14 +7,14 @@ import { textLayerOperationsService } from "./text-layer-operations.service";
|
|
|
7
7
|
import { imageLayerOperationsService } from "./image-layer-operations.service";
|
|
8
8
|
import { shapeLayerOperationsService } from "./shape-layer-operations.service";
|
|
9
9
|
import { layerManipulationService } from "./layer-manipulation.service";
|
|
10
|
-
import type { Scene, TextLayer, ImageLayer, Animation } from "../../domain/entities";
|
|
10
|
+
import type { Scene, TextLayer, ImageLayer, Animation } from "../../domain/entities/video-project.types";
|
|
11
11
|
import type {
|
|
12
12
|
LayerOperationResult,
|
|
13
13
|
LayerOrderAction,
|
|
14
14
|
AddTextLayerData,
|
|
15
15
|
AddImageLayerData,
|
|
16
16
|
AddShapeLayerData,
|
|
17
|
-
} from "../../domain/entities";
|
|
17
|
+
} from "../../domain/entities/video-project.types";
|
|
18
18
|
|
|
19
19
|
class LayerOperationsService {
|
|
20
20
|
/**
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { generateUUID } from "@umituz/react-native-design-system/uuid";
|
|
7
|
-
import type { Scene, Audio } from "../../domain/entities";
|
|
8
|
-
import type { SceneOperationResult } from "../../domain/entities";
|
|
7
|
+
import type { Scene, Audio } from "../../domain/entities/video-project.types";
|
|
8
|
+
import type { SceneOperationResult } from "../../domain/entities/video-project.types";
|
|
9
9
|
|
|
10
10
|
class SceneOperationsService {
|
|
11
11
|
/**
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { generateUUID } from "@umituz/react-native-design-system/uuid";
|
|
7
|
-
import type { Scene, ShapeLayer } from "../../domain/entities";
|
|
8
|
-
import type { LayerOperationResult, AddShapeLayerData } from "../../domain/entities";
|
|
7
|
+
import type { Scene, ShapeLayer } from "../../domain/entities/video-project.types";
|
|
8
|
+
import type { LayerOperationResult, AddShapeLayerData } from "../../domain/entities/video-project.types";
|
|
9
9
|
|
|
10
10
|
class ShapeLayerOperationsService {
|
|
11
11
|
/**
|
|
@@ -29,12 +29,12 @@ class ShapeLayerOperationsService {
|
|
|
29
29
|
const newLayer: ShapeLayer = {
|
|
30
30
|
id: generateUUID(),
|
|
31
31
|
type: "shape",
|
|
32
|
-
shape: (layerData.shape as ShapeLayer["shape"]
|
|
32
|
+
shape: (layerData.shape ?? "rectangle") as ShapeLayer["shape"],
|
|
33
33
|
position: { x: 25, y: 25 },
|
|
34
34
|
size: { width: 50, height: 50 },
|
|
35
35
|
rotation: 0,
|
|
36
|
-
opacity: layerData.opacity
|
|
37
|
-
fillColor: layerData.fillColor
|
|
36
|
+
opacity: layerData.opacity ?? 1,
|
|
37
|
+
fillColor: layerData.fillColor ?? defaultColor,
|
|
38
38
|
borderColor: layerData.borderColor,
|
|
39
39
|
borderWidth: layerData.borderWidth,
|
|
40
40
|
animation: {
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { generateUUID } from "@umituz/react-native-design-system/uuid";
|
|
7
|
-
import type { Scene, TextLayer } from "../../domain/entities";
|
|
8
|
-
import type { LayerOperationResult, AddTextLayerData } from "../../domain/entities";
|
|
7
|
+
import type { Scene, TextLayer } from "../../domain/entities/video-project.types";
|
|
8
|
+
import type { LayerOperationResult, AddTextLayerData } from "../../domain/entities/video-project.types";
|
|
9
9
|
|
|
10
10
|
class TextLayerOperationsService {
|
|
11
11
|
/**
|
|
@@ -94,10 +94,19 @@ class TextLayerOperationsService {
|
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
const existingLayer = updatedScenes[sceneIndex].layers[layerIndex];
|
|
98
|
+
if (existingLayer.type !== "text") {
|
|
99
|
+
return {
|
|
100
|
+
success: false,
|
|
101
|
+
updatedScenes: scenes,
|
|
102
|
+
error: "Layer is not a text layer",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
97
106
|
updatedScenes[sceneIndex].layers[layerIndex] = {
|
|
98
|
-
...
|
|
107
|
+
...existingLayer,
|
|
99
108
|
...layerData,
|
|
100
|
-
}
|
|
109
|
+
};
|
|
101
110
|
|
|
102
111
|
return { success: true, updatedScenes };
|
|
103
112
|
} catch (error) {
|
|
@@ -13,7 +13,14 @@ function toSrtTime(seconds: number): string {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export function generateSRT(subtitles: Subtitle[]): string {
|
|
16
|
-
const
|
|
16
|
+
const validSubtitles = subtitles.filter((sub) => (
|
|
17
|
+
sub.startTime >= 0 &&
|
|
18
|
+
sub.endTime >= 0 &&
|
|
19
|
+
sub.startTime < sub.endTime &&
|
|
20
|
+
sub.text.trim().length > 0
|
|
21
|
+
));
|
|
22
|
+
|
|
23
|
+
const sorted = [...validSubtitles].sort((a, b) => a.startTime - b.startTime);
|
|
17
24
|
return sorted
|
|
18
25
|
.map((sub, index) => `${index + 1}\n${toSrtTime(sub.startTime)} --> ${toSrtTime(sub.endTime)}\n${sub.text}\n`)
|
|
19
26
|
.join("\n");
|
package/src/player/index.ts
CHANGED
|
@@ -7,7 +7,6 @@ import React, { useMemo, useCallback } from "react";
|
|
|
7
7
|
import { View, Modal, StyleSheet, StatusBar } from "react-native";
|
|
8
8
|
import { Image } from "expo-image";
|
|
9
9
|
// expo-video is optional — lazy require
|
|
10
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
10
|
let VideoView: React.ComponentType<any> = () => null;
|
|
12
11
|
try {
|
|
13
12
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -7,7 +7,6 @@ import React, { useState, useCallback, useMemo, useEffect } from "react";
|
|
|
7
7
|
import { View, TouchableOpacity, StyleSheet, type ViewStyle } from "react-native";
|
|
8
8
|
import { Image } from "expo-image";
|
|
9
9
|
// expo-video is optional — lazy require so it is not auto-installed
|
|
10
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
10
|
let VideoView: React.ComponentType<any> = () => null;
|
|
12
11
|
try {
|
|
13
12
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -93,7 +92,7 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|
|
93
92
|
// Calculate fallback dimensions only when style doesn't provide sizing
|
|
94
93
|
const customSizing = hasCustomSizing(style as ViewStyle);
|
|
95
94
|
const videoWidth = customSizing ? undefined : (screenWidth - horizontalPadding * 2);
|
|
96
|
-
const videoHeight = videoWidth ? videoWidth / DEFAULT_ASPECT_RATIO : undefined;
|
|
95
|
+
const videoHeight = videoWidth !== undefined ? videoWidth / DEFAULT_ASPECT_RATIO : undefined;
|
|
97
96
|
|
|
98
97
|
const containerStyle = useMemo(() => ({
|
|
99
98
|
...(videoWidth !== undefined && { width: videoWidth }),
|
|
@@ -104,15 +103,17 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|
|
104
103
|
}), [tokens.colors.surface, videoWidth, videoHeight]);
|
|
105
104
|
|
|
106
105
|
const styles = useMemo(() => StyleSheet.create({
|
|
107
|
-
video: videoWidth !== undefined
|
|
108
|
-
? { width: videoWidth, height: videoHeight
|
|
106
|
+
video: videoWidth !== undefined && videoHeight !== undefined
|
|
107
|
+
? { width: videoWidth, height: videoHeight }
|
|
109
108
|
: { width: "100%", height: "100%" },
|
|
110
109
|
thumbnailContainer: { flex: 1, justifyContent: "center", alignItems: "center" },
|
|
111
|
-
thumbnail: videoWidth !== undefined
|
|
112
|
-
? { width: videoWidth, height: videoHeight
|
|
110
|
+
thumbnail: videoWidth !== undefined && videoHeight !== undefined
|
|
111
|
+
? { width: videoWidth, height: videoHeight }
|
|
113
112
|
: { width: "100%", height: "100%" },
|
|
114
113
|
placeholder: {
|
|
115
|
-
...(videoWidth !== undefined
|
|
114
|
+
...(videoWidth !== undefined && videoHeight !== undefined
|
|
115
|
+
? { width: videoWidth, height: videoHeight }
|
|
116
|
+
: { flex: 1, width: "100%" }),
|
|
116
117
|
backgroundColor: tokens.colors.surfaceSecondary,
|
|
117
118
|
},
|
|
118
119
|
playButtonContainer: { ...StyleSheet.absoluteFillObject, justifyContent: "center", alignItems: "center" },
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Custom overlay controls: top bar (title, back), center play/pause, bottom bar (progress, mute)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React
|
|
6
|
+
import React from "react";
|
|
7
7
|
import { View, TouchableOpacity, StyleSheet, TouchableWithoutFeedback } from "react-native";
|
|
8
8
|
import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system/atoms";
|
|
9
9
|
|
|
@@ -14,7 +14,6 @@ const POLL_INTERVAL_MS = 250;
|
|
|
14
14
|
* Also returns the player's actual playing state to detect out-of-sync conditions
|
|
15
15
|
*/
|
|
16
16
|
export const useVideoPlaybackProgress = (
|
|
17
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
17
|
player: any,
|
|
19
18
|
isPlayerValid: boolean,
|
|
20
19
|
isPlaying: boolean,
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
|
|
6
6
|
import { useState, useCallback, useMemo } from "react";
|
|
7
7
|
// expo-video is optional — module-level lazy require with null stub
|
|
8
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
8
|
let useExpoVideoPlayer: (...args: any[]) => any = () => null;
|
|
10
9
|
try {
|
|
11
10
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -20,6 +19,11 @@ import type {
|
|
|
20
19
|
VideoPlayerControls,
|
|
21
20
|
UseVideoPlayerControlResult,
|
|
22
21
|
} from "../../types";
|
|
22
|
+
|
|
23
|
+
// Extend VideoPlayer type to include playbackRate property
|
|
24
|
+
interface ExtendedVideoPlayer {
|
|
25
|
+
playbackRate?: number;
|
|
26
|
+
}
|
|
23
27
|
import {
|
|
24
28
|
safePlay,
|
|
25
29
|
safePause,
|
|
@@ -45,7 +49,6 @@ export const useVideoPlayerControl = (
|
|
|
45
49
|
const [playbackRate, setPlaybackRateState] = useState(initialRate);
|
|
46
50
|
const [isMuted, setIsMuted] = useState(muted);
|
|
47
51
|
|
|
48
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
52
|
const player = useExpoVideoPlayer(source || "", (p: any) => {
|
|
50
53
|
if (source && p) {
|
|
51
54
|
configurePlayer(p, { loop, muted, autoPlay });
|
|
@@ -94,8 +97,7 @@ export const useVideoPlayerControl = (
|
|
|
94
97
|
|
|
95
98
|
const setPlaybackRate = useCallback((rate: number) => {
|
|
96
99
|
if (!isPlayerValid || !player) return;
|
|
97
|
-
|
|
98
|
-
(player as any).playbackRate = rate;
|
|
100
|
+
(player as ExtendedVideoPlayer).playbackRate = rate;
|
|
99
101
|
setPlaybackRateState(rate);
|
|
100
102
|
}, [player, isPlayerValid]);
|
|
101
103
|
|
|
@@ -168,5 +168,6 @@ export type { VideoPlayer } from "expo-video";
|
|
|
168
168
|
export type {
|
|
169
169
|
VideoDownloadProgressCallback,
|
|
170
170
|
VideoCacheResult,
|
|
171
|
-
} from "
|
|
172
|
-
|
|
171
|
+
} from "./infrastructure/services/video-cache.service";
|
|
172
|
+
|
|
173
|
+
export type { VideoCachingState } from "./presentation/hooks/useVideoCaching";
|