@umituz/react-native-ai-generation-content 1.17.183 → 1.17.184

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.17.183",
3
+ "version": "1.17.184",
4
4
  "description": "Provider-agnostic AI generation orchestration for React Native",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -1,14 +1,13 @@
1
1
  /**
2
2
  * PhotoRestoreFeature Component
3
3
  * Self-contained photo restore feature UI component
4
- * Uses hook internally, only requires config and translations
4
+ * Uses centralized SingleImageFeatureLayout for consistent UX
5
5
  */
6
6
 
7
- import React, { useCallback, useMemo } from "react";
8
- import { ScrollView, StyleSheet } from "react-native";
9
- import { useAppDesignTokens } from "@umituz/react-native-design-system";
7
+ import React, { useMemo } from "react";
10
8
  import { PhotoUploadCard } from "../../../../presentation/components/PhotoUploadCard";
11
- import { AIGenerationForm } from "../../../../presentation/components/AIGenerationForm";
9
+ import { SingleImageFeatureLayout } from "../../../../presentation/layouts";
10
+ import type { ProcessingModalRenderProps } from "../../../../presentation/layouts";
12
11
  import { PhotoRestoreResultView } from "./PhotoRestoreResultView";
13
12
  import { usePhotoRestoreFeature } from "../hooks";
