@umituz/react-native-ai-generation-content 1.28.3 → 1.28.5
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/presentation/screens/CreationsGalleryScreen.tsx +22 -3
- package/src/domains/generation/wizard/infrastructure/builders/dynamic-step-builder.ts +9 -6
- package/src/domains/generation/wizard/presentation/components/GenericWizardFlow.tsx +15 -4
- package/src/domains/result-preview/presentation/hooks/useResultActions.ts +71 -47
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.28.
|
|
3
|
+
"version": "1.28.5",
|
|
4
4
|
"description": "Provider-agnostic AI generation orchestration for React Native with result preview components",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useMemo, useCallback } from "react";
|
|
1
|
+
import React, { useState, useMemo, useCallback, useEffect, useRef } from "react";
|
|
2
2
|
import { View, FlatList, RefreshControl, StyleSheet } from "react-native";
|
|
3
3
|
import {
|
|
4
4
|
useAppDesignTokens,
|
|
@@ -28,6 +28,7 @@ interface CreationsGalleryScreenProps {
|
|
|
28
28
|
readonly repository: ICreationsRepository;
|
|
29
29
|
readonly config: CreationsConfig;
|
|
30
30
|
readonly t: (key: string) => string;
|
|
31
|
+
readonly initialCreationId?: string;
|
|
31
32
|
readonly onEmptyAction?: () => void;
|
|
32
33
|
readonly emptyActionLabel?: string;
|
|
33
34
|
readonly showFilter?: boolean;
|
|
@@ -38,6 +39,7 @@ export function CreationsGalleryScreen({
|
|
|
38
39
|
repository,
|
|
39
40
|
config,
|
|
40
41
|
t,
|
|
42
|
+
initialCreationId,
|
|
41
43
|
onEmptyAction,
|
|
42
44
|
emptyActionLabel,
|
|
43
45
|
showFilter = config.showFilter ?? true,
|
|
@@ -47,14 +49,29 @@ export function CreationsGalleryScreen({
|
|
|
47
49
|
const alert = useAlert();
|
|
48
50
|
const [selectedCreation, setSelectedCreation] = useState<Creation | null>(null);
|
|
49
51
|
const [showRatingPicker, setShowRatingPicker] = useState(false);
|
|
52
|
+
const hasAutoSelectedRef = useRef(false);
|
|
50
53
|
|
|
51
54
|
const { data: creations, isLoading, refetch } = useCreations({ userId, repository });
|
|
55
|
+
|
|
56
|
+
// Auto-select creation when initialCreationId is provided
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (initialCreationId && creations && creations.length > 0 && !hasAutoSelectedRef.current) {
|
|
59
|
+
const creation = creations.find((c) => c.id === initialCreationId);
|
|
60
|
+
if (creation) {
|
|
61
|
+
hasAutoSelectedRef.current = true;
|
|
62
|
+
setSelectedCreation(creation);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}, [initialCreationId, creations]);
|
|
66
|
+
|
|
52
67
|
const deleteMutation = useDeleteCreation({ userId, repository });
|
|
53
68
|
|
|
54
69
|
const selectedImageUrl = selectedCreation ? (getPreviewUrl(selectedCreation.output) || selectedCreation.uri) : undefined;
|
|
70
|
+
const selectedVideoUrl = selectedCreation?.output?.videoUrl;
|
|
55
71
|
|
|
56
72
|
const { isSharing, isSaving, handleDownload, handleShare } = useResultActions({
|
|
57
73
|
imageUrl: selectedImageUrl,
|
|
74
|
+
videoUrl: selectedVideoUrl,
|
|
58
75
|
onSaveSuccess: () => alert.show(AlertType.SUCCESS, AlertMode.TOAST, t("result.saveSuccess"), t("result.saveSuccessMessage")),
|
|
59
76
|
onSaveError: () => alert.show(AlertType.ERROR, AlertMode.TOAST, t("common.error"), t("result.saveError")),
|
|
60
77
|
});
|
|
@@ -194,12 +211,14 @@ export function CreationsGalleryScreen({
|
|
|
194
211
|
), [isLoading, creations, filters.isFiltered, tokens, t, config, emptyActionLabel, onEmptyAction, filters.clearAllFilters]);
|
|
195
212
|
|
|
196
213
|
// Show result preview when a creation is selected
|
|
197
|
-
|
|
214
|
+
const hasMediaToShow = selectedImageUrl || selectedVideoUrl;
|
|
215
|
+
if (selectedCreation && hasMediaToShow) {
|
|
198
216
|
const hasRating = selectedCreation.rating !== undefined && selectedCreation.rating !== null;
|
|
199
217
|
return (
|
|
200
218
|
<>
|
|
201
219
|
<ResultPreviewScreen
|
|
202
|
-
imageUrl={selectedImageUrl}
|
|
220
|
+
imageUrl={selectedVideoUrl ? undefined : selectedImageUrl}
|
|
221
|
+
videoUrl={selectedVideoUrl}
|
|
203
222
|
isSaving={isSaving}
|
|
204
223
|
isSharing={isSharing}
|
|
205
224
|
onDownload={handleDownload}
|
|
@@ -41,6 +41,7 @@ export const buildFlowStepsFromWizard = (
|
|
|
41
41
|
options?: {
|
|
42
42
|
includePreview?: boolean;
|
|
43
43
|
includeGenerating?: boolean;
|
|
44
|
+
includeResult?: boolean;
|
|
44
45
|
},
|
|
45
46
|
): StepDefinition[] => {
|
|
46
47
|
const steps: StepDefinition[] = [];
|
|
@@ -69,12 +70,14 @@ export const buildFlowStepsFromWizard = (
|
|
|
69
70
|
required: true,
|
|
70
71
|
});
|
|
71
72
|
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
// Add result preview after generating (unless explicitly disabled)
|
|
74
|
+
if (options?.includeResult !== false) {
|
|
75
|
+
steps.push({
|
|
76
|
+
id: "RESULT_PREVIEW",
|
|
77
|
+
type: StepType.RESULT_PREVIEW,
|
|
78
|
+
required: true,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
return steps;
|
|
@@ -26,6 +26,7 @@ export interface GenericWizardFlowProps {
|
|
|
26
26
|
readonly scenario?: WizardScenarioData;
|
|
27
27
|
readonly userId?: string;
|
|
28
28
|
readonly alertMessages?: AlertMessages;
|
|
29
|
+
readonly skipResultStep?: boolean;
|
|
29
30
|
readonly onStepChange?: (stepId: string, stepType: StepType | string) => void;
|
|
30
31
|
readonly onGenerationStart?: (data: Record<string, unknown>, proceedToGenerating: () => void) => void;
|
|
31
32
|
readonly onGenerationComplete?: (result: unknown) => void;
|
|
@@ -46,6 +47,7 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = ({
|
|
|
46
47
|
scenario,
|
|
47
48
|
userId,
|
|
48
49
|
alertMessages,
|
|
50
|
+
skipResultStep = false,
|
|
49
51
|
onStepChange,
|
|
50
52
|
onGenerationStart,
|
|
51
53
|
onGenerationComplete,
|
|
@@ -70,8 +72,9 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = ({
|
|
|
70
72
|
return buildFlowStepsFromWizard(featureConfig, {
|
|
71
73
|
includePreview: true,
|
|
72
74
|
includeGenerating: true,
|
|
75
|
+
includeResult: !skipResultStep,
|
|
73
76
|
});
|
|
74
|
-
}, [featureConfig]);
|
|
77
|
+
}, [featureConfig, skipResultStep]);
|
|
75
78
|
|
|
76
79
|
const flow = useFlow({ steps: flowSteps, initialStepIndex: 0 });
|
|
77
80
|
const {
|
|
@@ -88,8 +91,10 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = ({
|
|
|
88
91
|
} = flow;
|
|
89
92
|
|
|
90
93
|
const resultImageUrl = currentCreation?.output?.imageUrl || currentCreation?.uri || "";
|
|
94
|
+
const resultVideoUrl = currentCreation?.output?.videoUrl || "";
|
|
91
95
|
const { isSaving, isSharing, handleDownload, handleShare } = useResultActions({
|
|
92
96
|
imageUrl: resultImageUrl,
|
|
97
|
+
videoUrl: resultVideoUrl,
|
|
93
98
|
});
|
|
94
99
|
|
|
95
100
|
const validatedScenario = useMemo(() => validateScenario(scenario), [scenario]);
|
|
@@ -100,13 +105,19 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = ({
|
|
|
100
105
|
|
|
101
106
|
const handleGenerationComplete = useCallback((result: unknown) => {
|
|
102
107
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
103
|
-
console.log("[GenericWizardFlow] Generation completed");
|
|
108
|
+
console.log("[GenericWizardFlow] Generation completed", { skipResultStep });
|
|
104
109
|
}
|
|
105
110
|
setResult(result);
|
|
106
111
|
setCurrentCreation(result as Creation);
|
|
107
|
-
|
|
112
|
+
|
|
113
|
+
// Call onGenerationComplete first so parent can navigate if needed
|
|
108
114
|
onGenerationComplete?.(result);
|
|
109
|
-
|
|
115
|
+
|
|
116
|
+
// Only go to result step if not skipping
|
|
117
|
+
if (!skipResultStep) {
|
|
118
|
+
nextStep();
|
|
119
|
+
}
|
|
120
|
+
}, [setResult, nextStep, onGenerationComplete, skipResultStep]);
|
|
110
121
|
|
|
111
122
|
useWizardGeneration({
|
|
112
123
|
scenario: validatedScenario,
|
|
@@ -69,6 +69,7 @@ export const useResultActions = (
|
|
|
69
69
|
): UseResultActionsReturn => {
|
|
70
70
|
const {
|
|
71
71
|
imageUrl,
|
|
72
|
+
videoUrl,
|
|
72
73
|
onSaveSuccess,
|
|
73
74
|
onSaveError,
|
|
74
75
|
onShareStart,
|
|
@@ -78,12 +79,45 @@ export const useResultActions = (
|
|
|
78
79
|
const [isSharing, setIsSharing] = useState(false);
|
|
79
80
|
const [isSaving, setIsSaving] = useState(false);
|
|
80
81
|
|
|
82
|
+
// Use videoUrl if available, otherwise imageUrl
|
|
83
|
+
const mediaUrl = videoUrl || imageUrl;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Determine if URL is a video
|
|
87
|
+
*/
|
|
88
|
+
const isVideoUrl = (url: string): boolean => {
|
|
89
|
+
const videoExtensions = [".mp4", ".mov", ".avi", ".webm", ".m4v"];
|
|
90
|
+
const lowerUrl = url.toLowerCase();
|
|
91
|
+
return videoExtensions.some((ext) => lowerUrl.includes(ext));
|
|
92
|
+
};
|
|
93
|
+
|
|
81
94
|
/**
|
|
82
|
-
*
|
|
95
|
+
* Download media from URL to file
|
|
96
|
+
*/
|
|
97
|
+
const downloadMediaToFile = async (url: string, isVideo: boolean): Promise<string> => {
|
|
98
|
+
const timestamp = Date.now();
|
|
99
|
+
const extension = isVideo ? "mp4" : "jpg";
|
|
100
|
+
const filename = `ai_generation_${timestamp}.${extension}`;
|
|
101
|
+
const file = new File(Paths.cache, filename);
|
|
102
|
+
|
|
103
|
+
const response = await fetch(url);
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
throw new Error(`Failed to download media: ${response.statusText}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
109
|
+
const bytes = new Uint8Array(arrayBuffer);
|
|
110
|
+
file.write(bytes);
|
|
111
|
+
|
|
112
|
+
return file.uri;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Save media to device gallery
|
|
83
117
|
*/
|
|
84
118
|
const handleDownload = useCallback(async () => {
|
|
85
|
-
if (!
|
|
86
|
-
onSaveError?.(new Error("No
|
|
119
|
+
if (!mediaUrl) {
|
|
120
|
+
onSaveError?.(new Error("No media URL provided"));
|
|
87
121
|
return;
|
|
88
122
|
}
|
|
89
123
|
|
|
@@ -96,13 +130,20 @@ export const useResultActions = (
|
|
|
96
130
|
throw new Error("Media library permission not granted");
|
|
97
131
|
}
|
|
98
132
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
133
|
+
const isVideo = Boolean(videoUrl) || isVideoUrl(mediaUrl);
|
|
134
|
+
let fileUri = mediaUrl;
|
|
135
|
+
|
|
136
|
+
// Handle base64 images
|
|
137
|
+
if (!isVideo) {
|
|
138
|
+
const normalizedUrl = toDataUrl(mediaUrl);
|
|
139
|
+
if (isBase64DataUrl(normalizedUrl)) {
|
|
140
|
+
fileUri = await saveBase64ToFile(normalizedUrl);
|
|
141
|
+
} else if (normalizedUrl.startsWith("http")) {
|
|
142
|
+
fileUri = await downloadMediaToFile(normalizedUrl, false);
|
|
143
|
+
}
|
|
144
|
+
} else if (mediaUrl.startsWith("http")) {
|
|
145
|
+
// Download video from URL
|
|
146
|
+
fileUri = await downloadMediaToFile(mediaUrl, true);
|
|
106
147
|
}
|
|
107
148
|
|
|
108
149
|
// Save to media library
|
|
@@ -116,33 +157,13 @@ export const useResultActions = (
|
|
|
116
157
|
} finally {
|
|
117
158
|
setIsSaving(false);
|
|
118
159
|
}
|
|
119
|
-
}, [
|
|
160
|
+
}, [mediaUrl, videoUrl, onSaveSuccess, onSaveError]);
|
|
120
161
|
|
|
121
162
|
/**
|
|
122
|
-
*
|
|
123
|
-
*/
|
|
124
|
-
const downloadUrlToFile = async (url: string): Promise<string> => {
|
|
125
|
-
const timestamp = Date.now();
|
|
126
|
-
const filename = `ai_generation_${timestamp}.jpg`;
|
|
127
|
-
const file = new File(Paths.cache, filename);
|
|
128
|
-
|
|
129
|
-
const response = await fetch(url);
|
|
130
|
-
if (!response.ok) {
|
|
131
|
-
throw new Error(`Failed to download image: ${response.statusText}`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
135
|
-
const bytes = new Uint8Array(arrayBuffer);
|
|
136
|
-
file.write(bytes);
|
|
137
|
-
|
|
138
|
-
return file.uri;
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Share image
|
|
163
|
+
* Share media (image or video)
|
|
143
164
|
*/
|
|
144
165
|
const handleShare = useCallback(async () => {
|
|
145
|
-
if (!
|
|
166
|
+
if (!mediaUrl) {
|
|
146
167
|
return;
|
|
147
168
|
}
|
|
148
169
|
|
|
@@ -150,24 +171,27 @@ export const useResultActions = (
|
|
|
150
171
|
setIsSharing(true);
|
|
151
172
|
onShareStart?.();
|
|
152
173
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
174
|
+
const isVideo = Boolean(videoUrl) || isVideoUrl(mediaUrl);
|
|
175
|
+
let shareUrl = mediaUrl;
|
|
176
|
+
|
|
177
|
+
// Prepare file for sharing
|
|
178
|
+
if (!isVideo) {
|
|
179
|
+
const normalizedUrl = toDataUrl(mediaUrl);
|
|
180
|
+
if (isBase64DataUrl(normalizedUrl)) {
|
|
181
|
+
shareUrl = await saveBase64ToFile(normalizedUrl);
|
|
182
|
+
} else if (normalizedUrl.startsWith("http")) {
|
|
183
|
+
shareUrl = await downloadMediaToFile(normalizedUrl, false);
|
|
184
|
+
}
|
|
185
|
+
} else if (mediaUrl.startsWith("http")) {
|
|
186
|
+
shareUrl = await downloadMediaToFile(mediaUrl, true);
|
|
162
187
|
}
|
|
163
188
|
|
|
164
189
|
// Use expo-sharing for cross-platform file sharing
|
|
165
190
|
const isAvailable = await Sharing.isAvailableAsync();
|
|
166
191
|
if (isAvailable) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
});
|
|
192
|
+
const mimeType = isVideo ? "video/mp4" : "image/jpeg";
|
|
193
|
+
const dialogTitle = isVideo ? "Share Video" : "Share Image";
|
|
194
|
+
await Sharing.shareAsync(shareUrl, { mimeType, dialogTitle });
|
|
171
195
|
onShareEnd?.(false);
|
|
172
196
|
} else {
|
|
173
197
|
onShareEnd?.(true);
|
|
@@ -179,7 +203,7 @@ export const useResultActions = (
|
|
|
179
203
|
} finally {
|
|
180
204
|
setIsSharing(false);
|
|
181
205
|
}
|
|
182
|
-
}, [
|
|
206
|
+
}, [mediaUrl, videoUrl, onShareStart, onShareEnd]);
|
|
183
207
|
|
|
184
208
|
return {
|
|
185
209
|
isSaving,
|