@umituz/react-native-video-editor 1.0.16 → 1.0.18
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 +13 -4
- package/src/player/index.ts +13 -0
- package/src/player/infrastructure/services/video-cache.service.ts +136 -0
- package/src/player/presentation/components/VideoPlayer.tsx +72 -80
- package/src/player/presentation/hooks/useVideoCaching.ts +104 -0
- package/src/player/types/index.ts +9 -0
- package/src/presentation/components/EditorToolPanel.tsx +3 -3
- package/src/presentation/components/LayerActionsMenu.tsx +5 -5
- package/src/presentation/components/animation-layer/AnimationEditorActions.tsx +1 -1
- package/src/presentation/components/animation-layer/AnimationInfoBanner.tsx +1 -1
- package/src/presentation/components/audio-layer/AudioEditorActions.tsx +1 -1
- package/src/presentation/components/audio-layer/AudioFileSelector.tsx +2 -2
- package/src/presentation/components/audio-layer/InfoBanner.tsx +1 -1
- package/src/presentation/components/export/ExportActions.tsx +1 -1
- package/src/presentation/components/export/ExportInfoBanner.tsx +1 -1
- package/src/presentation/components/image-layer/ImageSelectionButtons.tsx +2 -2
- package/src/presentation/components/shape-layer/ColorPickerHorizontal.tsx +1 -1
- package/src/presentation/components/text-layer/ColorPicker.tsx +1 -1
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-video-editor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.18",
|
|
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",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"typecheck": "
|
|
8
|
+
"typecheck": "npx tsc --noEmit",
|
|
9
9
|
"lint": "echo 'Lint passed'",
|
|
10
10
|
"version:patch": "npm version patch -m 'chore: release v%s'",
|
|
11
11
|
"version:minor": "npm version minor -m 'chore: release v%s'",
|
|
@@ -26,22 +26,31 @@
|
|
|
26
26
|
"url": "https://github.com/umituz/react-native-video-editor"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
|
-
"react": ">=18.2.0",
|
|
30
|
-
"react-native": ">=0.74.0",
|
|
31
29
|
"@umituz/react-native-design-system": ">=1.0.0",
|
|
32
30
|
"expo-document-picker": ">=14.0.0",
|
|
31
|
+
"expo-file-system": ">=17.0.0",
|
|
33
32
|
"expo-image": ">=1.0.0",
|
|
34
33
|
"expo-video": ">=3.0.0",
|
|
34
|
+
"react": ">=18.2.0",
|
|
35
|
+
"react-native": ">=0.74.0",
|
|
35
36
|
"zustand": ">=4.0.0"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
39
|
+
"@gorhom/bottom-sheet": "^5.2.8",
|
|
38
40
|
"@types/react": "~19.1.10",
|
|
39
41
|
"@umituz/react-native-design-system": "latest",
|
|
42
|
+
"@umituz/react-native-filesystem": "latest",
|
|
43
|
+
"expo-application": "^7.0.8",
|
|
44
|
+
"expo-device": "^8.0.10",
|
|
40
45
|
"expo-document-picker": "^14.0.8",
|
|
46
|
+
"expo-file-system": "^19.0.0",
|
|
41
47
|
"expo-image": "^3.0.11",
|
|
48
|
+
"expo-linear-gradient": "^15.0.7",
|
|
42
49
|
"expo-video": "^3.0.15",
|
|
43
50
|
"react": "19.1.0",
|
|
44
51
|
"react-native": "0.81.5",
|
|
52
|
+
"react-native-gesture-handler": "^2.30.0",
|
|
53
|
+
"react-native-reanimated": "^4.2.1",
|
|
45
54
|
"typescript": "~5.9.2"
|
|
46
55
|
},
|
|
47
56
|
"publishConfig": {
|
package/src/player/index.ts
CHANGED
|
@@ -12,6 +12,9 @@ export type {
|
|
|
12
12
|
VideoVisibilityConfig,
|
|
13
13
|
VideoPlayerProps,
|
|
14
14
|
VideoPlayer as VideoPlayerType,
|
|
15
|
+
VideoDownloadProgressCallback,
|
|
16
|
+
VideoCacheResult,
|
|
17
|
+
VideoCachingState,
|
|
15
18
|
} from "./types";
|
|
16
19
|
|
|
17
20
|
// Services
|
|
@@ -23,9 +26,19 @@ export {
|
|
|
23
26
|
configurePlayer,
|
|
24
27
|
} from "./infrastructure/services/player-control.service";
|
|
25
28
|
|
|
29
|
+
export {
|
|
30
|
+
isVideoCached,
|
|
31
|
+
getCachedVideoUri,
|
|
32
|
+
downloadVideo,
|
|
33
|
+
getOrDownloadVideo,
|
|
34
|
+
clearVideoCache,
|
|
35
|
+
deleteSpecificCachedVideo,
|
|
36
|
+
} from "./infrastructure/services/video-cache.service";
|
|
37
|
+
|
|
26
38
|
// Hooks
|
|
27
39
|
export { useVideoPlayerControl } from "./presentation/hooks/useVideoPlayerControl";
|
|
28
40
|
export { useVideoVisibility } from "./presentation/hooks/useVideoVisibility";
|
|
41
|
+
export { useVideoCaching } from "./presentation/hooks/useVideoCaching";
|
|
29
42
|
|
|
30
43
|
// Components
|
|
31
44
|
export { VideoPlayer } from "./presentation/components/VideoPlayer";
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Cache Service
|
|
3
|
+
* Downloads and caches remote videos for playback
|
|
4
|
+
* Bypasses CDN Range request issues by downloading entire file first
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
downloadFileWithProgress,
|
|
9
|
+
getCachedFileUri,
|
|
10
|
+
isUrlCached,
|
|
11
|
+
deleteCachedFile,
|
|
12
|
+
getCacheDirectory,
|
|
13
|
+
clearCache,
|
|
14
|
+
type DownloadProgressCallback,
|
|
15
|
+
type DownloadProgress,
|
|
16
|
+
} from "@umituz/react-native-filesystem";
|
|
17
|
+
|
|
18
|
+
declare const __DEV__: boolean;
|
|
19
|
+
|
|
20
|
+
const VIDEO_CACHE_SUBDIR = "video-cache";
|
|
21
|
+
|
|
22
|
+
/** Cache result type */
|
|
23
|
+
export interface VideoCacheResult {
|
|
24
|
+
readonly uri: string;
|
|
25
|
+
readonly fromCache: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Re-export progress callback type */
|
|
29
|
+
export type VideoDownloadProgressCallback = (progress: number) => void;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get video cache directory path
|
|
33
|
+
*/
|
|
34
|
+
const getVideoCacheDir = (): string => {
|
|
35
|
+
const cacheDir = getCacheDirectory();
|
|
36
|
+
return `${cacheDir}${VIDEO_CACHE_SUBDIR}/`;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if video is already cached
|
|
41
|
+
*/
|
|
42
|
+
export const isVideoCached = (url: string): boolean => {
|
|
43
|
+
if (!url || url.startsWith("file://")) return false;
|
|
44
|
+
return isUrlCached(url, getVideoCacheDir());
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get cached video URI if exists
|
|
49
|
+
*/
|
|
50
|
+
export const getCachedVideoUri = (url: string): string | null => {
|
|
51
|
+
if (!url) return null;
|
|
52
|
+
if (url.startsWith("file://")) return url;
|
|
53
|
+
return getCachedFileUri(url, getVideoCacheDir());
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Download video to cache with progress tracking
|
|
58
|
+
*/
|
|
59
|
+
export const downloadVideo = async (
|
|
60
|
+
url: string,
|
|
61
|
+
onProgress?: VideoDownloadProgressCallback,
|
|
62
|
+
): Promise<string> => {
|
|
63
|
+
if (!url) throw new Error("URL is required");
|
|
64
|
+
if (url.startsWith("file://")) return url;
|
|
65
|
+
|
|
66
|
+
if (__DEV__) {
|
|
67
|
+
console.log("[VideoCache] Downloading:", url);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const progressCallback: DownloadProgressCallback | undefined = onProgress
|
|
71
|
+
? (progress: DownloadProgress) => {
|
|
72
|
+
const percent = progress.totalBytesExpectedToWrite > 0
|
|
73
|
+
? progress.totalBytesWritten / progress.totalBytesExpectedToWrite
|
|
74
|
+
: 0;
|
|
75
|
+
onProgress(percent);
|
|
76
|
+
}
|
|
77
|
+
: undefined;
|
|
78
|
+
|
|
79
|
+
const result = await downloadFileWithProgress(
|
|
80
|
+
url,
|
|
81
|
+
getVideoCacheDir(),
|
|
82
|
+
progressCallback,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (!result.success || !result.uri) {
|
|
86
|
+
throw new Error(result.error || "Download failed");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (__DEV__) {
|
|
90
|
+
console.log("[VideoCache] Download complete:", result.uri);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return result.uri;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get video from cache or download if not cached
|
|
98
|
+
*/
|
|
99
|
+
export const getOrDownloadVideo = async (
|
|
100
|
+
url: string,
|
|
101
|
+
onProgress?: VideoDownloadProgressCallback,
|
|
102
|
+
): Promise<VideoCacheResult> => {
|
|
103
|
+
if (!url) throw new Error("URL is required");
|
|
104
|
+
|
|
105
|
+
if (url.startsWith("file://")) {
|
|
106
|
+
return { uri: url, fromCache: true };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check cache first
|
|
110
|
+
const cachedUri = getCachedVideoUri(url);
|
|
111
|
+
if (cachedUri) {
|
|
112
|
+
if (__DEV__) {
|
|
113
|
+
console.log("[VideoCache] Using cached video:", cachedUri);
|
|
114
|
+
}
|
|
115
|
+
return { uri: cachedUri, fromCache: true };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Download if not cached
|
|
119
|
+
const downloadedUri = await downloadVideo(url, onProgress);
|
|
120
|
+
return { uri: downloadedUri, fromCache: false };
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Clear all cached videos
|
|
125
|
+
*/
|
|
126
|
+
export const clearVideoCache = async (): Promise<void> => {
|
|
127
|
+
await clearCache();
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Delete a specific cached video
|
|
132
|
+
*/
|
|
133
|
+
export const deleteSpecificCachedVideo = (url: string): boolean => {
|
|
134
|
+
if (!url || url.startsWith("file://")) return false;
|
|
135
|
+
return deleteCachedFile(url, getVideoCacheDir());
|
|
136
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* VideoPlayer Component
|
|
3
|
-
* Reusable video player with thumbnail and controls
|
|
3
|
+
* Reusable video player with caching, thumbnail and controls
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import React, { useState, useCallback, useMemo, useEffect } from "react";
|
|
@@ -11,10 +11,12 @@ import {
|
|
|
11
11
|
useResponsive,
|
|
12
12
|
useAppDesignTokens,
|
|
13
13
|
AtomicIcon,
|
|
14
|
+
AtomicText,
|
|
14
15
|
} from "@umituz/react-native-design-system";
|
|
15
16
|
|
|
16
17
|
import type { VideoPlayerProps } from "../../types";
|
|
17
18
|
import { useVideoPlayerControl } from "../hooks/useVideoPlayerControl";
|
|
19
|
+
import { useVideoCaching } from "../hooks/useVideoCaching";
|
|
18
20
|
|
|
19
21
|
declare const __DEV__: boolean;
|
|
20
22
|
|
|
@@ -38,14 +40,17 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|
|
38
40
|
contentFit = "cover",
|
|
39
41
|
style,
|
|
40
42
|
}) => {
|
|
41
|
-
// IMPORTANT: Call useResponsive BEFORE useAppDesignTokens to maintain
|
|
42
|
-
// useAppDesignTokens internally calls useResponsive, so order matters
|
|
43
|
+
// IMPORTANT: Call useResponsive BEFORE useAppDesignTokens to maintain hook order
|
|
43
44
|
const { width: screenWidth, horizontalPadding } = useResponsive();
|
|
44
45
|
const tokens = useAppDesignTokens();
|
|
45
46
|
const [showVideo, setShowVideo] = useState(autoPlay);
|
|
46
47
|
|
|
48
|
+
// Cache the video first (downloads if needed)
|
|
49
|
+
const { localUri, isDownloading, downloadProgress, error } = useVideoCaching(source);
|
|
50
|
+
|
|
51
|
+
// Use cached local URI for player
|
|
47
52
|
const { player, state, controls } = useVideoPlayerControl({
|
|
48
|
-
source,
|
|
53
|
+
source: localUri,
|
|
49
54
|
loop,
|
|
50
55
|
muted,
|
|
51
56
|
autoPlay: false,
|
|
@@ -53,23 +58,21 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|
|
53
58
|
|
|
54
59
|
useEffect(() => {
|
|
55
60
|
if (showVideo && state.isPlayerValid && player) {
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
console.log("[VideoPlayer] Starting playback...");
|
|
61
|
+
if (__DEV__) {
|
|
62
|
+
console.log("[VideoPlayer] Starting playback from:", localUri);
|
|
59
63
|
}
|
|
60
64
|
controls.play();
|
|
61
65
|
}
|
|
62
|
-
}, [showVideo, state.isPlayerValid, player, controls]);
|
|
66
|
+
}, [showVideo, state.isPlayerValid, player, controls, localUri]);
|
|
63
67
|
|
|
64
68
|
const handlePlay = useCallback(() => {
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
console.log("[VideoPlayer] handlePlay called, source:", source);
|
|
69
|
+
if (__DEV__) {
|
|
70
|
+
console.log("[VideoPlayer] handlePlay, localUri:", localUri);
|
|
68
71
|
}
|
|
69
72
|
setShowVideo(true);
|
|
70
|
-
}, [
|
|
73
|
+
}, [localUri]);
|
|
71
74
|
|
|
72
|
-
// Calculate
|
|
75
|
+
// Calculate dimensions
|
|
73
76
|
const videoWidth = getWidthFromStyle(style as ViewStyle) ?? (screenWidth - horizontalPadding * 2);
|
|
74
77
|
const videoHeight = videoWidth / ASPECT_RATIO;
|
|
75
78
|
|
|
@@ -81,67 +84,63 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|
|
81
84
|
overflow: "hidden" as const,
|
|
82
85
|
}), [tokens.colors.surface, videoWidth, videoHeight]);
|
|
83
86
|
|
|
84
|
-
const styles = useMemo(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
backgroundColor: tokens.colors.surfaceSecondary,
|
|
104
|
-
},
|
|
105
|
-
playButtonContainer: {
|
|
106
|
-
...StyleSheet.absoluteFillObject,
|
|
107
|
-
justifyContent: "center",
|
|
108
|
-
alignItems: "center",
|
|
109
|
-
},
|
|
110
|
-
playButton: {
|
|
111
|
-
width: 64,
|
|
112
|
-
height: 64,
|
|
113
|
-
borderRadius: 32,
|
|
114
|
-
backgroundColor: tokens.colors.primary,
|
|
115
|
-
justifyContent: "center",
|
|
116
|
-
alignItems: "center",
|
|
117
|
-
paddingLeft: 4,
|
|
118
|
-
},
|
|
119
|
-
}),
|
|
120
|
-
[tokens, videoWidth, videoHeight]
|
|
121
|
-
);
|
|
87
|
+
const styles = useMemo(() => StyleSheet.create({
|
|
88
|
+
video: { width: videoWidth, height: videoHeight },
|
|
89
|
+
thumbnailContainer: { flex: 1, justifyContent: "center", alignItems: "center" },
|
|
90
|
+
thumbnail: { width: videoWidth, height: videoHeight },
|
|
91
|
+
placeholder: { width: videoWidth, height: videoHeight, backgroundColor: tokens.colors.surfaceSecondary },
|
|
92
|
+
playButtonContainer: { ...StyleSheet.absoluteFillObject, justifyContent: "center", alignItems: "center" },
|
|
93
|
+
playButton: {
|
|
94
|
+
width: 64, height: 64, borderRadius: 32,
|
|
95
|
+
backgroundColor: tokens.colors.primary,
|
|
96
|
+
justifyContent: "center", alignItems: "center", paddingLeft: 4,
|
|
97
|
+
},
|
|
98
|
+
progressContainer: {
|
|
99
|
+
...StyleSheet.absoluteFillObject,
|
|
100
|
+
justifyContent: "center", alignItems: "center",
|
|
101
|
+
backgroundColor: "rgba(0,0,0,0.5)",
|
|
102
|
+
},
|
|
103
|
+
progressText: { color: "#fff", fontSize: 16, fontWeight: "600" },
|
|
104
|
+
errorText: { color: tokens.colors.error, fontSize: 14, textAlign: "center", padding: 16 },
|
|
105
|
+
}), [tokens, videoWidth, videoHeight]);
|
|
122
106
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
107
|
+
// Show error state
|
|
108
|
+
if (error) {
|
|
109
|
+
return (
|
|
110
|
+
<View style={[containerStyle, style]}>
|
|
111
|
+
<View style={styles.thumbnailContainer}>
|
|
112
|
+
<View style={styles.placeholder} />
|
|
113
|
+
<View style={styles.progressContainer}>
|
|
114
|
+
<AtomicText style={styles.errorText}>{error}</AtomicText>
|
|
115
|
+
</View>
|
|
116
|
+
</View>
|
|
117
|
+
</View>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Show download progress
|
|
122
|
+
if (isDownloading) {
|
|
123
|
+
const progressPercent = Math.round(downloadProgress * 100);
|
|
124
|
+
return (
|
|
125
|
+
<View style={[containerStyle, style]}>
|
|
126
|
+
<View style={styles.thumbnailContainer}>
|
|
127
|
+
{thumbnailUrl ? (
|
|
128
|
+
<Image source={{ uri: thumbnailUrl }} style={styles.thumbnail} contentFit="cover" />
|
|
129
|
+
) : (
|
|
130
|
+
<View style={styles.placeholder} />
|
|
131
|
+
)}
|
|
132
|
+
<View style={styles.progressContainer}>
|
|
133
|
+
<AtomicText style={styles.progressText}>{progressPercent}%</AtomicText>
|
|
134
|
+
</View>
|
|
135
|
+
</View>
|
|
136
|
+
</View>
|
|
137
|
+
);
|
|
139
138
|
}
|
|
140
139
|
|
|
140
|
+
// Show video player
|
|
141
141
|
if (showVideo && state.isPlayerValid && player) {
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
console.log("[VideoPlayer] Rendering VideoView with dimensions:", { videoWidth, videoHeight });
|
|
142
|
+
if (__DEV__) {
|
|
143
|
+
console.log("[VideoPlayer] Rendering VideoView:", { videoWidth, videoHeight });
|
|
145
144
|
}
|
|
146
145
|
return (
|
|
147
146
|
<View style={[containerStyle, style]}>
|
|
@@ -155,19 +154,12 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|
|
155
154
|
);
|
|
156
155
|
}
|
|
157
156
|
|
|
157
|
+
// Show thumbnail with play button
|
|
158
158
|
return (
|
|
159
|
-
<TouchableOpacity
|
|
160
|
-
style={[containerStyle, style]}
|
|
161
|
-
onPress={handlePlay}
|
|
162
|
-
activeOpacity={0.8}
|
|
163
|
-
>
|
|
159
|
+
<TouchableOpacity style={[containerStyle, style]} onPress={handlePlay} activeOpacity={0.8}>
|
|
164
160
|
<View style={styles.thumbnailContainer}>
|
|
165
161
|
{thumbnailUrl ? (
|
|
166
|
-
<Image
|
|
167
|
-
source={{ uri: thumbnailUrl }}
|
|
168
|
-
style={styles.thumbnail}
|
|
169
|
-
contentFit="cover"
|
|
170
|
-
/>
|
|
162
|
+
<Image source={{ uri: thumbnailUrl }} style={styles.thumbnail} contentFit="cover" />
|
|
171
163
|
) : (
|
|
172
164
|
<View style={styles.placeholder} />
|
|
173
165
|
)}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useVideoCaching Hook
|
|
3
|
+
* Manages video caching state and download progress
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useEffect, useCallback } from "react";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
getOrDownloadVideo,
|
|
10
|
+
isVideoCached,
|
|
11
|
+
type VideoCacheResult,
|
|
12
|
+
} from "../../infrastructure/services/video-cache.service";
|
|
13
|
+
|
|
14
|
+
declare const __DEV__: boolean;
|
|
15
|
+
|
|
16
|
+
/** Video caching state */
|
|
17
|
+
export interface VideoCachingState {
|
|
18
|
+
readonly localUri: string | null;
|
|
19
|
+
readonly isDownloading: boolean;
|
|
20
|
+
readonly downloadProgress: number;
|
|
21
|
+
readonly fromCache: boolean;
|
|
22
|
+
readonly error: string | null;
|
|
23
|
+
readonly retry: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Hook for managing video caching
|
|
28
|
+
* Downloads remote videos to local cache for playback
|
|
29
|
+
*/
|
|
30
|
+
export const useVideoCaching = (sourceUrl: string | null): VideoCachingState => {
|
|
31
|
+
const [localUri, setLocalUri] = useState<string | null>(null);
|
|
32
|
+
const [isDownloading, setIsDownloading] = useState(false);
|
|
33
|
+
const [downloadProgress, setDownloadProgress] = useState(0);
|
|
34
|
+
const [fromCache, setFromCache] = useState(false);
|
|
35
|
+
const [error, setError] = useState<string | null>(null);
|
|
36
|
+
|
|
37
|
+
const loadVideo = useCallback(async () => {
|
|
38
|
+
if (!sourceUrl) {
|
|
39
|
+
setLocalUri(null);
|
|
40
|
+
setIsDownloading(false);
|
|
41
|
+
setDownloadProgress(0);
|
|
42
|
+
setFromCache(false);
|
|
43
|
+
setError(null);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (sourceUrl.startsWith("file://")) {
|
|
48
|
+
setLocalUri(sourceUrl);
|
|
49
|
+
setIsDownloading(false);
|
|
50
|
+
setDownloadProgress(1);
|
|
51
|
+
setFromCache(true);
|
|
52
|
+
setError(null);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const cached = isVideoCached(sourceUrl);
|
|
57
|
+
setIsDownloading(!cached);
|
|
58
|
+
setDownloadProgress(cached ? 1 : 0);
|
|
59
|
+
setError(null);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const result: VideoCacheResult = await getOrDownloadVideo(
|
|
63
|
+
sourceUrl,
|
|
64
|
+
(progress) => setDownloadProgress(progress),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
setLocalUri(result.uri);
|
|
68
|
+
setFromCache(result.fromCache);
|
|
69
|
+
setIsDownloading(false);
|
|
70
|
+
setDownloadProgress(1);
|
|
71
|
+
|
|
72
|
+
if (__DEV__) {
|
|
73
|
+
console.log("[useVideoCaching] Video ready:", result.uri);
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
const message = err instanceof Error ? err.message : "Download failed";
|
|
77
|
+
setError(message);
|
|
78
|
+
setIsDownloading(false);
|
|
79
|
+
setLocalUri(null);
|
|
80
|
+
|
|
81
|
+
if (__DEV__) {
|
|
82
|
+
console.log("[useVideoCaching] Error:", message);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}, [sourceUrl]);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
loadVideo();
|
|
89
|
+
}, [loadVideo]);
|
|
90
|
+
|
|
91
|
+
const retry = useCallback(() => {
|
|
92
|
+
setError(null);
|
|
93
|
+
loadVideo();
|
|
94
|
+
}, [loadVideo]);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
localUri,
|
|
98
|
+
isDownloading,
|
|
99
|
+
downloadProgress,
|
|
100
|
+
fromCache,
|
|
101
|
+
error,
|
|
102
|
+
retry,
|
|
103
|
+
};
|
|
104
|
+
};
|
|
@@ -72,3 +72,12 @@ export interface VideoPlayerProps {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
export type { VideoPlayer } from "expo-video";
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Video caching types
|
|
78
|
+
*/
|
|
79
|
+
export type {
|
|
80
|
+
VideoDownloadProgressCallback,
|
|
81
|
+
VideoCacheResult,
|
|
82
|
+
} from "../infrastructure/services/video-cache.service";
|
|
83
|
+
export type { VideoCachingState } from "../presentation/hooks/useVideoCaching";
|
|
@@ -60,7 +60,7 @@ export const EditorToolPanel: React.FC<EditorToolPanelProps> = ({
|
|
|
60
60
|
]}
|
|
61
61
|
onPress={onAddText}
|
|
62
62
|
>
|
|
63
|
-
<AtomicIcon name="
|
|
63
|
+
<AtomicIcon name="text-outline" size="md" color="primary" />
|
|
64
64
|
<AtomicText
|
|
65
65
|
type="labelSmall"
|
|
66
66
|
style={{ color: tokens.colors.textPrimary, marginTop: 4 }}
|
|
@@ -92,7 +92,7 @@ export const EditorToolPanel: React.FC<EditorToolPanelProps> = ({
|
|
|
92
92
|
]}
|
|
93
93
|
onPress={onAddShape}
|
|
94
94
|
>
|
|
95
|
-
<AtomicIcon name="
|
|
95
|
+
<AtomicIcon name="square-outline" size="md" color="primary" />
|
|
96
96
|
<AtomicText
|
|
97
97
|
type="labelSmall"
|
|
98
98
|
style={{ color: tokens.colors.textPrimary, marginTop: 4 }}
|
|
@@ -108,7 +108,7 @@ export const EditorToolPanel: React.FC<EditorToolPanelProps> = ({
|
|
|
108
108
|
]}
|
|
109
109
|
onPress={onAudio}
|
|
110
110
|
>
|
|
111
|
-
<AtomicIcon name="
|
|
111
|
+
<AtomicIcon name="musical-notes-outline" size="md" color="primary" />
|
|
112
112
|
<AtomicText
|
|
113
113
|
type="labelSmall"
|
|
114
114
|
style={{ color: tokens.colors.textPrimary, marginTop: 4 }}
|
|
@@ -43,7 +43,7 @@ export const LayerActionsMenu: React.FC<LayerActionsMenuProps> = ({
|
|
|
43
43
|
<View style={{ paddingVertical: 8 }}>
|
|
44
44
|
{layer.type === "text" && (
|
|
45
45
|
<TouchableOpacity style={styles.actionMenuItem} onPress={onEditText}>
|
|
46
|
-
<AtomicIcon name="
|
|
46
|
+
<AtomicIcon name="create-outline" size="md" color="primary" />
|
|
47
47
|
<AtomicText
|
|
48
48
|
type="bodyMedium"
|
|
49
49
|
style={{
|
|
@@ -57,7 +57,7 @@ export const LayerActionsMenu: React.FC<LayerActionsMenuProps> = ({
|
|
|
57
57
|
)}
|
|
58
58
|
{layer.type === "image" && (
|
|
59
59
|
<TouchableOpacity style={styles.actionMenuItem} onPress={onEditImage}>
|
|
60
|
-
<AtomicIcon name="
|
|
60
|
+
<AtomicIcon name="create-outline" size="md" color="primary" />
|
|
61
61
|
<AtomicText
|
|
62
62
|
type="bodyMedium"
|
|
63
63
|
style={{
|
|
@@ -107,7 +107,7 @@ export const LayerActionsMenu: React.FC<LayerActionsMenuProps> = ({
|
|
|
107
107
|
<View style={styles.divider} />
|
|
108
108
|
|
|
109
109
|
<TouchableOpacity style={styles.actionMenuItem} onPress={onMoveFront}>
|
|
110
|
-
<AtomicIcon name="
|
|
110
|
+
<AtomicIcon name="chevron-up-outline" size="md" color="secondary" />
|
|
111
111
|
<AtomicText
|
|
112
112
|
type="bodyMedium"
|
|
113
113
|
style={{
|
|
@@ -120,7 +120,7 @@ export const LayerActionsMenu: React.FC<LayerActionsMenuProps> = ({
|
|
|
120
120
|
</TouchableOpacity>
|
|
121
121
|
|
|
122
122
|
<TouchableOpacity style={styles.actionMenuItem} onPress={onMoveUp}>
|
|
123
|
-
<AtomicIcon name="
|
|
123
|
+
<AtomicIcon name="chevron-up-outline" size="md" color="secondary" />
|
|
124
124
|
<AtomicText
|
|
125
125
|
type="bodyMedium"
|
|
126
126
|
style={{
|
|
@@ -146,7 +146,7 @@ export const LayerActionsMenu: React.FC<LayerActionsMenuProps> = ({
|
|
|
146
146
|
</TouchableOpacity>
|
|
147
147
|
|
|
148
148
|
<TouchableOpacity style={styles.actionMenuItem} onPress={onMoveBack}>
|
|
149
|
-
<AtomicIcon name="
|
|
149
|
+
<AtomicIcon name="chevron-down-outline" size="md" color="secondary" />
|
|
150
150
|
<AtomicText
|
|
151
151
|
type="bodyMedium"
|
|
152
152
|
style={{
|
|
@@ -65,7 +65,7 @@ export const AnimationEditorActions: React.FC<AnimationEditorActionsProps> = ({
|
|
|
65
65
|
]}
|
|
66
66
|
onPress={onSave}
|
|
67
67
|
>
|
|
68
|
-
<AtomicIcon name="
|
|
68
|
+
<AtomicIcon name="checkmark-outline" size="sm" color="onSurface" />
|
|
69
69
|
<AtomicText
|
|
70
70
|
type="bodyMedium"
|
|
71
71
|
style={{ color: "#FFFFFF", fontWeight: "600", marginLeft: 6 }}
|
|
@@ -21,7 +21,7 @@ export const AnimationInfoBanner: React.FC = () => {
|
|
|
21
21
|
{ backgroundColor: tokens.colors.primary + "20" },
|
|
22
22
|
]}
|
|
23
23
|
>
|
|
24
|
-
<AtomicIcon name="
|
|
24
|
+
<AtomicIcon name="information-circle-outline" size="sm" color="primary" />
|
|
25
25
|
<AtomicText
|
|
26
26
|
type="labelSmall"
|
|
27
27
|
style={{ color: tokens.colors.primary, marginLeft: 8, flex: 1 }}
|
|
@@ -78,7 +78,7 @@ export const AudioEditorActions: React.FC<AudioEditorActionsProps> = ({
|
|
|
78
78
|
onPress={onSave}
|
|
79
79
|
disabled={!isValid}
|
|
80
80
|
>
|
|
81
|
-
<AtomicIcon name="
|
|
81
|
+
<AtomicIcon name="checkmark-outline" size="sm" color="onSurface" />
|
|
82
82
|
<AtomicText
|
|
83
83
|
type="bodyMedium"
|
|
84
84
|
style={{ color: "#FFFFFF", fontWeight: "600", marginLeft: 6 }}
|
|
@@ -31,7 +31,7 @@ export const AudioFileSelector: React.FC<AudioFileSelectorProps> = ({
|
|
|
31
31
|
style={[styles.fileCard, { backgroundColor: tokens.colors.surface }]}
|
|
32
32
|
>
|
|
33
33
|
<View style={styles.fileInfo}>
|
|
34
|
-
<AtomicIcon name="
|
|
34
|
+
<AtomicIcon name="musical-notes-outline" size="md" color="primary" />
|
|
35
35
|
<View style={{ marginLeft: 12, flex: 1 }}>
|
|
36
36
|
<AtomicText
|
|
37
37
|
type="bodySmall"
|
|
@@ -71,7 +71,7 @@ export const AudioFileSelector: React.FC<AudioFileSelectorProps> = ({
|
|
|
71
71
|
style={[styles.pickButton, { backgroundColor: tokens.colors.surface }]}
|
|
72
72
|
onPress={onPickAudio}
|
|
73
73
|
>
|
|
74
|
-
<AtomicIcon name="
|
|
74
|
+
<AtomicIcon name="cloud-upload-outline" size="md" color="primary" />
|
|
75
75
|
<AtomicText
|
|
76
76
|
type="bodyMedium"
|
|
77
77
|
style={{
|
|
@@ -21,7 +21,7 @@ export const InfoBanner: React.FC = () => {
|
|
|
21
21
|
{ backgroundColor: tokens.colors.primary + "20" },
|
|
22
22
|
]}
|
|
23
23
|
>
|
|
24
|
-
<AtomicIcon name="
|
|
24
|
+
<AtomicIcon name="information-circle-outline" size="sm" color="primary" />
|
|
25
25
|
<AtomicText
|
|
26
26
|
type="labelSmall"
|
|
27
27
|
style={{ color: tokens.colors.primary, marginLeft: 8, flex: 1 }}
|
|
@@ -66,7 +66,7 @@ export const ExportActions: React.FC<ExportActionsProps> = ({
|
|
|
66
66
|
{isExporting ? (
|
|
67
67
|
<AtomicSpinner size="sm" color="white" />
|
|
68
68
|
) : (
|
|
69
|
-
<AtomicIcon name="
|
|
69
|
+
<AtomicIcon name="download-outline" size="sm" color="onSurface" />
|
|
70
70
|
)}
|
|
71
71
|
<AtomicText
|
|
72
72
|
type="bodyMedium"
|
|
@@ -21,7 +21,7 @@ export const ExportInfoBanner: React.FC = () => {
|
|
|
21
21
|
{ backgroundColor: tokens.colors.primary + "20" },
|
|
22
22
|
]}
|
|
23
23
|
>
|
|
24
|
-
<AtomicIcon name="
|
|
24
|
+
<AtomicIcon name="information-circle-outline" size="sm" color="primary" />
|
|
25
25
|
<AtomicText
|
|
26
26
|
type="labelSmall"
|
|
27
27
|
style={{ color: tokens.colors.primary, marginLeft: 8, flex: 1 }}
|
|
@@ -34,7 +34,7 @@ export const ImageSelectionButtons: React.FC<ImageSelectionButtonsProps> = ({
|
|
|
34
34
|
]}
|
|
35
35
|
onPress={onPickFromGallery}
|
|
36
36
|
>
|
|
37
|
-
<AtomicIcon name="
|
|
37
|
+
<AtomicIcon name="folder-open-outline" size="md" color="primary" />
|
|
38
38
|
<AtomicText
|
|
39
39
|
type="bodySmall"
|
|
40
40
|
style={{ color: tokens.colors.textPrimary, marginTop: 8 }}
|
|
@@ -53,7 +53,7 @@ export const ImageSelectionButtons: React.FC<ImageSelectionButtonsProps> = ({
|
|
|
53
53
|
]}
|
|
54
54
|
onPress={onTakePhoto}
|
|
55
55
|
>
|
|
56
|
-
<AtomicIcon name="
|
|
56
|
+
<AtomicIcon name="camera-outline" size="md" color="primary" />
|
|
57
57
|
<AtomicText
|
|
58
58
|
type="bodySmall"
|
|
59
59
|
style={{ color: tokens.colors.textPrimary, marginTop: 8 }}
|
|
@@ -61,7 +61,7 @@ export const ColorPickerHorizontal: React.FC<ColorPickerHorizontalProps> = ({
|
|
|
61
61
|
>
|
|
62
62
|
{selectedColor === color.value && (
|
|
63
63
|
<AtomicIcon
|
|
64
|
-
name="
|
|
64
|
+
name="checkmark-outline"
|
|
65
65
|
size="sm"
|
|
66
66
|
color={color.value === "#FFFFFF" ? "primary" : "onSurface"}
|
|
67
67
|
/>
|