@umituz/react-native-ai-generation-content 1.83.90 → 1.83.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/domains/creations/presentation/components/CreationCardCompact.tsx +72 -0
- package/src/domains/creations/presentation/components/CreationImagePreview.tsx +18 -5
- package/src/domains/creations/presentation/components/GalleryHeader.tsx +81 -23
- package/src/domains/creations/presentation/components/index.ts +2 -0
- package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +42 -10
- package/src/domains/creations/presentation/screens/creations-gallery.styles.ts +9 -0
- package/src/domains/generation/wizard/presentation/hooks/usePhotoBlockingGeneration.ts +0 -1
- package/src/domains/generation/wizard/presentation/screens/GeneratingScreen.tsx +131 -16
- package/src/presentation/components/display/VideoResultPlayer.tsx +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.83.
|
|
3
|
+
"version": "1.83.91",
|
|
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",
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CreationCardCompact Component
|
|
3
|
+
* Compact 2-column grid card — image fills the card, badge overlay only
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo, useCallback } from "react";
|
|
7
|
+
import { View, StyleSheet, TouchableOpacity } from "react-native";
|
|
8
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
9
|
+
import { CreationPreview } from "./CreationPreview";
|
|
10
|
+
import { CreationBadges } from "./CreationBadges";
|
|
11
|
+
import { getPreviewUrl } from "../../domain/utils";
|
|
12
|
+
import type { CreationCardData, CreationCardCallbacks } from "./CreationCard.types";
|
|
13
|
+
import type { CreationTypeId } from "../../domain/types";
|
|
14
|
+
|
|
15
|
+
interface CreationCardCompactProps {
|
|
16
|
+
readonly creation: CreationCardData;
|
|
17
|
+
readonly callbacks?: CreationCardCallbacks;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function CreationCardCompact({ creation, callbacks }: CreationCardCompactProps) {
|
|
21
|
+
const tokens = useAppDesignTokens();
|
|
22
|
+
const previewUrl = getPreviewUrl(creation.output) || creation.uri;
|
|
23
|
+
|
|
24
|
+
const handlePress = useCallback(() => {
|
|
25
|
+
callbacks?.onPress?.(creation);
|
|
26
|
+
}, [callbacks, creation]);
|
|
27
|
+
|
|
28
|
+
const styles = useMemo(
|
|
29
|
+
() =>
|
|
30
|
+
StyleSheet.create({
|
|
31
|
+
card: {
|
|
32
|
+
flex: 1,
|
|
33
|
+
borderRadius: 12,
|
|
34
|
+
overflow: "hidden",
|
|
35
|
+
aspectRatio: 3 / 4,
|
|
36
|
+
backgroundColor: tokens.colors.surface,
|
|
37
|
+
},
|
|
38
|
+
previewWrapper: {
|
|
39
|
+
width: "100%",
|
|
40
|
+
height: "100%",
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
[tokens],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<TouchableOpacity
|
|
48
|
+
style={styles.card}
|
|
49
|
+
onPress={handlePress}
|
|
50
|
+
activeOpacity={callbacks?.onPress ? 0.85 : 1}
|
|
51
|
+
disabled={!callbacks?.onPress}
|
|
52
|
+
>
|
|
53
|
+
<View style={styles.previewWrapper}>
|
|
54
|
+
<CreationPreview
|
|
55
|
+
uri={previewUrl}
|
|
56
|
+
thumbnailUrl={creation.output?.thumbnailUrl}
|
|
57
|
+
status={creation.status}
|
|
58
|
+
type={creation.type as CreationTypeId}
|
|
59
|
+
aspectRatio={3 / 4}
|
|
60
|
+
showLoadingIndicator
|
|
61
|
+
/>
|
|
62
|
+
</View>
|
|
63
|
+
{creation.status && (
|
|
64
|
+
<CreationBadges
|
|
65
|
+
status={creation.status}
|
|
66
|
+
type={creation.type as CreationTypeId}
|
|
67
|
+
showType={false}
|
|
68
|
+
/>
|
|
69
|
+
)}
|
|
70
|
+
</TouchableOpacity>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Uses expo-image for caching and performance
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import React, { useMemo, useState, useCallback } from "react";
|
|
8
|
-
import { View, StyleSheet } from "react-native";
|
|
7
|
+
import React, { useMemo, useState, useCallback, useRef, useEffect } from "react";
|
|
8
|
+
import { View, StyleSheet, Animated } from "react-native";
|
|
9
9
|
import { AtomicIcon, AtomicSpinner } from "@umituz/react-native-design-system/atoms";
|
|
10
10
|
import { AtomicImage } from "@umituz/react-native-design-system/image";
|
|
11
11
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
@@ -40,6 +40,19 @@ export function CreationImagePreview({
|
|
|
40
40
|
const typeIcon = getTypeIcon(type);
|
|
41
41
|
const [imageError, setImageError] = useState(false);
|
|
42
42
|
const [isLoading, setIsLoading] = useState(true);
|
|
43
|
+
const pulseAnim = useRef(new Animated.Value(0.4)).current;
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!inProgress) return;
|
|
47
|
+
const animation = Animated.loop(
|
|
48
|
+
Animated.sequence([
|
|
49
|
+
Animated.timing(pulseAnim, { toValue: 1, duration: 1100, useNativeDriver: true }),
|
|
50
|
+
Animated.timing(pulseAnim, { toValue: 0.4, duration: 1100, useNativeDriver: true }),
|
|
51
|
+
]),
|
|
52
|
+
);
|
|
53
|
+
animation.start();
|
|
54
|
+
return () => animation.stop();
|
|
55
|
+
}, [inProgress, pulseAnim]);
|
|
43
56
|
|
|
44
57
|
const hasPreview = !!uri && !inProgress && !imageError;
|
|
45
58
|
|
|
@@ -91,15 +104,15 @@ export function CreationImagePreview({
|
|
|
91
104
|
[tokens, aspectRatio, height]
|
|
92
105
|
);
|
|
93
106
|
|
|
94
|
-
// Show
|
|
107
|
+
// Show pulsing shimmer during generation
|
|
95
108
|
if (inProgress && showLoadingIndicator) {
|
|
96
109
|
return (
|
|
97
110
|
<View style={styles.container}>
|
|
98
|
-
<View style={styles.loadingOverlay}>
|
|
111
|
+
<Animated.View style={[styles.loadingOverlay, { opacity: pulseAnim, backgroundColor: tokens.colors.backgroundSecondary }]}>
|
|
99
112
|
<View style={styles.loadingIcon}>
|
|
100
113
|
<AtomicSpinner size="lg" color="primary" />
|
|
101
114
|
</View>
|
|
102
|
-
</View>
|
|
115
|
+
</Animated.View>
|
|
103
116
|
</View>
|
|
104
117
|
);
|
|
105
118
|
}
|
|
@@ -19,6 +19,8 @@ interface GalleryHeaderProps {
|
|
|
19
19
|
readonly filterButtons?: FilterButtonConfig[];
|
|
20
20
|
readonly showFilter?: boolean;
|
|
21
21
|
readonly style?: ViewStyle;
|
|
22
|
+
readonly viewMode?: "list" | "grid";
|
|
23
|
+
readonly onViewModeChange?: (mode: "list" | "grid") => void;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
const EMPTY_FILTER_BUTTONS: FilterButtonConfig[] = [];
|
|
@@ -29,13 +31,15 @@ export const GalleryHeader: React.FC<GalleryHeaderProps> = ({
|
|
|
29
31
|
filterButtons = EMPTY_FILTER_BUTTONS,
|
|
30
32
|
showFilter = true,
|
|
31
33
|
style,
|
|
34
|
+
viewMode = "list",
|
|
35
|
+
onViewModeChange,
|
|
32
36
|
}: GalleryHeaderProps) => {
|
|
33
37
|
const tokens = useAppDesignTokens();
|
|
34
38
|
const styles = useStyles(tokens);
|
|
35
39
|
|
|
36
40
|
return (
|
|
37
41
|
<View style={[styles.headerArea, style]}>
|
|
38
|
-
<View>
|
|
42
|
+
<View style={styles.leftSection}>
|
|
39
43
|
{title ? (
|
|
40
44
|
<View style={styles.titleRow}>
|
|
41
45
|
<AtomicText style={styles.title}>{title}</AtomicText>
|
|
@@ -45,35 +49,64 @@ export const GalleryHeader: React.FC<GalleryHeaderProps> = ({
|
|
|
45
49
|
{countLabel}
|
|
46
50
|
</AtomicText>
|
|
47
51
|
</View>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
|
|
53
|
+
<View style={styles.rightSection}>
|
|
54
|
+
{showFilter && filterButtons.length > 0 && (
|
|
55
|
+
<View style={styles.filterRow}>
|
|
56
|
+
{filterButtons.map((btn: FilterButtonConfig) => (
|
|
57
|
+
<TouchableOpacity
|
|
58
|
+
key={btn.id}
|
|
59
|
+
onPress={() => {
|
|
60
|
+
if (__DEV__) {
|
|
61
|
+
console.log(`[GalleryHeader] ${btn.id} filter pressed`);
|
|
62
|
+
}
|
|
63
|
+
btn.onPress();
|
|
64
|
+
}}
|
|
65
|
+
style={[styles.filterButton, btn.isActive && styles.filterButtonActive]}
|
|
66
|
+
activeOpacity={0.7}
|
|
67
|
+
>
|
|
68
|
+
<AtomicIcon
|
|
69
|
+
name={btn.icon}
|
|
70
|
+
size="sm"
|
|
71
|
+
color={btn.isActive ? "primary" : "secondary"}
|
|
72
|
+
/>
|
|
73
|
+
<AtomicText
|
|
74
|
+
style={[styles.filterText, { color: btn.isActive ? tokens.colors.primary : tokens.colors.textSecondary }]}
|
|
75
|
+
>
|
|
76
|
+
{btn.label}
|
|
77
|
+
</AtomicText>
|
|
78
|
+
</TouchableOpacity>
|
|
79
|
+
))}
|
|
80
|
+
</View>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
{onViewModeChange && (
|
|
84
|
+
<View style={styles.viewToggle}>
|
|
51
85
|
<TouchableOpacity
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (__DEV__) {
|
|
55
|
-
|
|
56
|
-
console.log(`[GalleryHeader] ${btn.id} filter pressed`);
|
|
57
|
-
}
|
|
58
|
-
btn.onPress();
|
|
59
|
-
}}
|
|
60
|
-
style={[styles.filterButton, btn.isActive && styles.filterButtonActive]}
|
|
86
|
+
onPress={() => onViewModeChange("list")}
|
|
87
|
+
style={[styles.toggleBtn, viewMode === "list" && styles.toggleBtnActive]}
|
|
61
88
|
activeOpacity={0.7}
|
|
62
89
|
>
|
|
63
90
|
<AtomicIcon
|
|
64
|
-
name=
|
|
91
|
+
name="list-outline"
|
|
65
92
|
size="sm"
|
|
66
|
-
color={
|
|
93
|
+
color={viewMode === "list" ? "primary" : "secondary"}
|
|
67
94
|
/>
|
|
68
|
-
<AtomicText
|
|
69
|
-
style={[styles.filterText, { color: btn.isActive ? tokens.colors.primary : tokens.colors.textSecondary }]}
|
|
70
|
-
>
|
|
71
|
-
{btn.label}
|
|
72
|
-
</AtomicText>
|
|
73
95
|
</TouchableOpacity>
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
96
|
+
<TouchableOpacity
|
|
97
|
+
onPress={() => onViewModeChange("grid")}
|
|
98
|
+
style={[styles.toggleBtn, viewMode === "grid" && styles.toggleBtnActive]}
|
|
99
|
+
activeOpacity={0.7}
|
|
100
|
+
>
|
|
101
|
+
<AtomicIcon
|
|
102
|
+
name="grid-outline"
|
|
103
|
+
size="sm"
|
|
104
|
+
color={viewMode === "grid" ? "primary" : "secondary"}
|
|
105
|
+
/>
|
|
106
|
+
</TouchableOpacity>
|
|
107
|
+
</View>
|
|
108
|
+
)}
|
|
109
|
+
</View>
|
|
77
110
|
</View>
|
|
78
111
|
);
|
|
79
112
|
};
|
|
@@ -88,6 +121,14 @@ const useStyles = (tokens: DesignTokens) =>
|
|
|
88
121
|
paddingVertical: tokens.spacing.sm,
|
|
89
122
|
marginBottom: tokens.spacing.sm,
|
|
90
123
|
},
|
|
124
|
+
leftSection: {
|
|
125
|
+
flex: 1,
|
|
126
|
+
},
|
|
127
|
+
rightSection: {
|
|
128
|
+
flexDirection: "row",
|
|
129
|
+
alignItems: "center",
|
|
130
|
+
gap: tokens.spacing.sm,
|
|
131
|
+
},
|
|
91
132
|
titleRow: {
|
|
92
133
|
flexDirection: "row",
|
|
93
134
|
alignItems: "center",
|
|
@@ -127,4 +168,21 @@ const useStyles = (tokens: DesignTokens) =>
|
|
|
127
168
|
fontSize: 13,
|
|
128
169
|
fontWeight: "500",
|
|
129
170
|
},
|
|
171
|
+
viewToggle: {
|
|
172
|
+
flexDirection: "row",
|
|
173
|
+
gap: 2,
|
|
174
|
+
backgroundColor: tokens.colors.surfaceVariant,
|
|
175
|
+
borderRadius: 8,
|
|
176
|
+
padding: 2,
|
|
177
|
+
},
|
|
178
|
+
toggleBtn: {
|
|
179
|
+
width: 32,
|
|
180
|
+
height: 32,
|
|
181
|
+
borderRadius: 6,
|
|
182
|
+
justifyContent: "center",
|
|
183
|
+
alignItems: "center",
|
|
184
|
+
},
|
|
185
|
+
toggleBtnActive: {
|
|
186
|
+
backgroundColor: tokens.colors.surface,
|
|
187
|
+
},
|
|
130
188
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useMemo, useCallback } from "react";
|
|
1
|
+
import React, { useMemo, useCallback, useState } from "react";
|
|
2
2
|
import { View } from "react-native";
|
|
3
3
|
import { ScreenLayout } from "@umituz/react-native-design-system/layouts";
|
|
4
4
|
import { FilterSheet, useAppFocusEffect } from "@umituz/react-native-design-system/molecules";
|
|
@@ -9,7 +9,7 @@ import { useProcessingJobsPoller } from "../hooks/useProcessingJobsPoller";
|
|
|
9
9
|
import { useGalleryFilters } from "../hooks/useGalleryFilters";
|
|
10
10
|
import { useGalleryCallbacks } from "../hooks/useGalleryCallbacks";
|
|
11
11
|
import { useGalleryState } from "../hooks/useGalleryState";
|
|
12
|
-
import { GalleryHeader, CreationCard, GalleryEmptyStates } from "../components";
|
|
12
|
+
import { GalleryHeader, CreationCard, CreationCardCompact, GalleryEmptyStates } from "../components";
|
|
13
13
|
import { GalleryResultPreview } from "../components/GalleryResultPreview";
|
|
14
14
|
import { GalleryScreenHeader } from "../components/GalleryScreenHeader";
|
|
15
15
|
import { MEDIA_FILTER_OPTIONS, STATUS_FILTER_OPTIONS } from "../../domain/types/creation-filter";
|
|
@@ -34,6 +34,7 @@ export function CreationsGalleryScreen({
|
|
|
34
34
|
onCreationPress,
|
|
35
35
|
}: CreationsGalleryScreenProps) {
|
|
36
36
|
const tokens = useAppDesignTokens();
|
|
37
|
+
const [viewMode, setViewMode] = useState<"list" | "grid">("list");
|
|
37
38
|
|
|
38
39
|
const { data: creations, isLoading, refetch } = useCreations({ userId, repository });
|
|
39
40
|
const deleteMutation = useDeleteCreation({ userId, repository });
|
|
@@ -89,18 +90,43 @@ export function CreationsGalleryScreen({
|
|
|
89
90
|
[config.types, t, getCreationTitle]
|
|
90
91
|
);
|
|
91
92
|
|
|
93
|
+
const getItemCallbacks = useCallback((item: Creation) => ({
|
|
94
|
+
onPress: () => onCreationPress ? onCreationPress(item) : callbacks.handleCardPress(item),
|
|
95
|
+
onShare: async () => callbacks.handleShareCard(item),
|
|
96
|
+
onDelete: () => callbacks.handleDelete(item),
|
|
97
|
+
onFavorite: () => callbacks.handleFavorite(item),
|
|
98
|
+
}), [callbacks, onCreationPress]);
|
|
99
|
+
|
|
92
100
|
const renderItem = useCallback(({ item }: { item: Creation }) => (
|
|
93
101
|
<CreationCard
|
|
94
102
|
creation={item}
|
|
95
103
|
titleText={getItemTitle(item)}
|
|
96
|
-
callbacks={
|
|
97
|
-
onPress: () => onCreationPress ? onCreationPress(item) : callbacks.handleCardPress(item),
|
|
98
|
-
onShare: async () => callbacks.handleShareCard(item),
|
|
99
|
-
onDelete: () => callbacks.handleDelete(item),
|
|
100
|
-
onFavorite: () => callbacks.handleFavorite(item),
|
|
101
|
-
}}
|
|
104
|
+
callbacks={getItemCallbacks(item)}
|
|
102
105
|
/>
|
|
103
|
-
), [
|
|
106
|
+
), [getItemTitle, getItemCallbacks]);
|
|
107
|
+
|
|
108
|
+
const renderGridItems = useCallback((items: Creation[]) => {
|
|
109
|
+
const rows: Array<{ left: Creation; right: Creation | null }> = [];
|
|
110
|
+
for (let i = 0; i < items.length; i += 2) {
|
|
111
|
+
rows.push({ left: items[i], right: items[i + 1] ?? null });
|
|
112
|
+
}
|
|
113
|
+
return rows.map((row, index) => (
|
|
114
|
+
<View key={`grid-row-${index}`} style={styles.gridRow}>
|
|
115
|
+
<CreationCardCompact
|
|
116
|
+
creation={row.left}
|
|
117
|
+
callbacks={{ onPress: getItemCallbacks(row.left).onPress }}
|
|
118
|
+
/>
|
|
119
|
+
{row.right ? (
|
|
120
|
+
<CreationCardCompact
|
|
121
|
+
creation={row.right}
|
|
122
|
+
callbacks={{ onPress: getItemCallbacks(row.right).onPress }}
|
|
123
|
+
/>
|
|
124
|
+
) : (
|
|
125
|
+
<View style={styles.gridPlaceholder} />
|
|
126
|
+
)}
|
|
127
|
+
</View>
|
|
128
|
+
));
|
|
129
|
+
}, [getItemCallbacks]);
|
|
104
130
|
|
|
105
131
|
const hasScreenHeader = Boolean(onBack);
|
|
106
132
|
|
|
@@ -115,10 +141,12 @@ export function CreationsGalleryScreen({
|
|
|
115
141
|
countLabel={`${filters.filtered.length} ${t(config.translations.photoCount)}`}
|
|
116
142
|
showFilter={showFilter}
|
|
117
143
|
filterButtons={filterButtons}
|
|
144
|
+
viewMode={viewMode}
|
|
145
|
+
onViewModeChange={setViewMode}
|
|
118
146
|
/>
|
|
119
147
|
</View>
|
|
120
148
|
);
|
|
121
|
-
}, [creations, isLoading, filters.filtered.length, showFilter, filterButtons, t, config, tokens, hasScreenHeader]);
|
|
149
|
+
}, [creations, isLoading, filters.filtered.length, showFilter, filterButtons, t, config, tokens, hasScreenHeader, viewMode]);
|
|
122
150
|
|
|
123
151
|
const renderEmpty = useMemo(() => (
|
|
124
152
|
<GalleryEmptyStates
|
|
@@ -165,6 +193,10 @@ export function CreationsGalleryScreen({
|
|
|
165
193
|
<View style={[styles.listContent, styles.emptyContent]}>
|
|
166
194
|
{renderEmpty}
|
|
167
195
|
</View>
|
|
196
|
+
) : viewMode === "grid" ? (
|
|
197
|
+
<View style={styles.gridContent}>
|
|
198
|
+
{renderGridItems(filters.filtered)}
|
|
199
|
+
</View>
|
|
168
200
|
) : (
|
|
169
201
|
<View style={styles.listContent}>
|
|
170
202
|
{filters.filtered.map((item) => (
|
|
@@ -7,7 +7,16 @@ import { StyleSheet } from "react-native";
|
|
|
7
7
|
export const creationsGalleryStyles = StyleSheet.create({
|
|
8
8
|
header: { borderBottomWidth: 1 },
|
|
9
9
|
listContent: { paddingHorizontal: 16, paddingTop: 16 },
|
|
10
|
+
gridContent: { paddingHorizontal: 12, paddingTop: 12 },
|
|
10
11
|
emptyContent: { flexGrow: 1 },
|
|
12
|
+
gridRow: {
|
|
13
|
+
flexDirection: "row",
|
|
14
|
+
gap: 8,
|
|
15
|
+
marginBottom: 8,
|
|
16
|
+
},
|
|
17
|
+
gridPlaceholder: {
|
|
18
|
+
flex: 1,
|
|
19
|
+
},
|
|
11
20
|
screenHeader: {
|
|
12
21
|
flexDirection: "row",
|
|
13
22
|
alignItems: "center",
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* Supports background generation - user can dismiss and generation continues
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import React, { useMemo } from "react";
|
|
9
|
-
import { View, StyleSheet, ActivityIndicator, TouchableOpacity } from "react-native";
|
|
8
|
+
import React, { useMemo, useRef, useEffect } from "react";
|
|
9
|
+
import { View, StyleSheet, ActivityIndicator, TouchableOpacity, Animated } from "react-native";
|
|
10
10
|
import { AtomicText } from "@umituz/react-native-design-system/atoms";
|
|
11
11
|
import { ScreenLayout } from "@umituz/react-native-design-system/layouts";
|
|
12
12
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
@@ -30,6 +30,18 @@ interface GeneratingScreenProps {
|
|
|
30
30
|
readonly onDismiss?: () => void;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
const PHASES = [
|
|
34
|
+
{ id: "queued", index: 0 },
|
|
35
|
+
{ id: "processing", index: 1 },
|
|
36
|
+
{ id: "finalizing", index: 2 },
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
const PHASE_LABELS = {
|
|
40
|
+
queued: "Analyzing",
|
|
41
|
+
processing: "Creating",
|
|
42
|
+
finalizing: "Refining",
|
|
43
|
+
} as const;
|
|
44
|
+
|
|
33
45
|
export const GeneratingScreen: React.FC<GeneratingScreenProps> = ({
|
|
34
46
|
progress: _progress,
|
|
35
47
|
scenario,
|
|
@@ -38,6 +50,18 @@ export const GeneratingScreen: React.FC<GeneratingScreenProps> = ({
|
|
|
38
50
|
}) => {
|
|
39
51
|
const tokens = useAppDesignTokens();
|
|
40
52
|
const phase = useGenerationPhase();
|
|
53
|
+
const pulseAnim = useRef(new Animated.Value(0.6)).current;
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const animation = Animated.loop(
|
|
57
|
+
Animated.sequence([
|
|
58
|
+
Animated.timing(pulseAnim, { toValue: 1, duration: 900, useNativeDriver: true }),
|
|
59
|
+
Animated.timing(pulseAnim, { toValue: 0.6, duration: 900, useNativeDriver: true }),
|
|
60
|
+
]),
|
|
61
|
+
);
|
|
62
|
+
animation.start();
|
|
63
|
+
return () => animation.stop();
|
|
64
|
+
}, [pulseAnim]);
|
|
41
65
|
|
|
42
66
|
const messages = useMemo(() => {
|
|
43
67
|
const custom = scenario?.generatingMessages;
|
|
@@ -52,26 +76,84 @@ export const GeneratingScreen: React.FC<GeneratingScreenProps> = ({
|
|
|
52
76
|
const statusMessage = useMemo(() => {
|
|
53
77
|
switch (phase) {
|
|
54
78
|
case "queued":
|
|
55
|
-
return t("generator.status.queued") || "
|
|
79
|
+
return t("generator.status.queued") || "Analyzing your photos...";
|
|
56
80
|
case "processing":
|
|
57
|
-
return t("generator.status.processing") || "
|
|
81
|
+
return t("generator.status.processing") || "Creating your scene...";
|
|
58
82
|
case "finalizing":
|
|
59
|
-
return t("generator.status.finalizing") || "
|
|
83
|
+
return t("generator.status.finalizing") || "Adding finishing touches...";
|
|
60
84
|
default:
|
|
61
85
|
return messages.waitMessage;
|
|
62
86
|
}
|
|
63
87
|
}, [phase, t, messages.waitMessage]);
|
|
64
88
|
|
|
89
|
+
const activePhaseIndex = PHASES.find((p) => p.id === phase)?.index ?? 0;
|
|
90
|
+
|
|
65
91
|
return (
|
|
66
92
|
<ScreenLayout backgroundColor={tokens.colors.backgroundPrimary}>
|
|
67
93
|
<View style={styles.container}>
|
|
68
94
|
<View style={styles.content}>
|
|
69
|
-
|
|
95
|
+
{/* Pulsing spinner */}
|
|
96
|
+
<Animated.View style={[styles.spinnerContainer, { opacity: pulseAnim, backgroundColor: tokens.colors.primary + "15" }]}>
|
|
97
|
+
<ActivityIndicator size="large" color={tokens.colors.primary} />
|
|
98
|
+
</Animated.View>
|
|
70
99
|
|
|
71
100
|
<AtomicText type="headlineMedium" style={styles.title}>
|
|
72
101
|
{messages.title}
|
|
73
102
|
</AtomicText>
|
|
74
103
|
|
|
104
|
+
{/* Phase step bubbles */}
|
|
105
|
+
<View style={styles.stepsRow}>
|
|
106
|
+
{PHASES.map((p, idx) => {
|
|
107
|
+
const isActive = idx === activePhaseIndex;
|
|
108
|
+
const isDone = idx < activePhaseIndex;
|
|
109
|
+
return (
|
|
110
|
+
<React.Fragment key={p.id}>
|
|
111
|
+
<View style={styles.stepItem}>
|
|
112
|
+
<View
|
|
113
|
+
style={[
|
|
114
|
+
styles.stepBubble,
|
|
115
|
+
{
|
|
116
|
+
backgroundColor: isDone || isActive
|
|
117
|
+
? tokens.colors.primary
|
|
118
|
+
: tokens.colors.surfaceVariant,
|
|
119
|
+
borderColor: isActive ? tokens.colors.primary : "transparent",
|
|
120
|
+
},
|
|
121
|
+
]}
|
|
122
|
+
>
|
|
123
|
+
<AtomicText
|
|
124
|
+
style={[
|
|
125
|
+
styles.stepNumber,
|
|
126
|
+
{ color: isDone || isActive ? "#FFFFFF" : tokens.colors.textSecondary },
|
|
127
|
+
]}
|
|
128
|
+
>
|
|
129
|
+
{isDone ? "✓" : String(idx + 1)}
|
|
130
|
+
</AtomicText>
|
|
131
|
+
</View>
|
|
132
|
+
<AtomicText
|
|
133
|
+
style={[
|
|
134
|
+
styles.stepLabel,
|
|
135
|
+
{
|
|
136
|
+
color: isActive ? tokens.colors.primary : tokens.colors.textSecondary,
|
|
137
|
+
fontWeight: isActive ? "700" : "400",
|
|
138
|
+
},
|
|
139
|
+
]}
|
|
140
|
+
>
|
|
141
|
+
{PHASE_LABELS[p.id]}
|
|
142
|
+
</AtomicText>
|
|
143
|
+
</View>
|
|
144
|
+
{idx < PHASES.length - 1 && (
|
|
145
|
+
<View
|
|
146
|
+
style={[
|
|
147
|
+
styles.stepConnector,
|
|
148
|
+
{ backgroundColor: idx < activePhaseIndex ? tokens.colors.primary : tokens.colors.surfaceVariant },
|
|
149
|
+
]}
|
|
150
|
+
/>
|
|
151
|
+
)}
|
|
152
|
+
</React.Fragment>
|
|
153
|
+
);
|
|
154
|
+
})}
|
|
155
|
+
</View>
|
|
156
|
+
|
|
75
157
|
<AtomicText type="bodyMedium" style={[styles.message, { color: tokens.colors.textSecondary }]}>
|
|
76
158
|
{statusMessage}
|
|
77
159
|
</AtomicText>
|
|
@@ -83,12 +165,6 @@ export const GeneratingScreen: React.FC<GeneratingScreenProps> = ({
|
|
|
83
165
|
/>
|
|
84
166
|
</View>
|
|
85
167
|
|
|
86
|
-
{scenario && (
|
|
87
|
-
<AtomicText type="bodySmall" style={[styles.hint, { color: tokens.colors.textSecondary }]}>
|
|
88
|
-
{scenario.title || scenario.id}
|
|
89
|
-
</AtomicText>
|
|
90
|
-
)}
|
|
91
|
-
|
|
92
168
|
<AtomicText type="bodySmall" style={[styles.hint, { color: tokens.colors.textSecondary }]}>
|
|
93
169
|
{messages.hint}
|
|
94
170
|
</AtomicText>
|
|
@@ -116,25 +192,64 @@ const styles = StyleSheet.create({
|
|
|
116
192
|
alignItems: "center",
|
|
117
193
|
},
|
|
118
194
|
content: {
|
|
119
|
-
width: "
|
|
195
|
+
width: "85%",
|
|
120
196
|
maxWidth: 400,
|
|
121
197
|
alignItems: "center",
|
|
122
198
|
gap: 16,
|
|
123
199
|
},
|
|
200
|
+
spinnerContainer: {
|
|
201
|
+
width: 80,
|
|
202
|
+
height: 80,
|
|
203
|
+
borderRadius: 40,
|
|
204
|
+
justifyContent: "center",
|
|
205
|
+
alignItems: "center",
|
|
206
|
+
marginBottom: 8,
|
|
207
|
+
},
|
|
124
208
|
title: {
|
|
125
209
|
textAlign: "center",
|
|
126
|
-
|
|
210
|
+
},
|
|
211
|
+
stepsRow: {
|
|
212
|
+
flexDirection: "row",
|
|
213
|
+
alignItems: "center",
|
|
214
|
+
justifyContent: "center",
|
|
215
|
+
marginVertical: 8,
|
|
216
|
+
},
|
|
217
|
+
stepItem: {
|
|
218
|
+
alignItems: "center",
|
|
219
|
+
gap: 6,
|
|
220
|
+
},
|
|
221
|
+
stepBubble: {
|
|
222
|
+
width: 32,
|
|
223
|
+
height: 32,
|
|
224
|
+
borderRadius: 16,
|
|
225
|
+
justifyContent: "center",
|
|
226
|
+
alignItems: "center",
|
|
227
|
+
borderWidth: 2,
|
|
228
|
+
},
|
|
229
|
+
stepNumber: {
|
|
230
|
+
fontSize: 12,
|
|
231
|
+
fontWeight: "700",
|
|
232
|
+
},
|
|
233
|
+
stepLabel: {
|
|
234
|
+
fontSize: 11,
|
|
235
|
+
},
|
|
236
|
+
stepConnector: {
|
|
237
|
+
width: 40,
|
|
238
|
+
height: 2,
|
|
239
|
+
borderRadius: 1,
|
|
240
|
+
marginBottom: 18,
|
|
241
|
+
marginHorizontal: 4,
|
|
127
242
|
},
|
|
128
243
|
message: {
|
|
129
244
|
textAlign: "center",
|
|
130
245
|
},
|
|
131
246
|
progressContainer: {
|
|
132
247
|
width: "100%",
|
|
133
|
-
marginTop:
|
|
248
|
+
marginTop: 8,
|
|
134
249
|
},
|
|
135
250
|
hint: {
|
|
136
251
|
textAlign: "center",
|
|
137
|
-
marginTop:
|
|
252
|
+
marginTop: 4,
|
|
138
253
|
},
|
|
139
254
|
backgroundHintButton: {
|
|
140
255
|
marginTop: 32,
|
|
@@ -6,7 +6,6 @@ let useVideoPlayer: (...args: any[]) => any = () => null;
|
|
|
6
6
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
7
|
let VideoView: React.ComponentType<any> = () => null;
|
|
8
8
|
try {
|
|
9
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
10
9
|
const expoVideo = require("expo-video");
|
|
11
10
|
useVideoPlayer = expoVideo.useVideoPlayer;
|
|
12
11
|
VideoView = expoVideo.VideoView;
|