14
13
  import type {
@@ -17,21 +16,17 @@ import type {
17
16
  } from "../../domain/types";
18
17
 
19
18
  export interface PhotoRestoreFeatureProps {
20
- /** Feature configuration with provider-specific settings */
21
19
  config: PhotoRestoreFeatureConfig;
22
- /** Translations for all UI text */
23
- translations: PhotoRestoreTranslations;
24
- /** Image picker callback */
20
+ translations: PhotoRestoreTranslations & {
21
+ modalTitle?: string;
22
+ modalMessage?: string;
23
+ modalHint?: string;
24
+ modalBackgroundHint?: string;
25
+ };
25
26
  onSelectImage: () => Promise<string | null>;
26
- /** Save image callback */
27
27
  onSaveImage: (imageUrl: string) => Promise<void>;
28
- /** Called before processing starts. Return false to cancel. */
29
28
  onBeforeProcess?: () => Promise<boolean>;
30
- /** Optional custom processing modal renderer */
31
- renderProcessingModal?: (props: {
32
- visible: boolean;
33
- progress: number;
34
- }) => React.ReactNode;
29
+ renderProcessingModal?: (props: ProcessingModalRenderProps) => React.ReactNode;
35
30
  }
36
31
 
37
32
  export const PhotoRestoreFeature: React.FC<PhotoRestoreFeatureProps> = ({
@@ -42,8 +37,6 @@ export const PhotoRestoreFeature: React.FC<PhotoRestoreFeatureProps> = ({
42
37
  onBeforeProcess,
43
38
  renderProcessingModal,
44
39
  }) => {
45
- const tokens = useAppDesignTokens();
46
-
47
40
  const feature = usePhotoRestoreFeature({
48
41
  config,
49
42
  onSelectImage,
@@ -51,38 +44,46 @@ export const PhotoRestoreFeature: React.FC<PhotoRestoreFeatureProps> = ({
51
44
  onBeforeProcess,
52
45
  });
53
46
 
54
- const photoTranslations = useMemo(
47
+ const modalTranslations = useMemo(
55
48
  () => ({
56
- tapToUpload: translations.uploadTitle,
57
- selectPhoto: translations.uploadSubtitle,
58
- change: translations.uploadChange,
59
- analyzing: translations.uploadAnalyzing,
49
+ title: translations.modalTitle || "Processing",
50
+ message: translations.modalMessage || "AI is restoring your photo...",
51
+ hint: translations.modalHint || "This may take a moment",
52
+ backgroundHint: translations.modalBackgroundHint || "Continue in background",
60
53
  }),
61
54
  [translations],
62
55
  );
63
56
 
64
- const handleProcess = useCallback(() => {
65
- void feature.process();
66
- }, [feature]);
67
-
68
- const handleSave = useCallback(() => {
69
- void feature.save();
70
- }, [feature]);
71
-
72
- const handleSelectImage = useCallback(() => {
73
- void feature.selectImage();
74
- }, [feature]);
75
-
76
- if (feature.processedUrl && feature.imageUri) {
77
- return (
78
- <ScrollView
79
- style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
80
- contentContainerStyle={styles.content}
81
- showsVerticalScrollIndicator={false}
82
- >
57
+ return (
58
+ <SingleImageFeatureLayout
59
+ feature={feature}
60
+ translations={translations}
61
+ modalTranslations={modalTranslations}
62
+ renderProcessingModal={renderProcessingModal}
63
+ renderInput={({ imageUri, onSelect, isDisabled, isProcessing }) => (
64
+ <PhotoUploadCard
65
+ imageUri={imageUri}
66
+ onPress={onSelect}
67
+ isValidating={isProcessing}
68
+ disabled={isDisabled}
69
+ translations={{
70
+ tapToUpload: translations.uploadTitle,
71
+ selectPhoto: translations.uploadSubtitle,
72
+ change: translations.uploadChange,
73
+ analyzing: translations.uploadAnalyzing,
74
+ }}
75
+ config={{
76
+ aspectRatio: 1,
77
+ borderRadius: 24,
78
+ showValidationStatus: false,
79
+ allowChange: true,
80
+ }}
81
+ />
82
+ )}
83
+ renderCustomResult={({ processedUrl, originalImageUri, onSave, onReset }) => (
83
84
  <PhotoRestoreResultView
84
- originalUri={feature.imageUri}
85
- processedUri={feature.processedUrl}
85
+ originalUri={originalImageUri}
86
+ processedUri={processedUrl}
86
87
  translations={{
87
88
  successText: translations.successText,
88
89
  saveButtonText: translations.saveButtonText,
@@ -90,72 +91,10 @@ export const PhotoRestoreFeature: React.FC<PhotoRestoreFeatureProps> = ({
90
91
  beforeLabel: translations.beforeLabel,
91
92
  afterLabel: translations.afterLabel,
92
93
  }}
93
- onSave={handleSave}
94
- onReset={feature.reset}
94
+ onSave={onSave}
95
+ onReset={onReset}
95
96
  />
96
- </ScrollView>
97
- );
98
- }
99
-
100
- return (
101
- <>
102
- <ScrollView
103
- style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
104
- contentContainerStyle={styles.content}
105
- showsVerticalScrollIndicator={false}
106
- >
107
- <AIGenerationForm
108
- onGenerate={handleProcess}
109
- isGenerating={feature.isProcessing}
110
- progress={feature.progress}
111
- translations={{
112
- generateButton: translations.processButtonText,
113
- generatingButton: translations.processingText,
114
- progressTitle: translations.processingText,
115
- }}
116
- >
117
- <PhotoUploadCard
118
- imageUri={feature.imageUri}
119
- onPress={handleSelectImage}
120
- isValidating={feature.isProcessing}
121
- disabled={feature.isProcessing}
122
- translations={photoTranslations}
123
- config={{
124
- aspectRatio: 1,
125
- borderRadius: 24,
126
- showValidationStatus: false,
127
- allowChange: true,
128
- }}
129
- />
130
- </AIGenerationForm>
131
- </ScrollView>
132
-
133
- {renderProcessingModal?.({ visible: feature.isProcessing, progress: feature.progress })}
134
- </>
97
+ )}
98
+ />
135
99
  );
136
100
  };
137
-
138
- const styles = StyleSheet.create({
139
- container: {
140
- flex: 1,
141
- },
142
- content: {
143
- paddingVertical: 16,
144
- },
145
- description: {
146
- textAlign: "center",
147
- marginHorizontal: 24,
148
- marginBottom: 24,
149
- lineHeight: 24,
150
- },
151
- errorContainer: {
152
- marginHorizontal: 24,
153
- marginBottom: 16,
154
- padding: 16,
155
- borderRadius: 12,
156
- },
157
- buttonContainer: {
158
- marginHorizontal: 24,
159
- marginTop: 8,
160
- },
161
- });
@@ -1,19 +1,18 @@
1
1
  /**
2
2
  * RemoveObjectFeature Component
3
3
  * Self-contained remove object feature UI component
4
- * Uses hook internally, only requires config and translations
4
+ * Uses centralized SingleImageWithPromptFeatureLayout for consistent UX
5
5
  */
6
6
 
7
- import React, { useCallback, useMemo } from "react";
8
- import { View, ScrollView, StyleSheet, Image, TextInput } from "react-native";
7
+ import React, { useMemo } from "react";
8
+ import { View, Image, StyleSheet, TextInput } from "react-native";
9
9
  import {
10
10
  useAppDesignTokens,
11
- useResponsive,
12
11
  AtomicText,
13
12
  } from "@umituz/react-native-design-system";
14
13
  import { PhotoUploadCard } from "../../../../presentation/components/PhotoUploadCard";
15
- import { AIGenerationForm } from "../../../../presentation/components/AIGenerationForm";
16
- import { AIGenerationResult } from "../../../../presentation/components/display/AIGenerationResult";
14
+ import { SingleImageWithPromptFeatureLayout } from "../../../../presentation/layouts";
15
+ import type { ProcessingModalRenderProps } from "../../../../presentation/layouts";
17
16
  import { useRemoveObjectFeature } from "../hooks";
18
17
  import type {
19
18
  RemoveObjectTranslations,
@@ -22,15 +21,16 @@ import type {
22
21
 
23
22
  export interface RemoveObjectFeatureProps {
24
23
  config: RemoveObjectFeatureConfig;
25
- translations: RemoveObjectTranslations;
24
+ translations: RemoveObjectTranslations & {
25
+ modalTitle?: string;
26
+ modalMessage?: string;
27
+ modalHint?: string;
28
+ modalBackgroundHint?: string;
29
+ };
26
30
  onSelectImage: () => Promise<string | null>;
27
31
  onSaveImage: (imageUrl: string) => Promise<void>;
28
- /** Called before processing starts. Return false to cancel. */
29
32
  onBeforeProcess?: () => Promise<boolean>;
30
- renderProcessingModal?: (props: {
31
- visible: boolean;
32
- progress: number;
33
- }) => React.ReactNode;
33
+ renderProcessingModal?: (props: ProcessingModalRenderProps) => React.ReactNode;
34
34
  }
35
35
 
36
36
  export const RemoveObjectFeature: React.FC<RemoveObjectFeatureProps> = ({
@@ -42,8 +42,6 @@ export const RemoveObjectFeature: React.FC<RemoveObjectFeatureProps> = ({
42
42
  renderProcessingModal,
43
43
  }) => {
44
44
  const tokens = useAppDesignTokens();
45
- const { width: screenWidth, horizontalPadding } = useResponsive();
46
- const imageSize = screenWidth - horizontalPadding * 2;
47
45
 
48
46
  const feature = useRemoveObjectFeature({
49
47
  config,
@@ -52,79 +50,35 @@ export const RemoveObjectFeature: React.FC<RemoveObjectFeatureProps> = ({
52
50
  onBeforeProcess,
53
51
  });
54
52
 
55
- const photoTranslations = useMemo(
53
+ const modalTranslations = useMemo(
56
54
  () => ({
57
- tapToUpload: translations.uploadTitle,
58
- selectPhoto: translations.uploadSubtitle,
59
- change: translations.uploadChange,
60
- analyzing: translations.uploadAnalyzing,
55
+ title: translations.modalTitle || "Processing",
56
+ message: translations.modalMessage || "AI is removing objects...",
57
+ hint: translations.modalHint || "This may take a moment",
58
+ backgroundHint: translations.modalBackgroundHint || "Continue in background",
61
59
  }),
62
60
  [translations],
63
61
  );
64
62
 
65
- const handleProcess = useCallback(() => {
66
- void feature.process();
67
- }, [feature]);
68
-
69
- const handleSave = useCallback(() => {
70
- void feature.save();
71
- }, [feature]);
72
-
73
- const handleSelectImage = useCallback(() => {
74
- void feature.selectImage();
75
- }, [feature]);
76
-
77
- if (feature.processedUrl) {
78
- return (
79
- <ScrollView
80
- style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
81
- contentContainerStyle={styles.content}
82
- showsVerticalScrollIndicator={false}
83
- >
84
- <AIGenerationResult
85
- successText={translations.successText}
86
- primaryAction={{
87
- label: translations.saveButtonText,
88
- onPress: handleSave,
89
- }}
90
- secondaryAction={{
91
- label: translations.tryAnotherText,
92
- onPress: feature.reset,
93
- }}
94
- >
95
- <Image
96
- source={{ uri: feature.processedUrl }}
97
- style={[styles.resultImage, { width: imageSize, height: imageSize }]}
98
- resizeMode="contain"
99
- />
100
- </AIGenerationResult>
101
- </ScrollView>
102
- );
103
- }
104
-
105
63
  return (
106
- <>
107
- <ScrollView
108
- style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
109
- contentContainerStyle={styles.content}
110
- showsVerticalScrollIndicator={false}
111
- >
112
- <AIGenerationForm
113
- onGenerate={handleProcess}
114
- isGenerating={feature.isProcessing}
115
- progress={feature.progress}
116
- translations={{
117
- generateButton: translations.processButtonText,
118
- generatingButton: translations.processingText,
119
- progressTitle: translations.processingText,
120
- }}
121
- >
64
+ <SingleImageWithPromptFeatureLayout
65
+ feature={feature}
66
+ translations={translations}
67
+ modalTranslations={modalTranslations}
68
+ renderProcessingModal={renderProcessingModal}
69
+ renderInput={({ imageUri, onSelect, isDisabled, isProcessing, prompt, onPromptChange }) => (
70
+ <>
122
71
  <PhotoUploadCard
123
- imageUri={feature.imageUri}
124
- onPress={handleSelectImage}
125
- isValidating={feature.isProcessing}
126
- disabled={feature.isProcessing}
127
- translations={photoTranslations}
72
+ imageUri={imageUri}
73
+ onPress={onSelect}
74
+ isValidating={isProcessing}
75
+ disabled={isDisabled}
76
+ translations={{
77
+ tapToUpload: translations.uploadTitle,
78
+ selectPhoto: translations.uploadSubtitle,
79
+ change: translations.uploadChange,
80
+ analyzing: translations.uploadAnalyzing,
81
+ }}
128
82
  config={{
129
83
  aspectRatio: 1,
130
84
  borderRadius: 24,
@@ -149,13 +103,13 @@ export const RemoveObjectFeature: React.FC<RemoveObjectFeatureProps> = ({
149
103
  borderColor: tokens.colors.border,
150
104
  },
151
105
  ]}
152
- value={feature.prompt}
153
- onChangeText={feature.setPrompt}
106
+ value={prompt}
107
+ onChangeText={onPromptChange}
154
108
  placeholder={translations.promptPlaceholder}
155
109
  placeholderTextColor={tokens.colors.textTertiary}
156
110
  multiline
157
111
  numberOfLines={3}
158
- editable={!feature.isProcessing}
112
+ editable={!isProcessing}
159
113
  />
160
114
  <AtomicText
161
115
  type="bodySmall"
@@ -164,27 +118,20 @@ export const RemoveObjectFeature: React.FC<RemoveObjectFeatureProps> = ({
164
118
  {translations.maskSubtitle}
165
119
  </AtomicText>
166
120
  </View>
167
- </AIGenerationForm>
168
- </ScrollView>
169
-
170
- {renderProcessingModal?.({ visible: feature.isProcessing, progress: feature.progress })}
171
- </>
121
+ </>
122
+ )}
123
+ renderResult={({ imageUrl, imageSize }) => (
124
+ <Image
125
+ source={{ uri: imageUrl }}
126
+ style={[styles.resultImage, { width: imageSize, height: imageSize }]}
127
+ resizeMode="contain"
128
+ />
129
+ )}
130
+ />
172
131
  );
173
132
  };
174
133
 
175
134
  const styles = StyleSheet.create({
176
- container: {
177
- flex: 1,
178
- },
179
- content: {
180
- paddingVertical: 16,
181
- },
182
- description: {
183
- textAlign: "center",
184
- marginHorizontal: 24,
185
- marginBottom: 24,
186
- lineHeight: 24,
187
- },
188
135
  promptContainer: {
189
136
  marginHorizontal: 24,
190
137
  marginTop: 16,
@@ -204,24 +151,7 @@ const styles = StyleSheet.create({
204
151
  promptHint: {
205
152
  marginTop: 8,
206
153
  },
207
- successText: {
208
- textAlign: "center",
209
- marginBottom: 24,
210
- },
211
- resultImageContainer: {
212
- alignItems: "center",
213
- marginHorizontal: 24,
214
- marginBottom: 24,
215
- },
216
154
  resultImage: {
217
155
  borderRadius: 16,
218
156
  },
219
- resultActions: {
220
- marginHorizontal: 24,
221
- gap: 12,
222
- },
223
- buttonContainer: {
224
- marginHorizontal: 24,
225
- marginTop: 8,
226
- },
227
157
  });
@@ -1,19 +1,15 @@
1
1
  /**
2
2
  * ReplaceBackgroundFeature Component
3
3
  * Self-contained background replacement feature UI component
4
- * Uses hook internally, only requires config and translations
4
+ * Uses centralized SingleImageWithPromptFeatureLayout for consistent UX
5
5
  */
6
6
 
7
- import React, { useCallback, useMemo } from "react";
8
- import { View, ScrollView, StyleSheet, Image } from "react-native";
9
- import {
10
- useAppDesignTokens,
11
- useResponsive,
12
- AtomicInput,
13
- } from "@umituz/react-native-design-system";
7
+ import React, { useMemo } from "react";
8
+ import { View, Image, StyleSheet } from "react-native";
9
+ import { AtomicInput } from "@umituz/react-native-design-system";
14
10
  import { PhotoUploadCard } from "../../../../presentation/components/PhotoUploadCard";
15
- import { AIGenerationForm } from "../../../../presentation/components/AIGenerationForm";
16
- import { AIGenerationResult } from "../../../../presentation/components/display/AIGenerationResult";
11
+ import { SingleImageWithPromptFeatureLayout } from "../../../../presentation/layouts";
12
+ import type { ProcessingModalRenderProps } from "../../../../presentation/layouts";
17
13
  import { useReplaceBackgroundFeature } from "../hooks";
18
14
  import type {
19
15
  ReplaceBackgroundTranslations,
@@ -22,15 +18,16 @@ import type {
22
18
 
23
19
  export interface ReplaceBackgroundFeatureProps {
24
20
  config: ReplaceBackgroundFeatureConfig;
25
- translations: ReplaceBackgroundTranslations;
21
+ translations: ReplaceBackgroundTranslations & {
22
+ modalTitle?: string;
23
+ modalMessage?: string;
24
+ modalHint?: string;
25
+ modalBackgroundHint?: string;
26
+ };
26
27
  onSelectImage: () => Promise<string | null>;
27
28
  onSaveImage: (imageUrl: string) => Promise<void>;
28
- /** Called before processing starts. Return false to cancel. */
29
29
  onBeforeProcess?: () => Promise<boolean>;
30
- renderProcessingModal?: (props: {
31
- visible: boolean;
32
- progress: number;
33
- }) => React.ReactNode;
30
+ renderProcessingModal?: (props: ProcessingModalRenderProps) => React.ReactNode;
34
31
  }
35
32
 
36
33
  export const ReplaceBackgroundFeature: React.FC<ReplaceBackgroundFeatureProps> = ({
@@ -41,10 +38,6 @@ export const ReplaceBackgroundFeature: React.FC<ReplaceBackgroundFeatureProps> =
41
38
  onBeforeProcess,
42
39
  renderProcessingModal,
43
40
  }) => {
44
- const tokens = useAppDesignTokens();
45
- const { width: screenWidth, horizontalPadding } = useResponsive();
46
- const imageSize = screenWidth - horizontalPadding * 2;
47
-
48
41
  const feature = useReplaceBackgroundFeature({
49
42
  config,
50
43
  onSelectImage,
@@ -52,77 +45,35 @@ export const ReplaceBackgroundFeature: React.FC<ReplaceBackgroundFeatureProps> =
52
45
  onBeforeProcess,
53
46
  });
54
47
 
55
- const photoTranslations = useMemo(
48
+ const modalTranslations = useMemo(
56
49
  () => ({
57
- tapToUpload: translations.uploadTitle,
58
- selectPhoto: translations.uploadSubtitle,
59
- change: translations.uploadChange,
60
- analyzing: translations.uploadAnalyzing,
50
+ title: translations.modalTitle || "Processing",
51
+ message: translations.modalMessage || "AI is replacing the background...",
52
+ hint: translations.modalHint || "This may take a moment",
53
+ backgroundHint: translations.modalBackgroundHint || "Continue in background",
61
54
  }),
62
55
  [translations],
63
56
  );
64
57
 
65
- const handleProcess = useCallback(() => {
66
- void feature.process();
67
- }, [feature]);
68
-
69
- const handleSave = useCallback(() => {
70
- void feature.save();
71
- }, [feature]);
72
-
73
- const handleSelectImage = useCallback(() => {
74
- void feature.selectImage();
75
- }, [feature]);
76
-
77
- if (feature.processedUrl) {
78
- return (
79
- <ScrollView
80
- style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
81
- contentContainerStyle={styles.content}
82
- showsVerticalScrollIndicator={false}
83
- >
84
- <AIGenerationResult
85
- successText={translations.successText}
86
- primaryAction={{
87
- label: translations.saveButtonText,
88
- onPress: handleSave,
89
- }}
90
- secondaryAction={{
91
- label: translations.tryAnotherText,
92
- onPress: feature.reset,
93
- }}
94
- >
95
- <Image
96
- source={{ uri: feature.processedUrl }}
97
- style={[styles.resultImage, { width: imageSize, height: imageSize }]}
98
- resizeMode="contain"
99
- />
100
- </AIGenerationResult>
101
- </ScrollView>
102
- );
103
- }
104
-
105
58
  return (
106
- <>
107
- <ScrollView
108
- style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
109
- contentContainerStyle={styles.content}
110
- showsVerticalScrollIndicator={false}
111
- >
112
- <AIGenerationForm
113
- onGenerate={handleProcess}
114
- isGenerating={feature.isProcessing}
115
- translations={{
116
- generateButton: translations.processButtonText,
117
- generatingButton: translations.processingText,
118
- }}
119
- >
59
+ <SingleImageWithPromptFeatureLayout
60
+ feature={feature}
61
+ translations={translations}
62
+ modalTranslations={modalTranslations}
63
+ renderProcessingModal={renderProcessingModal}
64
+ renderInput={({ imageUri, onSelect, isDisabled, isProcessing, prompt, onPromptChange }) => (
65
+ <>
120
66
  <PhotoUploadCard
121
- imageUri={feature.imageUri}
122
- onPress={handleSelectImage}
123
- isValidating={feature.isProcessing}
124
- disabled={feature.isProcessing}
125
- translations={photoTranslations}
67
+ imageUri={imageUri}
68
+ onPress={onSelect}
69
+ isValidating={isProcessing}
70
+ disabled={isDisabled}
71
+ translations={{
72
+ tapToUpload: translations.uploadTitle,
73
+ selectPhoto: translations.uploadSubtitle,
74
+ change: translations.uploadChange,
75
+ analyzing: translations.uploadAnalyzing,
76
+ }}
126
77
  config={{
127
78
  aspectRatio: 1,
128
79
  borderRadius: 24,
@@ -133,58 +84,34 @@ export const ReplaceBackgroundFeature: React.FC<ReplaceBackgroundFeatureProps> =
133
84
 
134
85
  <View style={styles.promptContainer}>
135
86
  <AtomicInput
136
- value={feature.prompt}
137
- onChangeText={feature.setPrompt}
87
+ value={prompt}
88
+ onChangeText={onPromptChange}
138
89
  placeholder={translations.promptPlaceholder}
139
90
  multiline
140
91
  numberOfLines={3}
141
- disabled={feature.isProcessing}
92
+ disabled={isProcessing}
142
93
  />
143
94
  </View>
144
- </AIGenerationForm>
145
- </ScrollView>
146
-
147
- {renderProcessingModal?.({ visible: feature.isProcessing, progress: feature.progress })}
148
- </>
95
+ </>
96
+ )}
97
+ renderResult={({ imageUrl, imageSize }) => (
98
+ <Image
99
+ source={{ uri: imageUrl }}
100
+ style={[styles.resultImage, { width: imageSize, height: imageSize }]}
101
+ resizeMode="contain"
102
+ />
103
+ )}
104
+ />
149
105
  );
150
106
  };
151
107
 
152
108
  const styles = StyleSheet.create({
153
- container: {
154
- flex: 1,
155
- },
156
- content: {
157
- paddingVertical: 16,
158
- },
159
- description: {
160
- textAlign: "center",
161
- marginHorizontal: 24,
162
- marginBottom: 24,
163
- lineHeight: 24,
164
- },
165
109
  promptContainer: {
166
110
  marginHorizontal: 24,
167
111
  marginTop: 16,
168
112
  marginBottom: 8,
169
113
  },
170
- successText: {
171
- textAlign: "center",
172
- marginBottom: 24,
173
- },
174
- resultImageContainer: {
175
- alignItems: "center",
176
- marginHorizontal: 24,
177
- marginBottom: 24,
178
- },
179
114
  resultImage: {
180
115
  borderRadius: 16,
181
116
  },
182
- resultActions: {
183
- marginHorizontal: 24,
184
- gap: 12,
185
- },
186
- buttonContainer: {
187
- marginHorizontal: 24,
188
- marginTop: 8,
189
- },
190
117
  });
@@ -1,17 +1,13 @@
1
1
  /**
2
2
  * UpscaleFeature Component
3
3
  * Self-contained upscale feature UI component
4
- * Uses hook internally, only requires config and translations
4
+ * Uses centralized SingleImageFeatureLayout for consistent UX
5
5
  */
6
6
 
7
- import React, { useCallback, useMemo } from "react";
8
- import { ScrollView, StyleSheet } from "react-native";
9
- import {
10
- useAppDesignTokens,
11
- AtomicText,
12
- } from "@umituz/react-native-design-system";
7
+ import React, { useMemo } from "react";
13
8
  import { PhotoUploadCard } from "../../../../presentation/components/PhotoUploadCard";
14
- import { AIGenerationForm } from "../../../../presentation/components/AIGenerationForm";
9
+ import { SingleImageFeatureLayout } from "../../../../presentation/layouts";
10
+ import type { ProcessingModalRenderProps } from "../../../../presentation/layouts";
15
11
  import { UpscaleResultView } from "./UpscaleResultView";
16
12
  import { useUpscaleFeature } from "../hooks";
17
13
  import type {
@@ -20,21 +16,17 @@ import type {
20
16
  } from "../../domain/types";
21
17
 
22
18
  export interface UpscaleFeatureProps {
23
- /** Feature configuration with provider-specific settings */
24
19
  config: UpscaleFeatureConfig;
25
- /** Translations for all UI text */
26
- translations: UpscaleTranslations;
27
- /** Image picker callback */
20
+ translations: UpscaleTranslations & {
21
+ modalTitle?: string;
22
+ modalMessage?: string;
23
+ modalHint?: string;
24
+ modalBackgroundHint?: string;
25
+ };
28
26
  onSelectImage: () => Promise<string | null>;
29
- /** Save image callback */
30
27
  onSaveImage: (imageUrl: string) => Promise<void>;
31
- /** Called before processing starts. Return false to cancel. */
32
28
  onBeforeProcess?: () => Promise<boolean>;
33
- /** Optional custom processing modal renderer */
34
- renderProcessingModal?: (props: {
35
- visible: boolean;
36
- progress: number;
37
- }) => React.ReactNode;
29
+ renderProcessingModal?: (props: ProcessingModalRenderProps) => React.ReactNode;
38
30
  }
39
31
 
40
32
  export const UpscaleFeature: React.FC<UpscaleFeatureProps> = ({
@@ -45,8 +37,6 @@ export const UpscaleFeature: React.FC<UpscaleFeatureProps> = ({
45
37
  onBeforeProcess,
46
38
  renderProcessingModal,
47
39
  }) => {
48
- const tokens = useAppDesignTokens();
49
-
50
40
  const feature = useUpscaleFeature({
51
41
  config,
52
42
  onSelectImage,
@@ -54,38 +44,47 @@ export const UpscaleFeature: React.FC<UpscaleFeatureProps> = ({
54
44
  onBeforeProcess,
55
45
  });
56
46
 
57
- const photoTranslations = useMemo(
47
+ const modalTranslations = useMemo(
58
48
  () => ({
59
- tapToUpload: translations.uploadTitle,
60
- selectPhoto: translations.uploadSubtitle,
61
- change: translations.uploadChange,
62
- analyzing: translations.uploadAnalyzing,
49
+ title: translations.modalTitle || "Processing",
50
+ message: translations.modalMessage || "AI is upscaling your image...",
51
+ hint: translations.modalHint || "This may take a moment",
52
+ backgroundHint: translations.modalBackgroundHint || "Continue in background",
63
53
  }),
64
54
  [translations],
65
55
  );
66
56
 
67
- const handleProcess = useCallback(() => {
68
- void feature.process();
69
- }, [feature]);
70
-
71
- const handleSave = useCallback(() => {
72
- void feature.save();
73
- }, [feature]);
74
-
75
- const handleSelectImage = useCallback(() => {
76
- void feature.selectImage();
77
- }, [feature]);
78
-
79
- if (feature.processedUrl && feature.imageUri) {
80
- return (
81
- <ScrollView
82
- style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
83
- contentContainerStyle={styles.content}
84
- showsVerticalScrollIndicator={false}
85
- >
57
+ return (
58
+ <SingleImageFeatureLayout
59
+ feature={feature}
60
+ translations={translations}
61
+ modalTranslations={modalTranslations}
62
+ description={translations.description}
63
+ renderProcessingModal={renderProcessingModal}
64
+ renderInput={({ imageUri, onSelect, isDisabled, isProcessing }) => (
65
+ <PhotoUploadCard
66
+ imageUri={imageUri}
67
+ onPress={onSelect}
68
+ isValidating={isProcessing}
69
+ disabled={isDisabled}
70
+ translations={{
71
+ tapToUpload: translations.uploadTitle,
72
+ selectPhoto: translations.uploadSubtitle,
73
+ change: translations.uploadChange,
74
+ analyzing: translations.uploadAnalyzing,
75
+ }}
76
+ config={{
77
+ aspectRatio: 1,
78
+ borderRadius: 24,
79
+ showValidationStatus: false,
80
+ allowChange: true,
81
+ }}
82
+ />
83
+ )}
84
+ renderCustomResult={({ processedUrl, originalImageUri, onSave, onReset }) => (
86
85
  <UpscaleResultView
87
- originalUri={feature.imageUri}
88
- processedUri={feature.processedUrl}
86
+ originalUri={originalImageUri}
87
+ processedUri={processedUrl}
89
88
  translations={{
90
89
  successText: translations.successText,
91
90
  saveButtonText: translations.saveButtonText,
@@ -93,79 +92,10 @@ export const UpscaleFeature: React.FC<UpscaleFeatureProps> = ({
93
92
  beforeLabel: translations.beforeLabel,
94
93
  afterLabel: translations.afterLabel,
95
94
  }}
96
- onSave={handleSave}
97
- onReset={feature.reset}
95
+ onSave={onSave}
96
+ onReset={onReset}
98
97
  />
99
- </ScrollView>
100
- );
101
- }
102
-
103
- return (
104
- <>
105
- <ScrollView
106
- style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
107
- contentContainerStyle={styles.content}
108
- showsVerticalScrollIndicator={false}
109
- >
110
- <AIGenerationForm
111
- onGenerate={handleProcess}
112
- isGenerating={feature.isProcessing}
113
- progress={feature.progress}
114
- translations={{
115
- generateButton: translations.processButtonText,
116
- generatingButton: translations.processingText,
117
- progressTitle: translations.processingText,
118
- }}
119
- >
120
- <AtomicText
121
- type="bodyLarge"
122
- style={[styles.description, { color: tokens.colors.textSecondary }]}
123
- >
124
- {translations.description}
125
- </AtomicText>
126
-
127
- <PhotoUploadCard
128
- imageUri={feature.imageUri}
129
- onPress={handleSelectImage}
130
- isValidating={feature.isProcessing}
131
- disabled={feature.isProcessing}
132
- translations={photoTranslations}
133
- config={{
134
- aspectRatio: 1,
135
- borderRadius: 24,
136
- showValidationStatus: false,
137
- allowChange: true,
138
- }}
139
- />
140
- </AIGenerationForm>
141
- </ScrollView>
142
-
143
- {renderProcessingModal?.({ visible: feature.isProcessing, progress: feature.progress })}
144
- </>
98
+ )}
99
+ />
145
100
  );
146
101
  };
147
-
148
- const styles = StyleSheet.create({
149
- container: {
150
- flex: 1,
151
- },
152
- content: {
153
- paddingVertical: 16,
154
- },
155
- description: {
156
- textAlign: "center",
157
- marginHorizontal: 24,
158
- marginBottom: 24,
159
- lineHeight: 24,
160
- },
161
- errorContainer: {
162
- marginHorizontal: 24,
163
- marginBottom: 16,
164
- padding: 16,
165
- borderRadius: 12,
166
- },
167
- buttonContainer: {
168
- marginHorizontal: 24,
169
- marginTop: 8,
170
- },
171
- });
package/src/index.ts CHANGED
@@ -92,10 +92,15 @@ export {
92
92
  ASPECT_RATIO_IDS, COMMON_DURATIONS,
93
93
  } from "./presentation/components";
94
94
 
95
- export { SingleImageFeatureLayout, DualImageFeatureLayout, DualImageVideoFeatureLayout } from "./presentation/layouts";
95
+ export {
96
+ SingleImageFeatureLayout, SingleImageWithPromptFeatureLayout,
97
+ DualImageFeatureLayout, DualImageVideoFeatureLayout,
98
+ } from "./presentation/layouts";
96
99
  export type {
97
100
  ModalTranslations, BaseLayoutTranslations, PhotoUploadTranslations,
98
- SingleImageInputRenderProps, DualImageInputRenderProps, ResultRenderProps,
101
+ SingleImageInputRenderProps, SingleImageWithPromptInputRenderProps,
102
+ SingleImageWithPromptFeatureState, SingleImageWithPromptFeatureLayoutProps,
103
+ DualImageInputRenderProps, ResultRenderProps, CustomResultRenderProps,
99
104
  ProcessingModalRenderProps, SingleImageFeatureLayoutProps, DualImageFeatureLayoutProps,
100
105
  DualImageVideoFeatureState, DualImageVideoFeatureLayoutProps,
101
106
  } from "./presentation/layouts";
@@ -23,6 +23,7 @@ export const SingleImageFeatureLayout: React.FC<SingleImageFeatureLayoutProps> =
23
23
  modalIcon = "sparkles",
24
24
  renderInput,
25
25
  renderResult,
26
+ renderCustomResult,
26
27
  description,
27
28
  renderProcessingModal,
28
29
  children,
@@ -57,8 +58,27 @@ export const SingleImageFeatureLayout: React.FC<SingleImageFeatureLayoutProps> =
57
58
  />
58
59
  );
59
60
 
60
- // Result view
61
- if (feature.processedUrl) {
61
+ // Custom result view (for comparison sliders, etc.)
62
+ if (feature.processedUrl && renderCustomResult && feature.imageUri) {
63
+ return (
64
+ <ScrollView
65
+ style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
66
+ contentContainerStyle={styles.content}
67
+ showsVerticalScrollIndicator={false}
68
+ >
69
+ {renderCustomResult({
70
+ processedUrl: feature.processedUrl,
71
+ originalImageUri: feature.imageUri,
72
+ imageSize,
73
+ onSave: handleSave,
74
+ onReset: feature.reset,
75
+ })}
76
+ </ScrollView>
77
+ );
78
+ }
79
+
80
+ // Standard result view with AIGenerationResult wrapper
81
+ if (feature.processedUrl && renderResult) {
62
82
  return (
63
83
  <ScrollView
64
84
  style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
@@ -0,0 +1,153 @@
1
+ /**
2
+ * SingleImageWithPromptFeatureLayout
3
+ * Centralized layout for single-image + prompt processing features
4
+ * (e.g., replace-background, remove-object)
5
+ * Handles: Modal, ScrollView, AIGenerationForm, AIGenerationResult
6
+ */
7
+
8
+ import React, { useCallback } from "react";
9
+ import { ScrollView, StyleSheet } from "react-native";
10
+ import {
11
+ useAppDesignTokens,
12
+ useResponsive,
13
+ AtomicText,
14
+ } from "@umituz/react-native-design-system";
15
+ import { AIGenerationForm } from "../components/AIGenerationForm";
16
+ import { AIGenerationResult } from "../components/display/AIGenerationResult";
17
+ import { GenerationProgressModal } from "../components/GenerationProgressModal";
18
+ import type { SingleImageWithPromptFeatureLayoutProps } from "./types";
19
+
20
+ export const SingleImageWithPromptFeatureLayout: React.FC<SingleImageWithPromptFeatureLayoutProps> = ({
21
+ feature,
22
+ translations,
23
+ modalTranslations,
24
+ modalIcon = "sparkles",
25
+ renderInput,
26
+ renderResult,
27
+ description,
28
+ renderProcessingModal,
29
+ children,
30
+ }) => {
31
+ const tokens = useAppDesignTokens();
32
+ const { width: screenWidth, horizontalPadding } = useResponsive();
33
+ const imageSize = screenWidth - horizontalPadding * 2;
34
+
35
+ const handleProcess = useCallback(() => {
36
+ void feature.process();
37
+ }, [feature]);
38
+
39
+ const handleSave = useCallback(() => {
40
+ void feature.save();
41
+ }, [feature]);
42
+
43
+ const handleSelectImage = useCallback(() => {
44
+ void feature.selectImage();
45
+ }, [feature]);
46
+
47
+ const handlePromptChange = useCallback(
48
+ (prompt: string) => {
49
+ feature.setPrompt(prompt);
50
+ },
51
+ [feature],
52
+ );
53
+
54
+ // Default modal
55
+ const defaultModal = (
56
+ <GenerationProgressModal
57
+ visible={feature.isProcessing}
58
+ progress={feature.progress}
59
+ icon={modalIcon}
60
+ title={modalTranslations.title}
61
+ message={modalTranslations.message}
62
+ hint={modalTranslations.hint}
63
+ backgroundHint={modalTranslations.backgroundHint}
64
+ onClose={() => {}}
65
+ />
66
+ );
67
+
68
+ // Result view
69
+ if (feature.processedUrl) {
70
+ return (
71
+ <ScrollView
72
+ style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
73
+ contentContainerStyle={styles.content}
74
+ showsVerticalScrollIndicator={false}
75
+ >
76
+ <AIGenerationResult
77
+ successText={translations.successText}
78
+ primaryAction={{
79
+ label: translations.saveButtonText,
80
+ onPress: handleSave,
81
+ }}
82
+ secondaryAction={{
83
+ label: translations.tryAnotherText,
84
+ onPress: feature.reset,
85
+ }}
86
+ >
87
+ {renderResult({ imageUrl: feature.processedUrl, imageSize })}
88
+ </AIGenerationResult>
89
+ </ScrollView>
90
+ );
91
+ }
92
+
93
+ // Input view
94
+ return (
95
+ <>
96
+ <ScrollView
97
+ style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
98
+ contentContainerStyle={styles.content}
99
+ showsVerticalScrollIndicator={false}
100
+ >
101
+ <AIGenerationForm
102
+ onGenerate={handleProcess}
103
+ isGenerating={feature.isProcessing}
104
+ progress={feature.progress}
105
+ translations={{
106
+ generateButton: translations.processButtonText,
107
+ generatingButton: translations.processingText,
108
+ progressTitle: translations.processingText,
109
+ }}
110
+ >
111
+ {description && (
112
+ <AtomicText
113
+ type="bodyLarge"
114
+ style={[styles.description, { color: tokens.colors.textSecondary }]}
115
+ >
116
+ {description}
117
+ </AtomicText>
118
+ )}
119
+
120
+ {children}
121
+
122
+ {renderInput({
123
+ imageUri: feature.imageUri,
124
+ onSelect: handleSelectImage,
125
+ isDisabled: feature.isProcessing,
126
+ isProcessing: feature.isProcessing,
127
+ prompt: feature.prompt,
128
+ onPromptChange: handlePromptChange,
129
+ })}
130
+ </AIGenerationForm>
131
+ </ScrollView>
132
+
133
+ {renderProcessingModal
134
+ ? renderProcessingModal({ visible: feature.isProcessing, progress: feature.progress })
135
+ : defaultModal}
136
+ </>
137
+ );
138
+ };
139
+
140
+ const styles = StyleSheet.create({
141
+ container: {
142
+ flex: 1,
143
+ },
144
+ content: {
145
+ paddingVertical: 16,
146
+ },
147
+ description: {
148
+ textAlign: "center",
149
+ marginHorizontal: 24,
150
+ marginBottom: 24,
151
+ lineHeight: 24,
152
+ },
153
+ });
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  export { SingleImageFeatureLayout } from "./SingleImageFeatureLayout";
7
+ export { SingleImageWithPromptFeatureLayout } from "./SingleImageWithPromptFeatureLayout";
7
8
  export { DualImageFeatureLayout } from "./DualImageFeatureLayout";
8
9
  export { DualImageVideoFeatureLayout } from "./DualImageVideoFeatureLayout";
9
10
  export type {
@@ -11,8 +12,12 @@ export type {
11
12
  BaseLayoutTranslations,
12
13
  PhotoUploadTranslations,
13
14
  SingleImageInputRenderProps,
15
+ SingleImageWithPromptInputRenderProps,
16
+ SingleImageWithPromptFeatureState,
17
+ SingleImageWithPromptFeatureLayoutProps,
14
18
  DualImageInputRenderProps,
15
19
  ResultRenderProps,
20
+ CustomResultRenderProps,
16
21
  ProcessingModalRenderProps,
17
22
  SingleImageFeatureLayoutProps,
18
23
  DualImageFeatureLayoutProps,
@@ -78,6 +78,17 @@ export interface ProcessingModalRenderProps {
78
78
  progress: number;
79
79
  }
80
80
 
81
+ /**
82
+ * Custom result render props (includes feature state for comparison views)
83
+ */
84
+ export interface CustomResultRenderProps {
85
+ processedUrl: string;
86
+ originalImageUri: string;
87
+ imageSize: number;
88
+ onSave: () => void;
89
+ onReset: () => void;
90
+ }
91
+
81
92
  /**
82
93
  * Single image feature layout props
83
94
  */
@@ -92,8 +103,10 @@ export interface SingleImageFeatureLayoutProps {
92
103
  modalIcon?: string;
93
104
  /** Render the input section (photo upload) */
94
105
  renderInput: (props: SingleImageInputRenderProps) => ReactNode;
95
- /** Render the result section */
96
- renderResult: (props: ResultRenderProps) => ReactNode;
106
+ /** Render the result section (wrapped with AIGenerationResult) */
107
+ renderResult?: (props: ResultRenderProps) => ReactNode;
108
+ /** Render a fully custom result section (no AIGenerationResult wrapper) */
109
+ renderCustomResult?: (props: CustomResultRenderProps) => ReactNode;
97
110
  /** Optional description text */
98
111
  description?: string;
99
112
  /** Optional custom processing modal */
@@ -164,3 +177,52 @@ export interface DualImageVideoFeatureLayoutProps {
164
177
  /** Optional children to render before the input */
165
178
  children?: ReactNode;
166
179
  }
180
+
181
+ /**
182
+ * Single image with prompt feature state
183
+ */
184
+ export interface SingleImageWithPromptFeatureState {
185
+ imageUri: string | null;
186
+ prompt: string;
187
+ processedUrl: string | null;
188
+ isProcessing: boolean;
189
+ progress: number;
190
+ error: string | null;
191
+ selectImage: () => Promise<void>;
192
+ setPrompt: (prompt: string) => void;
193
+ process: () => Promise<void>;
194
+ save: () => Promise<void>;
195
+ reset: () => void;
196
+ }
197
+
198
+ /**
199
+ * Input render props for single image with prompt
200
+ */
201
+ export interface SingleImageWithPromptInputRenderProps extends SingleImageInputRenderProps {
202
+ prompt: string;
203
+ onPromptChange: (prompt: string) => void;
204
+ }
205
+
206
+ /**
207
+ * Single image with prompt feature layout props
208
+ */
209
+ export interface SingleImageWithPromptFeatureLayoutProps {
210
+ /** Feature hook return */
211
+ feature: SingleImageWithPromptFeatureState;
212
+ /** UI translations */
213
+ translations: BaseLayoutTranslations & PhotoUploadTranslations;
214
+ /** Modal translations */
215
+ modalTranslations: ModalTranslations;
216
+ /** Modal icon */
217
+ modalIcon?: string;
218
+ /** Render the input section (photo upload + prompt) */
219
+ renderInput: (props: SingleImageWithPromptInputRenderProps) => ReactNode;
220
+ /** Render the result section */
221
+ renderResult: (props: ResultRenderProps) => ReactNode;
222
+ /** Optional description text */
223
+ description?: string;
224
+ /** Optional custom processing modal */
225
+ renderProcessingModal?: (props: ProcessingModalRenderProps) => ReactNode;
226
+ /** Optional children to render before the input */
227
+ children?: ReactNode;
228
+ }