@umituz/react-native-ai-generation-content 1.17.109 → 1.17.112
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/domains/creations/infrastructure/repositories/CreationsWriter.ts +8 -0
- package/src/domains/creations/presentation/components/CreationImagePreview.tsx +124 -0
- package/src/domains/creations/presentation/components/CreationPreview.tsx +39 -85
- package/src/domains/creations/presentation/components/CreationVideoPreview.tsx +149 -0
- package/src/domains/creations/presentation/components/index.ts +2 -0
- package/src/features/text-to-video/domain/types/callback.types.ts +11 -1
- package/src/features/text-to-video/domain/types/index.ts +1 -0
- package/src/features/text-to-video/index.ts +1 -0
- package/src/features/text-to-video/presentation/hooks/useTextToVideoFeature.ts +16 -2
- package/src/presentation/components/AIGenerationForm.tsx +5 -1
- package/src/presentation/components/AIGenerationForm.types.ts +6 -0
- package/src/presentation/components/GenerationProgressContent.tsx +53 -0
- package/src/presentation/components/GenerationProgressModal.tsx +6 -0
package/package.json
CHANGED
|
@@ -23,6 +23,8 @@ export class CreationsWriter {
|
|
|
23
23
|
metadata: creation.metadata || {},
|
|
24
24
|
isShared: creation.isShared || false,
|
|
25
25
|
isFavorite: creation.isFavorite || false,
|
|
26
|
+
status: creation.status,
|
|
27
|
+
output: creation.output,
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
await setDoc(docRef, data);
|
|
@@ -59,6 +61,12 @@ export class CreationsWriter {
|
|
|
59
61
|
if (updates.prompt !== undefined) {
|
|
60
62
|
updateData.prompt = updates.prompt;
|
|
61
63
|
}
|
|
64
|
+
if (updates.status !== undefined) {
|
|
65
|
+
updateData.status = updates.status;
|
|
66
|
+
}
|
|
67
|
+
if (updates.output !== undefined) {
|
|
68
|
+
updateData.output = updates.output;
|
|
69
|
+
}
|
|
62
70
|
|
|
63
71
|
await updateDoc(docRef, updateData);
|
|
64
72
|
return true;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CreationImagePreview Component
|
|
3
|
+
* Displays image preview with loading/placeholder states
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo, useState } from "react";
|
|
7
|
+
import { View, StyleSheet, Image, ImageErrorEventData, NativeSyntheticEvent } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
useAppDesignTokens,
|
|
10
|
+
AtomicIcon,
|
|
11
|
+
AtomicSpinner,
|
|
12
|
+
} from "@umituz/react-native-design-system";
|
|
13
|
+
import type { CreationStatus, CreationTypeId } from "../../domain/types";
|
|
14
|
+
import { isInProgress, getTypeIcon } from "../../domain/utils";
|
|
15
|
+
|
|
16
|
+
export interface CreationImagePreviewProps {
|
|
17
|
+
/** Preview image URL */
|
|
18
|
+
readonly uri?: string | null;
|
|
19
|
+
/** Creation status */
|
|
20
|
+
readonly status?: CreationStatus;
|
|
21
|
+
/** Creation type for placeholder icon */
|
|
22
|
+
readonly type?: CreationTypeId;
|
|
23
|
+
/** Aspect ratio (default: 16/9) */
|
|
24
|
+
readonly aspectRatio?: number;
|
|
25
|
+
/** Custom height (overrides aspectRatio) */
|
|
26
|
+
readonly height?: number;
|
|
27
|
+
/** Show loading indicator when in progress */
|
|
28
|
+
readonly showLoadingIndicator?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function CreationImagePreview({
|
|
32
|
+
uri,
|
|
33
|
+
status = "completed",
|
|
34
|
+
type = "text-to-image",
|
|
35
|
+
aspectRatio = 16 / 9,
|
|
36
|
+
height,
|
|
37
|
+
showLoadingIndicator = true,
|
|
38
|
+
}: CreationImagePreviewProps) {
|
|
39
|
+
const tokens = useAppDesignTokens();
|
|
40
|
+
const inProgress = isInProgress(status);
|
|
41
|
+
const typeIcon = getTypeIcon(type);
|
|
42
|
+
const [imageError, setImageError] = useState(false);
|
|
43
|
+
|
|
44
|
+
const hasPreview = !!uri && !inProgress && !imageError;
|
|
45
|
+
|
|
46
|
+
const handleImageError = (_error: NativeSyntheticEvent<ImageErrorEventData>) => {
|
|
47
|
+
setImageError(true);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const styles = useMemo(
|
|
51
|
+
() =>
|
|
52
|
+
StyleSheet.create({
|
|
53
|
+
container: {
|
|
54
|
+
width: "100%",
|
|
55
|
+
aspectRatio: height ? undefined : aspectRatio,
|
|
56
|
+
height: height,
|
|
57
|
+
backgroundColor: tokens.colors.backgroundSecondary,
|
|
58
|
+
position: "relative",
|
|
59
|
+
overflow: "hidden",
|
|
60
|
+
},
|
|
61
|
+
image: {
|
|
62
|
+
width: "100%",
|
|
63
|
+
height: "100%",
|
|
64
|
+
},
|
|
65
|
+
placeholder: {
|
|
66
|
+
width: "100%",
|
|
67
|
+
height: "100%",
|
|
68
|
+
justifyContent: "center",
|
|
69
|
+
alignItems: "center",
|
|
70
|
+
},
|
|
71
|
+
loadingContainer: {
|
|
72
|
+
width: "100%",
|
|
73
|
+
height: "100%",
|
|
74
|
+
justifyContent: "center",
|
|
75
|
+
alignItems: "center",
|
|
76
|
+
},
|
|
77
|
+
loadingIcon: {
|
|
78
|
+
width: 64,
|
|
79
|
+
height: 64,
|
|
80
|
+
borderRadius: 32,
|
|
81
|
+
backgroundColor: tokens.colors.primary + "20",
|
|
82
|
+
justifyContent: "center",
|
|
83
|
+
alignItems: "center",
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
[tokens, aspectRatio, height]
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Show loading state
|
|
90
|
+
if (inProgress && showLoadingIndicator) {
|
|
91
|
+
return (
|
|
92
|
+
<View style={styles.container}>
|
|
93
|
+
<View style={styles.loadingContainer}>
|
|
94
|
+
<View style={styles.loadingIcon}>
|
|
95
|
+
<AtomicSpinner size="lg" color="primary" />
|
|
96
|
+
</View>
|
|
97
|
+
</View>
|
|
98
|
+
</View>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Show image preview
|
|
103
|
+
if (hasPreview) {
|
|
104
|
+
return (
|
|
105
|
+
<View style={styles.container}>
|
|
106
|
+
<Image
|
|
107
|
+
source={{ uri }}
|
|
108
|
+
style={styles.image}
|
|
109
|
+
resizeMode="cover"
|
|
110
|
+
onError={handleImageError}
|
|
111
|
+
/>
|
|
112
|
+
</View>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Show placeholder
|
|
117
|
+
return (
|
|
118
|
+
<View style={styles.container}>
|
|
119
|
+
<View style={styles.placeholder}>
|
|
120
|
+
<AtomicIcon name={typeIcon} color="secondary" size="xl" />
|
|
121
|
+
</View>
|
|
122
|
+
</View>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CreationPreview Component
|
|
3
|
-
*
|
|
3
|
+
* Smart wrapper that delegates to CreationImagePreview or CreationVideoPreview
|
|
4
|
+
* based on creation type
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import React
|
|
7
|
-
import { View, StyleSheet, Image } from "react-native";
|
|
8
|
-
import {
|
|
9
|
-
useAppDesignTokens,
|
|
10
|
-
AtomicIcon,
|
|
11
|
-
AtomicSpinner,
|
|
12
|
-
} from "@umituz/react-native-design-system";
|
|
7
|
+
import React from "react";
|
|
13
8
|
import type { CreationStatus, CreationTypeId } from "../../domain/types";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
9
|
+
import { CreationImagePreview } from "./CreationImagePreview";
|
|
10
|
+
import { CreationVideoPreview } from "./CreationVideoPreview";
|
|
11
|
+
|
|
12
|
+
/** Video creation types */
|
|
13
|
+
const VIDEO_TYPES: CreationTypeId[] = ["text-to-video", "image-to-video"];
|
|
14
|
+
|
|
15
|
+
/** Check if creation type is a video type */
|
|
16
|
+
function isVideoType(type?: CreationTypeId | string): boolean {
|
|
17
|
+
return VIDEO_TYPES.includes(type as CreationTypeId);
|
|
18
|
+
}
|
|
16
19
|
|
|
17
20
|
interface CreationPreviewProps {
|
|
18
|
-
/** Preview image URL */
|
|
21
|
+
/** Preview image/thumbnail URL */
|
|
19
22
|
readonly uri?: string | null;
|
|
23
|
+
/** Thumbnail URL for videos (optional, if different from uri) */
|
|
24
|
+
readonly thumbnailUrl?: string | null;
|
|
20
25
|
/** Creation status */
|
|
21
26
|
readonly status?: CreationStatus;
|
|
22
|
-
/** Creation type for
|
|
23
|
-
readonly type?: CreationTypeId;
|
|
27
|
+
/** Creation type for determining preview type */
|
|
28
|
+
readonly type?: CreationTypeId | string;
|
|
24
29
|
/** Aspect ratio (default: 16/9) */
|
|
25
30
|
readonly aspectRatio?: number;
|
|
26
31
|
/** Custom height (overrides aspectRatio) */
|
|
@@ -31,88 +36,37 @@ interface CreationPreviewProps {
|
|
|
31
36
|
|
|
32
37
|
export function CreationPreview({
|
|
33
38
|
uri,
|
|
39
|
+
thumbnailUrl,
|
|
34
40
|
status = "completed",
|
|
35
41
|
type = "text-to-image",
|
|
36
42
|
aspectRatio = 16 / 9,
|
|
37
43
|
height,
|
|
38
44
|
showLoadingIndicator = true,
|
|
39
45
|
}: CreationPreviewProps) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const typeIcon = getTypeIcon(type);
|
|
43
|
-
const hasPreview = !!uri && !inProgress;
|
|
44
|
-
|
|
45
|
-
const styles = useMemo(
|
|
46
|
-
() =>
|
|
47
|
-
StyleSheet.create({
|
|
48
|
-
container: {
|
|
49
|
-
width: "100%",
|
|
50
|
-
aspectRatio: height ? undefined : aspectRatio,
|
|
51
|
-
height: height,
|
|
52
|
-
backgroundColor: tokens.colors.backgroundSecondary,
|
|
53
|
-
position: "relative",
|
|
54
|
-
overflow: "hidden",
|
|
55
|
-
},
|
|
56
|
-
image: {
|
|
57
|
-
width: "100%",
|
|
58
|
-
height: "100%",
|
|
59
|
-
},
|
|
60
|
-
placeholder: {
|
|
61
|
-
width: "100%",
|
|
62
|
-
height: "100%",
|
|
63
|
-
justifyContent: "center",
|
|
64
|
-
alignItems: "center",
|
|
65
|
-
},
|
|
66
|
-
loadingContainer: {
|
|
67
|
-
width: "100%",
|
|
68
|
-
height: "100%",
|
|
69
|
-
justifyContent: "center",
|
|
70
|
-
alignItems: "center",
|
|
71
|
-
},
|
|
72
|
-
loadingIcon: {
|
|
73
|
-
width: 64,
|
|
74
|
-
height: 64,
|
|
75
|
-
borderRadius: 32,
|
|
76
|
-
backgroundColor: tokens.colors.primary + "20",
|
|
77
|
-
justifyContent: "center",
|
|
78
|
-
alignItems: "center",
|
|
79
|
-
},
|
|
80
|
-
}),
|
|
81
|
-
[tokens, aspectRatio, height]
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
// Show loading state
|
|
85
|
-
if (inProgress && showLoadingIndicator) {
|
|
86
|
-
return (
|
|
87
|
-
<View style={styles.container}>
|
|
88
|
-
<View style={styles.loadingContainer}>
|
|
89
|
-
<View style={styles.loadingIcon}>
|
|
90
|
-
<AtomicSpinner size="lg" color="primary" />
|
|
91
|
-
</View>
|
|
92
|
-
</View>
|
|
93
|
-
</View>
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Show image preview
|
|
98
|
-
if (hasPreview) {
|
|
46
|
+
// For video types, use CreationVideoPreview
|
|
47
|
+
if (isVideoType(type)) {
|
|
99
48
|
return (
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
49
|
+
<CreationVideoPreview
|
|
50
|
+
thumbnailUrl={thumbnailUrl || uri}
|
|
51
|
+
videoUrl={uri}
|
|
52
|
+
status={status}
|
|
53
|
+
type={type as CreationTypeId}
|
|
54
|
+
aspectRatio={aspectRatio}
|
|
55
|
+
height={height}
|
|
56
|
+
showLoadingIndicator={showLoadingIndicator}
|
|
57
|
+
/>
|
|
107
58
|
);
|
|
108
59
|
}
|
|
109
60
|
|
|
110
|
-
//
|
|
61
|
+
// For image types, use CreationImagePreview
|
|
111
62
|
return (
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
63
|
+
<CreationImagePreview
|
|
64
|
+
uri={uri}
|
|
65
|
+
status={status}
|
|
66
|
+
type={type as CreationTypeId}
|
|
67
|
+
aspectRatio={aspectRatio}
|
|
68
|
+
height={height}
|
|
69
|
+
showLoadingIndicator={showLoadingIndicator}
|
|
70
|
+
/>
|
|
117
71
|
);
|
|
118
72
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CreationVideoPreview Component
|
|
3
|
+
* Displays video preview with thumbnail and play icon overlay
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
|
+
import { View, StyleSheet, Image } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
useAppDesignTokens,
|
|
10
|
+
AtomicIcon,
|
|
11
|
+
AtomicSpinner,
|
|
12
|
+
} from "@umituz/react-native-design-system";
|
|
13
|
+
import type { CreationStatus, CreationTypeId } from "../../domain/types";
|
|
14
|
+
import { isInProgress } from "../../domain/utils";
|
|
15
|
+
|
|
16
|
+
export interface CreationVideoPreviewProps {
|
|
17
|
+
/** Thumbnail image URL (optional) */
|
|
18
|
+
readonly thumbnailUrl?: string | null;
|
|
19
|
+
/** Video URL (for display purposes only) */
|
|
20
|
+
readonly videoUrl?: string | null;
|
|
21
|
+
/** Creation status */
|
|
22
|
+
readonly status?: CreationStatus;
|
|
23
|
+
/** Creation type for placeholder icon */
|
|
24
|
+
readonly type?: CreationTypeId;
|
|
25
|
+
/** Aspect ratio (default: 16/9) */
|
|
26
|
+
readonly aspectRatio?: number;
|
|
27
|
+
/** Custom height (overrides aspectRatio) */
|
|
28
|
+
readonly height?: number;
|
|
29
|
+
/** Show loading indicator when in progress */
|
|
30
|
+
readonly showLoadingIndicator?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Check if URL is a video URL (mp4, mov, etc.) */
|
|
34
|
+
function isVideoUrl(url?: string | null): boolean {
|
|
35
|
+
if (!url) return false;
|
|
36
|
+
const videoExtensions = [".mp4", ".mov", ".avi", ".webm", ".mkv"];
|
|
37
|
+
const lowerUrl = url.toLowerCase();
|
|
38
|
+
return videoExtensions.some((ext) => lowerUrl.includes(ext));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function CreationVideoPreview({
|
|
42
|
+
thumbnailUrl,
|
|
43
|
+
videoUrl,
|
|
44
|
+
status = "completed",
|
|
45
|
+
aspectRatio = 16 / 9,
|
|
46
|
+
height,
|
|
47
|
+
showLoadingIndicator = true,
|
|
48
|
+
}: CreationVideoPreviewProps) {
|
|
49
|
+
const tokens = useAppDesignTokens();
|
|
50
|
+
const inProgress = isInProgress(status);
|
|
51
|
+
|
|
52
|
+
// Only use thumbnail if it's a real image URL, not a video URL
|
|
53
|
+
const hasThumbnail = !!thumbnailUrl && !inProgress && !isVideoUrl(thumbnailUrl);
|
|
54
|
+
|
|
55
|
+
const styles = useMemo(
|
|
56
|
+
() =>
|
|
57
|
+
StyleSheet.create({
|
|
58
|
+
container: {
|
|
59
|
+
width: "100%",
|
|
60
|
+
aspectRatio: height ? undefined : aspectRatio,
|
|
61
|
+
height: height,
|
|
62
|
+
backgroundColor: tokens.colors.backgroundSecondary,
|
|
63
|
+
position: "relative",
|
|
64
|
+
overflow: "hidden",
|
|
65
|
+
},
|
|
66
|
+
thumbnail: {
|
|
67
|
+
width: "100%",
|
|
68
|
+
height: "100%",
|
|
69
|
+
},
|
|
70
|
+
placeholder: {
|
|
71
|
+
width: "100%",
|
|
72
|
+
height: "100%",
|
|
73
|
+
justifyContent: "center",
|
|
74
|
+
alignItems: "center",
|
|
75
|
+
},
|
|
76
|
+
loadingContainer: {
|
|
77
|
+
width: "100%",
|
|
78
|
+
height: "100%",
|
|
79
|
+
justifyContent: "center",
|
|
80
|
+
alignItems: "center",
|
|
81
|
+
},
|
|
82
|
+
loadingIcon: {
|
|
83
|
+
width: 64,
|
|
84
|
+
height: 64,
|
|
85
|
+
borderRadius: 32,
|
|
86
|
+
backgroundColor: tokens.colors.primary + "20",
|
|
87
|
+
justifyContent: "center",
|
|
88
|
+
alignItems: "center",
|
|
89
|
+
},
|
|
90
|
+
playIconOverlay: {
|
|
91
|
+
...StyleSheet.absoluteFillObject,
|
|
92
|
+
justifyContent: "center",
|
|
93
|
+
alignItems: "center",
|
|
94
|
+
},
|
|
95
|
+
playIconContainer: {
|
|
96
|
+
width: 56,
|
|
97
|
+
height: 56,
|
|
98
|
+
borderRadius: 28,
|
|
99
|
+
backgroundColor: tokens.colors.primary,
|
|
100
|
+
justifyContent: "center",
|
|
101
|
+
alignItems: "center",
|
|
102
|
+
paddingLeft: 4,
|
|
103
|
+
},
|
|
104
|
+
}),
|
|
105
|
+
[tokens, aspectRatio, height]
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Show loading state
|
|
109
|
+
if (inProgress && showLoadingIndicator) {
|
|
110
|
+
return (
|
|
111
|
+
<View style={styles.container}>
|
|
112
|
+
<View style={styles.loadingContainer}>
|
|
113
|
+
<View style={styles.loadingIcon}>
|
|
114
|
+
<AtomicSpinner size="lg" color="primary" />
|
|
115
|
+
</View>
|
|
116
|
+
</View>
|
|
117
|
+
</View>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Show thumbnail with play icon overlay
|
|
122
|
+
if (hasThumbnail) {
|
|
123
|
+
return (
|
|
124
|
+
<View style={styles.container}>
|
|
125
|
+
<Image
|
|
126
|
+
source={{ uri: thumbnailUrl }}
|
|
127
|
+
style={styles.thumbnail}
|
|
128
|
+
resizeMode="cover"
|
|
129
|
+
/>
|
|
130
|
+
<View style={styles.playIconOverlay}>
|
|
131
|
+
<View style={styles.playIconContainer}>
|
|
132
|
+
<AtomicIcon name="play" customSize={24} color="onPrimary" />
|
|
133
|
+
</View>
|
|
134
|
+
</View>
|
|
135
|
+
</View>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Show placeholder with play icon (no thumbnail available)
|
|
140
|
+
return (
|
|
141
|
+
<View style={styles.container}>
|
|
142
|
+
<View style={styles.placeholder}>
|
|
143
|
+
<View style={styles.playIconContainer}>
|
|
144
|
+
<AtomicIcon name="play" customSize={24} color="onPrimary" />
|
|
145
|
+
</View>
|
|
146
|
+
</View>
|
|
147
|
+
</View>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
// Core Components
|
|
6
6
|
export { CreationPreview } from "./CreationPreview";
|
|
7
|
+
export { CreationImagePreview, type CreationImagePreviewProps } from "./CreationImagePreview";
|
|
8
|
+
export { CreationVideoPreview, type CreationVideoPreviewProps } from "./CreationVideoPreview";
|
|
7
9
|
export { CreationBadges } from "./CreationBadges";
|
|
8
10
|
export { CreationActions, type CreationAction } from "./CreationActions";
|
|
9
11
|
export {
|
|
@@ -27,12 +27,22 @@ export interface CreationData {
|
|
|
27
27
|
metadata?: Record<string, unknown>;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export interface GenerationStartData {
|
|
31
|
+
creationId: string;
|
|
32
|
+
type: "text-to-video";
|
|
33
|
+
prompt: string;
|
|
34
|
+
metadata?: Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
|
|
30
37
|
export interface TextToVideoCallbacks {
|
|
31
38
|
onCreditCheck?: (cost: number) => Promise<boolean>;
|
|
32
39
|
onCreditDeduct?: (cost: number) => Promise<void>;
|
|
33
40
|
onAuthCheck?: () => boolean;
|
|
34
41
|
onModeration?: (prompt: string) => Promise<VideoModerationResult>;
|
|
35
|
-
|
|
42
|
+
/** Called when generation starts - save a "processing" creation */
|
|
43
|
+
onGenerationStart?: (data: GenerationStartData) => Promise<void>;
|
|
44
|
+
/** Called when generation completes - update creation to "completed" */
|
|
45
|
+
onCreationSave?: (data: CreationData & { creationId: string }) => Promise<void>;
|
|
36
46
|
onGenerate?: (result: TextToVideoResult) => void;
|
|
37
47
|
onError?: (error: string) => void;
|
|
38
48
|
onProgress?: (progress: number) => void;
|
|
@@ -124,6 +124,9 @@ export function useTextToVideoFeature(
|
|
|
124
124
|
|
|
125
125
|
const executeGeneration = useCallback(
|
|
126
126
|
async (prompt: string, options?: TextToVideoOptions): Promise<TextToVideoResult> => {
|
|
127
|
+
// Generate unique creation ID for tracking
|
|
128
|
+
const creationId = `text-to-video_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
129
|
+
|
|
127
130
|
setState((prev) => ({
|
|
128
131
|
...prev,
|
|
129
132
|
isProcessing: true,
|
|
@@ -133,7 +136,17 @@ export function useTextToVideoFeature(
|
|
|
133
136
|
|
|
134
137
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
135
138
|
// eslint-disable-next-line no-console
|
|
136
|
-
console.log("[TextToVideoFeature] Starting generation with prompt:", prompt);
|
|
139
|
+
console.log("[TextToVideoFeature] Starting generation with prompt:", prompt, "creationId:", creationId);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Create "processing" creation at start
|
|
143
|
+
if (callbacks.onGenerationStart) {
|
|
144
|
+
await callbacks.onGenerationStart({
|
|
145
|
+
creationId,
|
|
146
|
+
type: "text-to-video",
|
|
147
|
+
prompt,
|
|
148
|
+
metadata: options as Record<string, unknown> | undefined,
|
|
149
|
+
});
|
|
137
150
|
}
|
|
138
151
|
|
|
139
152
|
const result = await executeTextToVideo(
|
|
@@ -163,9 +176,10 @@ export function useTextToVideoFeature(
|
|
|
163
176
|
await callbacks.onCreditDeduct(config.creditCost);
|
|
164
177
|
}
|
|
165
178
|
|
|
166
|
-
//
|
|
179
|
+
// Update creation to completed after successful generation
|
|
167
180
|
if (callbacks.onCreationSave) {
|
|
168
181
|
await callbacks.onCreationSave({
|
|
182
|
+
creationId,
|
|
169
183
|
type: "text-to-video",
|
|
170
184
|
videoUrl: result.videoUrl,
|
|
171
185
|
thumbnailUrl: result.thumbnailUrl,
|
|
@@ -37,6 +37,8 @@ export const AIGenerationForm: React.FC<AIGenerationFormProps> = ({
|
|
|
37
37
|
hideGenerateButton,
|
|
38
38
|
progress,
|
|
39
39
|
progressIcon,
|
|
40
|
+
isProgressModalVisible,
|
|
41
|
+
onCloseProgressModal,
|
|
40
42
|
generateButtonProps,
|
|
41
43
|
showAdvanced,
|
|
42
44
|
onAdvancedToggle,
|
|
@@ -158,11 +160,13 @@ export const AIGenerationForm: React.FC<AIGenerationFormProps> = ({
|
|
|
158
160
|
|
|
159
161
|
{/* MANDATORY: Progress Modal shows automatically when isGenerating */}
|
|
160
162
|
<GenerationProgressModal
|
|
161
|
-
visible={isGenerating}
|
|
163
|
+
visible={isProgressModalVisible ?? isGenerating}
|
|
162
164
|
progress={progress ?? 0}
|
|
163
165
|
icon={progressIcon || "sparkles-outline"}
|
|
164
166
|
title={translations.progressTitle || translations.generatingButton}
|
|
165
167
|
message={translations.progressMessage || translations.progressHint}
|
|
168
|
+
onClose={onCloseProgressModal}
|
|
169
|
+
backgroundHint={onCloseProgressModal ? translations.progressBackgroundHint : undefined}
|
|
166
170
|
/>
|
|
167
171
|
</>
|
|
168
172
|
);
|
|
@@ -18,6 +18,8 @@ export interface AIGenerationFormTranslations {
|
|
|
18
18
|
progressTitle?: string;
|
|
19
19
|
progressMessage?: string;
|
|
20
20
|
progressHint?: string;
|
|
21
|
+
/** Hint for background generation (e.g., "Continue in background") */
|
|
22
|
+
progressBackgroundHint?: string;
|
|
21
23
|
presetsTitle?: string;
|
|
22
24
|
showAdvancedLabel?: string;
|
|
23
25
|
hideAdvancedLabel?: string;
|
|
@@ -57,6 +59,10 @@ export interface AIGenerationFormProps extends PropsWithChildren {
|
|
|
57
59
|
// Optional: Generation Progress
|
|
58
60
|
progress?: number;
|
|
59
61
|
progressIcon?: string;
|
|
62
|
+
/** Override modal visibility (defaults to isGenerating) */
|
|
63
|
+
isProgressModalVisible?: boolean;
|
|
64
|
+
/** Callback when user closes the progress modal (for background generation) */
|
|
65
|
+
onCloseProgressModal?: () => void;
|
|
60
66
|
|
|
61
67
|
// Custom Generate Button Props
|
|
62
68
|
generateButtonProps?: {
|
|
@@ -21,6 +21,10 @@ export interface GenerationProgressContentProps {
|
|
|
21
21
|
readonly hint?: string;
|
|
22
22
|
readonly dismissLabel?: string;
|
|
23
23
|
readonly onDismiss?: () => void;
|
|
24
|
+
/** Close button in top-right corner for background generation */
|
|
25
|
+
readonly onClose?: () => void;
|
|
26
|
+
/** Hint text shown near close button (e.g., "Continue in background") */
|
|
27
|
+
readonly backgroundHint?: string;
|
|
24
28
|
readonly backgroundColor?: string;
|
|
25
29
|
readonly textColor?: string;
|
|
26
30
|
readonly hintColor?: string;
|
|
@@ -39,6 +43,8 @@ export const GenerationProgressContent: React.FC<
|
|
|
39
43
|
hint,
|
|
40
44
|
dismissLabel,
|
|
41
45
|
onDismiss,
|
|
46
|
+
onClose,
|
|
47
|
+
backgroundHint,
|
|
42
48
|
backgroundColor,
|
|
43
49
|
textColor,
|
|
44
50
|
hintColor,
|
|
@@ -62,6 +68,17 @@ export const GenerationProgressContent: React.FC<
|
|
|
62
68
|
},
|
|
63
69
|
]}
|
|
64
70
|
>
|
|
71
|
+
{/* Close button in top-right corner */}
|
|
72
|
+
{onClose && (
|
|
73
|
+
<TouchableOpacity
|
|
74
|
+
style={styles.closeButton}
|
|
75
|
+
onPress={onClose}
|
|
76
|
+
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
77
|
+
>
|
|
78
|
+
<AtomicIcon name="close" size="md" color="secondary" />
|
|
79
|
+
</TouchableOpacity>
|
|
80
|
+
)}
|
|
81
|
+
|
|
65
82
|
{icon && (
|
|
66
83
|
<View style={styles.iconContainer}>
|
|
67
84
|
<AtomicIcon name={icon} size="xl" color="primary" />
|
|
@@ -102,6 +119,21 @@ export const GenerationProgressContent: React.FC<
|
|
|
102
119
|
</AtomicText>
|
|
103
120
|
)}
|
|
104
121
|
|
|
122
|
+
{/* Background hint - clickable to close and continue in background */}
|
|
123
|
+
{onClose && backgroundHint && (
|
|
124
|
+
<TouchableOpacity
|
|
125
|
+
style={styles.backgroundHintButton}
|
|
126
|
+
onPress={onClose}
|
|
127
|
+
>
|
|
128
|
+
<AtomicText
|
|
129
|
+
type="bodySmall"
|
|
130
|
+
style={[styles.backgroundHintText, { color: tokens.colors.primary }]}
|
|
131
|
+
>
|
|
132
|
+
{backgroundHint}
|
|
133
|
+
</AtomicText>
|
|
134
|
+
</TouchableOpacity>
|
|
135
|
+
)}
|
|
136
|
+
|
|
105
137
|
{onDismiss && (
|
|
106
138
|
<TouchableOpacity
|
|
107
139
|
style={[
|
|
@@ -130,6 +162,18 @@ const styles = StyleSheet.create({
|
|
|
130
162
|
padding: 32,
|
|
131
163
|
borderWidth: 1,
|
|
132
164
|
alignItems: "center",
|
|
165
|
+
position: "relative",
|
|
166
|
+
},
|
|
167
|
+
closeButton: {
|
|
168
|
+
position: "absolute",
|
|
169
|
+
top: 16,
|
|
170
|
+
right: 16,
|
|
171
|
+
width: 32,
|
|
172
|
+
height: 32,
|
|
173
|
+
borderRadius: 16,
|
|
174
|
+
justifyContent: "center",
|
|
175
|
+
alignItems: "center",
|
|
176
|
+
zIndex: 1,
|
|
133
177
|
},
|
|
134
178
|
iconContainer: {
|
|
135
179
|
marginBottom: 20,
|
|
@@ -149,6 +193,15 @@ const styles = StyleSheet.create({
|
|
|
149
193
|
lineHeight: 18,
|
|
150
194
|
paddingHorizontal: 8,
|
|
151
195
|
},
|
|
196
|
+
backgroundHintButton: {
|
|
197
|
+
marginTop: 16,
|
|
198
|
+
paddingVertical: 8,
|
|
199
|
+
paddingHorizontal: 16,
|
|
200
|
+
},
|
|
201
|
+
backgroundHintText: {
|
|
202
|
+
textAlign: "center",
|
|
203
|
+
textDecorationLine: "underline",
|
|
204
|
+
},
|
|
152
205
|
dismissButton: {
|
|
153
206
|
marginTop: 16,
|
|
154
207
|
paddingVertical: 14,
|
|
@@ -22,6 +22,7 @@ export interface GenerationProgressRenderProps {
|
|
|
22
22
|
readonly message?: string;
|
|
23
23
|
readonly hint?: string;
|
|
24
24
|
readonly onDismiss?: () => void;
|
|
25
|
+
readonly onClose?: () => void;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export interface GenerationProgressModalProps
|
|
@@ -44,6 +45,8 @@ export const GenerationProgressModal: React.FC<
|
|
|
44
45
|
hint,
|
|
45
46
|
dismissLabel,
|
|
46
47
|
onDismiss,
|
|
48
|
+
onClose,
|
|
49
|
+
backgroundHint,
|
|
47
50
|
modalBackgroundColor,
|
|
48
51
|
textColor,
|
|
49
52
|
hintColor,
|
|
@@ -67,6 +70,7 @@ export const GenerationProgressModal: React.FC<
|
|
|
67
70
|
message,
|
|
68
71
|
hint,
|
|
69
72
|
onDismiss,
|
|
73
|
+
onClose,
|
|
70
74
|
})
|
|
71
75
|
) : (
|
|
72
76
|
<GenerationProgressContent
|
|
@@ -77,6 +81,8 @@ export const GenerationProgressModal: React.FC<
|
|
|
77
81
|
hint={hint}
|
|
78
82
|
dismissLabel={dismissLabel}
|
|
79
83
|
onDismiss={onDismiss}
|
|
84
|
+
onClose={onClose}
|
|
85
|
+
backgroundHint={backgroundHint}
|
|
80
86
|
backgroundColor={modalBackgroundColor || tokens.colors.surface}
|
|
81
87
|
textColor={textColor || tokens.colors.textPrimary}
|
|
82
88
|
hintColor={hintColor || tokens.colors.textTertiary}
|