@umituz/react-native-ai-generation-content 1.17.183 → 1.17.185
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/features/photo-restoration/presentation/components/PhotoRestoreFeature.tsx +49 -110
- package/src/features/remove-object/presentation/components/RemoveObjectFeature.tsx +47 -117
- package/src/features/replace-background/presentation/components/ReplaceBackgroundFeature.tsx +48 -121
- package/src/features/upscaling/presentation/components/UpscaleFeature.tsx +50 -120
- package/src/index.ts +7 -2
- package/src/presentation/layouts/SingleImageFeatureLayout.tsx +22 -2
- package/src/presentation/layouts/SingleImageWithPromptFeatureLayout.tsx +153 -0
- package/src/presentation/layouts/index.ts +5 -0
- package/src/presentation/layouts/types.ts +64 -2
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PhotoRestoreFeature Component
|
|
3
3
|
* Self-contained photo restore feature UI component
|
|
4
|
-
* Uses
|
|
4
|
+
* Uses centralized SingleImageFeatureLayout for consistent UX
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import 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 {
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
|
47
|
+
const modalTranslations = useMemo(
|
|
55
48
|
() => ({
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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={
|
|
85
|
-
processedUri={
|
|
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={
|
|
94
|
-
onReset={
|
|
94
|
+
onSave={onSave}
|
|
95
|
+
onReset={onReset}
|
|
95
96
|
/>
|
|
96
|
-
|
|
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
|
|
4
|
+
* Uses centralized SingleImageWithPromptFeatureLayout for consistent UX
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import React, {
|
|
8
|
-
import { View,
|
|
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 {
|
|
16
|
-
import {
|
|
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
|
|
53
|
+
const modalTranslations = useMemo(
|
|
56
54
|
() => ({
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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={
|
|
124
|
-
onPress={
|
|
125
|
-
isValidating={
|
|
126
|
-
disabled={
|
|
127
|
-
translations={
|
|
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={
|
|
153
|
-
onChangeText={
|
|
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={!
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
});
|
package/src/features/replace-background/presentation/components/ReplaceBackgroundFeature.tsx
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ReplaceBackgroundFeature Component
|
|
3
3
|
* Self-contained background replacement feature UI component
|
|
4
|
-
* Uses
|
|
4
|
+
* Uses centralized SingleImageWithPromptFeatureLayout for consistent UX
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import React, {
|
|
8
|
-
import { View,
|
|
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 {
|
|
16
|
-
import {
|
|
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
|
|
48
|
+
const modalTranslations = useMemo(
|
|
56
49
|
() => ({
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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={
|
|
122
|
-
onPress={
|
|
123
|
-
isValidating={
|
|
124
|
-
disabled={
|
|
125
|
-
translations={
|
|
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={
|
|
137
|
-
onChangeText={
|
|
87
|
+
value={prompt}
|
|
88
|
+
onChangeText={onPromptChange}
|
|
138
89
|
placeholder={translations.promptPlaceholder}
|
|
139
90
|
multiline
|
|
140
91
|
numberOfLines={3}
|
|
141
|
-
disabled={
|
|
92
|
+
disabled={isProcessing}
|
|
142
93
|
/>
|
|
143
94
|
</View>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
4
|
+
* Uses centralized SingleImageFeatureLayout for consistent UX
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import 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 {
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
47
|
+
const modalTranslations = useMemo(
|
|
58
48
|
() => ({
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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={
|
|
88
|
-
processedUri={
|
|
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={
|
|
97
|
-
onReset={
|
|
95
|
+
onSave={onSave}
|
|
96
|
+
onReset={onReset}
|
|
98
97
|
/>
|
|
99
|
-
|
|
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 {
|
|
95
|
+
export {
|
|
96
|
+
SingleImageFeatureLayout, SingleImageWithPromptFeatureLayout,
|
|
97
|
+
DualImageFeatureLayout, DualImageVideoFeatureLayout,
|
|
98
|
+
} from "./presentation/layouts";
|
|
96
99
|
export type {
|
|
97
100
|
ModalTranslations, BaseLayoutTranslations, PhotoUploadTranslations,
|
|
98
|
-
SingleImageInputRenderProps,
|
|
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
|
-
//
|
|
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
|
|
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
|
+
}
|