@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.28.3",
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
- if (selectedCreation && selectedImageUrl) {
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
- // Always add result preview after generating
73
- steps.push({
74
- id: "RESULT_PREVIEW",
75
- type: StepType.RESULT_PREVIEW,
76
- required: true,
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
- nextStep();
112
+
113
+ // Call onGenerationComplete first so parent can navigate if needed
108
114
  onGenerationComplete?.(result);
109
- }, [setResult, nextStep, onGenerationComplete]);
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
- * Save image to device gallery
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 (!imageUrl) {
86
- onSaveError?.(new Error("No image URL provided"));
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
- // Convert to data URL if raw base64
100
- const normalizedUrl = toDataUrl(imageUrl);
101
- let fileUri = normalizedUrl;
102
-
103
- // If it's a base64 string, save to file first
104
- if (isBase64DataUrl(normalizedUrl)) {
105
- fileUri = await saveBase64ToFile(normalizedUrl);
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
- }, [imageUrl, onSaveSuccess, onSaveError]);
160
+ }, [mediaUrl, videoUrl, onSaveSuccess, onSaveError]);
120
161
 
121
162
  /**
122
- * Download image from URL to file
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 (!imageUrl) {
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
- // Convert to data URL if raw base64
154
- const normalizedUrl = toDataUrl(imageUrl);
155
- let shareUrl = normalizedUrl;
156
-
157
- // Download to file for sharing (works for both base64 and remote URLs)
158
- if (isBase64DataUrl(normalizedUrl)) {
159
- shareUrl = await saveBase64ToFile(normalizedUrl);
160
- } else if (normalizedUrl.startsWith("http")) {
161
- shareUrl = await downloadUrlToFile(normalizedUrl);
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
- await Sharing.shareAsync(shareUrl, {
168
- mimeType: "image/jpeg",
169
- dialogTitle: "Share Image",
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
- }, [imageUrl, onShareStart, onShareEnd]);
206
+ }, [mediaUrl, videoUrl, onShareStart, onShareEnd]);
183
207
 
184
208
  return {
185
209
  isSaving,