@umituz/react-native-ai-generation-content 1.17.16 → 1.17.18
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 -4
- package/src/domains/creations/presentation/components/CreationDetail/DetailImage.tsx +14 -14
- package/src/domains/creations/presentation/components/CreationDetail/DetailVideo.tsx +51 -58
- package/src/domains/creations/presentation/screens/CreationDetailScreen.tsx +36 -29
- package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +98 -69
- package/src/features/text-to-image/index.ts +92 -4
- package/src/features/text-to-image/presentation/components/AspectRatioSelector.tsx +98 -0
- package/src/features/text-to-image/presentation/components/ExamplePrompts.tsx +88 -0
- package/src/features/text-to-image/presentation/components/ImageSizeSelector.tsx +98 -0
- package/src/features/text-to-image/presentation/components/NumImagesSelector.tsx +93 -0
- package/src/features/text-to-image/presentation/components/OutputFormatSelector.tsx +98 -0
- package/src/features/text-to-image/presentation/components/SettingsSheet.tsx +139 -0
- package/src/features/text-to-image/presentation/components/StyleSelector.tsx +110 -0
- package/src/features/text-to-image/presentation/components/TextToImageGenerateButton.tsx +84 -0
- package/src/features/text-to-image/presentation/components/TextToImagePromptInput.tsx +90 -0
- package/src/features/text-to-image/presentation/components/index.ts +44 -0
- package/src/features/text-to-image/presentation/hooks/index.ts +25 -0
- package/src/features/text-to-image/presentation/hooks/useFormState.ts +103 -0
- package/src/features/text-to-image/presentation/hooks/useGeneration.ts +98 -0
- package/src/features/text-to-image/presentation/hooks/useTextToImageForm.ts +58 -0
- package/src/features/text-to-image/presentation/index.ts +6 -0
- package/src/features/text-to-voice/domain/types/component.types.ts +91 -0
- package/src/features/text-to-voice/domain/types/config.types.ts +34 -0
- package/src/features/text-to-voice/domain/types/form.types.ts +39 -0
- package/src/features/text-to-voice/domain/types/generation.types.ts +43 -0
- package/src/features/text-to-voice/domain/types/index.ts +31 -3
- package/src/features/text-to-voice/infrastructure/services/text-to-voice-executor.ts +2 -10
- package/src/features/text-to-voice/domain/types/text-to-voice.types.ts +0 -65
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.18",
|
|
4
4
|
"description": "Provider-agnostic AI generation orchestration for React Native",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -101,8 +101,5 @@
|
|
|
101
101
|
},
|
|
102
102
|
"publishConfig": {
|
|
103
103
|
"access": "public"
|
|
104
|
-
},
|
|
105
|
-
"dependencies": {
|
|
106
|
-
"@umituz/react-native-ai-generation-content": "^1.17.14"
|
|
107
104
|
}
|
|
108
105
|
}
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
|
|
2
1
|
import React from 'react';
|
|
3
|
-
import { View, StyleSheet,
|
|
4
|
-
import { useAppDesignTokens
|
|
2
|
+
import { View, StyleSheet, useWindowDimensions } from 'react-native';
|
|
3
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
4
|
+
import { Image } from 'expo-image';
|
|
5
5
|
|
|
6
6
|
interface DetailImageProps {
|
|
7
7
|
readonly uri: string;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const HORIZONTAL_PADDING = 16;
|
|
11
|
+
const ASPECT_RATIO = 16 / 9;
|
|
11
12
|
|
|
12
13
|
export const DetailImage: React.FC<DetailImageProps> = ({ uri }) => {
|
|
13
14
|
const tokens = useAppDesignTokens();
|
|
14
|
-
const
|
|
15
|
+
const { width } = useWindowDimensions();
|
|
16
|
+
const imageWidth = width - (HORIZONTAL_PADDING * 2);
|
|
17
|
+
const imageHeight = imageWidth / ASPECT_RATIO;
|
|
15
18
|
|
|
16
19
|
return (
|
|
17
20
|
<View style={styles.container}>
|
|
18
|
-
<View style={styles.frame}>
|
|
19
|
-
<Image source={{ uri }} style={styles.image}
|
|
21
|
+
<View style={[styles.frame, { width: imageWidth, height: imageHeight, backgroundColor: tokens.colors.surface }]}>
|
|
22
|
+
<Image source={{ uri }} style={styles.image} contentFit="cover" />
|
|
20
23
|
</View>
|
|
21
24
|
</View>
|
|
22
25
|
);
|
|
23
26
|
};
|
|
24
27
|
|
|
25
|
-
const
|
|
28
|
+
const styles = StyleSheet.create({
|
|
26
29
|
container: {
|
|
27
|
-
paddingHorizontal:
|
|
28
|
-
marginVertical:
|
|
30
|
+
paddingHorizontal: HORIZONTAL_PADDING,
|
|
31
|
+
marginVertical: 16,
|
|
29
32
|
},
|
|
30
33
|
frame: {
|
|
31
|
-
|
|
32
|
-
height: width - (tokens.spacing.lg * 2),
|
|
33
|
-
borderRadius: 24,
|
|
34
|
+
borderRadius: 16,
|
|
34
35
|
overflow: 'hidden',
|
|
35
|
-
backgroundColor: tokens.colors.surface,
|
|
36
36
|
},
|
|
37
37
|
image: {
|
|
38
38
|
width: '100%',
|
|
@@ -4,12 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import React, { useState, useCallback } from "react";
|
|
7
|
-
import { View, StyleSheet,
|
|
8
|
-
import {
|
|
9
|
-
useAppDesignTokens,
|
|
10
|
-
AtomicIcon,
|
|
11
|
-
type DesignTokens,
|
|
12
|
-
} from "@umituz/react-native-design-system";
|
|
7
|
+
import { View, StyleSheet, TouchableOpacity, useWindowDimensions } from "react-native";
|
|
8
|
+
import { useAppDesignTokens, AtomicIcon } from "@umituz/react-native-design-system";
|
|
13
9
|
import { Image } from "expo-image";
|
|
14
10
|
import { useVideoPlayer, VideoView } from "expo-video";
|
|
15
11
|
|
|
@@ -18,14 +14,17 @@ interface DetailVideoProps {
|
|
|
18
14
|
readonly thumbnailUrl?: string;
|
|
19
15
|
}
|
|
20
16
|
|
|
21
|
-
const
|
|
17
|
+
const HORIZONTAL_PADDING = 16;
|
|
18
|
+
const ASPECT_RATIO = 16 / 9;
|
|
22
19
|
|
|
23
20
|
export const DetailVideo: React.FC<DetailVideoProps> = ({
|
|
24
21
|
videoUrl,
|
|
25
22
|
thumbnailUrl,
|
|
26
23
|
}) => {
|
|
27
24
|
const tokens = useAppDesignTokens();
|
|
28
|
-
const
|
|
25
|
+
const { width } = useWindowDimensions();
|
|
26
|
+
const videoWidth = width - (HORIZONTAL_PADDING * 2);
|
|
27
|
+
const videoHeight = videoWidth / ASPECT_RATIO;
|
|
29
28
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
30
29
|
|
|
31
30
|
const player = useVideoPlayer(videoUrl, (p) => {
|
|
@@ -39,7 +38,7 @@ export const DetailVideo: React.FC<DetailVideoProps> = ({
|
|
|
39
38
|
|
|
40
39
|
return (
|
|
41
40
|
<View style={styles.container}>
|
|
42
|
-
<View style={styles.frame}>
|
|
41
|
+
<View style={[styles.frame, { width: videoWidth, height: videoHeight, backgroundColor: tokens.colors.surface }]}>
|
|
43
42
|
{isPlaying ? (
|
|
44
43
|
<VideoView
|
|
45
44
|
player={player}
|
|
@@ -60,10 +59,10 @@ export const DetailVideo: React.FC<DetailVideoProps> = ({
|
|
|
60
59
|
contentFit="cover"
|
|
61
60
|
/>
|
|
62
61
|
) : (
|
|
63
|
-
<View style={styles.placeholder} />
|
|
62
|
+
<View style={[styles.placeholder, { backgroundColor: tokens.colors.surfaceSecondary }]} />
|
|
64
63
|
)}
|
|
65
64
|
<View style={styles.playButtonContainer}>
|
|
66
|
-
<View style={styles.playButton}>
|
|
65
|
+
<View style={[styles.playButton, { backgroundColor: tokens.colors.primary }]}>
|
|
67
66
|
<AtomicIcon name="play" customSize={32} color="onPrimary" />
|
|
68
67
|
</View>
|
|
69
68
|
</View>
|
|
@@ -74,50 +73,44 @@ export const DetailVideo: React.FC<DetailVideoProps> = ({
|
|
|
74
73
|
);
|
|
75
74
|
};
|
|
76
75
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
backgroundColor: tokens.colors.primary,
|
|
119
|
-
justifyContent: "center",
|
|
120
|
-
alignItems: "center",
|
|
121
|
-
paddingLeft: 4,
|
|
122
|
-
},
|
|
123
|
-
});
|
|
76
|
+
const styles = StyleSheet.create({
|
|
77
|
+
container: {
|
|
78
|
+
paddingHorizontal: HORIZONTAL_PADDING,
|
|
79
|
+
marginVertical: 16,
|
|
80
|
+
},
|
|
81
|
+
frame: {
|
|
82
|
+
borderRadius: 16,
|
|
83
|
+
overflow: "hidden",
|
|
84
|
+
},
|
|
85
|
+
video: {
|
|
86
|
+
width: "100%",
|
|
87
|
+
height: "100%",
|
|
88
|
+
},
|
|
89
|
+
thumbnailContainer: {
|
|
90
|
+
width: "100%",
|
|
91
|
+
height: "100%",
|
|
92
|
+
justifyContent: "center",
|
|
93
|
+
alignItems: "center",
|
|
94
|
+
},
|
|
95
|
+
thumbnail: {
|
|
96
|
+
width: "100%",
|
|
97
|
+
height: "100%",
|
|
98
|
+
},
|
|
99
|
+
placeholder: {
|
|
100
|
+
width: "100%",
|
|
101
|
+
height: "100%",
|
|
102
|
+
},
|
|
103
|
+
playButtonContainer: {
|
|
104
|
+
...StyleSheet.absoluteFillObject,
|
|
105
|
+
justifyContent: "center",
|
|
106
|
+
alignItems: "center",
|
|
107
|
+
},
|
|
108
|
+
playButton: {
|
|
109
|
+
width: 64,
|
|
110
|
+
height: 64,
|
|
111
|
+
borderRadius: 32,
|
|
112
|
+
justifyContent: "center",
|
|
113
|
+
alignItems: "center",
|
|
114
|
+
paddingLeft: 4,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
|
-
import { StyleSheet } from 'react-native';
|
|
3
|
-
import {
|
|
2
|
+
import { View, ScrollView, StyleSheet } from 'react-native';
|
|
3
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
4
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
4
5
|
import type { Creation } from '../../domain/entities/Creation';
|
|
5
6
|
import { hasVideoContent, getPreviewUrl } from '../../domain/utils';
|
|
6
7
|
import { DetailHeader } from '../components/CreationDetail/DetailHeader';
|
|
@@ -37,6 +38,7 @@ export const CreationDetailScreen: React.FC<CreationDetailScreenProps> = ({
|
|
|
37
38
|
t
|
|
38
39
|
}) => {
|
|
39
40
|
const tokens = useAppDesignTokens();
|
|
41
|
+
const insets = useSafeAreaInsets();
|
|
40
42
|
const { getLocalizedTitle } = useCreationsProvider();
|
|
41
43
|
|
|
42
44
|
// Extract data safely
|
|
@@ -57,48 +59,53 @@ export const CreationDetailScreen: React.FC<CreationDetailScreenProps> = ({
|
|
|
57
59
|
return false;
|
|
58
60
|
}, [creation.type, creation.output]);
|
|
59
61
|
|
|
60
|
-
const styles = useStyles(tokens);
|
|
61
|
-
|
|
62
62
|
// Get video URL and thumbnail for video content
|
|
63
63
|
const videoUrl = creation.output?.videoUrl || creation.uri;
|
|
64
64
|
const thumbnailUrl = getPreviewUrl(creation.output) || undefined;
|
|
65
65
|
|
|
66
66
|
return (
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
edges={['top', 'bottom']}
|
|
70
|
-
backgroundColor={tokens.colors.background}
|
|
71
|
-
header={
|
|
67
|
+
<View style={[styles.container, { backgroundColor: tokens.colors.background }]}>
|
|
68
|
+
<View style={{ paddingTop: insets.top }}>
|
|
72
69
|
<DetailHeader
|
|
73
70
|
title={title}
|
|
74
71
|
date={date}
|
|
75
72
|
onClose={onClose}
|
|
76
73
|
/>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
74
|
+
</View>
|
|
75
|
+
<ScrollView
|
|
76
|
+
style={styles.scrollView}
|
|
77
|
+
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 32 }]}
|
|
78
|
+
showsVerticalScrollIndicator={false}
|
|
79
|
+
>
|
|
80
|
+
{isVideo ? (
|
|
81
|
+
<DetailVideo videoUrl={videoUrl} thumbnailUrl={thumbnailUrl} />
|
|
82
|
+
) : (
|
|
83
|
+
<DetailImage uri={creation.uri} />
|
|
84
|
+
)}
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
{story ? (
|
|
87
|
+
<DetailStory story={story} />
|
|
88
|
+
) : null}
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
90
|
+
<DetailActions
|
|
91
|
+
onShare={() => onShare(creation)}
|
|
92
|
+
onDelete={() => onDelete(creation)}
|
|
93
|
+
shareLabel={t("result.shareButton")}
|
|
94
|
+
deleteLabel={t("common.delete")}
|
|
95
|
+
/>
|
|
96
|
+
</ScrollView>
|
|
97
|
+
</View>
|
|
97
98
|
);
|
|
98
99
|
};
|
|
99
100
|
|
|
100
|
-
const
|
|
101
|
+
const styles = StyleSheet.create({
|
|
102
|
+
container: {
|
|
103
|
+
flex: 1,
|
|
104
|
+
},
|
|
105
|
+
scrollView: {
|
|
106
|
+
flex: 1,
|
|
107
|
+
},
|
|
101
108
|
scrollContent: {
|
|
102
|
-
|
|
109
|
+
paddingTop: 8,
|
|
103
110
|
},
|
|
104
111
|
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
declare const __DEV__: boolean;
|
|
2
2
|
|
|
3
3
|
import React, { useMemo, useCallback, useState } from "react";
|
|
4
|
+
import { View, FlatList, RefreshControl, StyleSheet } from "react-native";
|
|
5
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
4
6
|
import {
|
|
5
7
|
useAppDesignTokens,
|
|
6
8
|
useAlert,
|
|
@@ -9,7 +11,7 @@ import {
|
|
|
9
11
|
useSharing,
|
|
10
12
|
FilterBottomSheet,
|
|
11
13
|
type BottomSheetModalRef,
|
|
12
|
-
|
|
14
|
+
type DesignTokens,
|
|
13
15
|
} from "@umituz/react-native-design-system";
|
|
14
16
|
import { useFocusEffect } from "@react-navigation/native";
|
|
15
17
|
import { useCreations } from "../hooks/useCreations";
|
|
@@ -17,7 +19,7 @@ import { useDeleteCreation } from "../hooks/useDeleteCreation";
|
|
|
17
19
|
import { useCreationsFilter } from "../hooks/useCreationsFilter";
|
|
18
20
|
import {
|
|
19
21
|
GalleryHeader,
|
|
20
|
-
|
|
22
|
+
CreationCard,
|
|
21
23
|
CreationImageViewer,
|
|
22
24
|
GalleryEmptyStates,
|
|
23
25
|
} from "../components";
|
|
@@ -26,6 +28,7 @@ import type { Creation } from "../../domain/entities/Creation";
|
|
|
26
28
|
import type { CreationsConfig } from "../../domain/value-objects/CreationsConfig";
|
|
27
29
|
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
28
30
|
import { CreationDetailScreen } from "./CreationDetailScreen";
|
|
31
|
+
import { CreationsProvider } from "../components/CreationsProvider";
|
|
29
32
|
|
|
30
33
|
interface CreationsGalleryScreenProps {
|
|
31
34
|
readonly userId: string | null;
|
|
@@ -40,8 +43,6 @@ interface CreationsGalleryScreenProps {
|
|
|
40
43
|
readonly showFilter?: boolean;
|
|
41
44
|
}
|
|
42
45
|
|
|
43
|
-
import { CreationsProvider } from "../components/CreationsProvider";
|
|
44
|
-
|
|
45
46
|
export function CreationsGalleryScreen(props: CreationsGalleryScreenProps) {
|
|
46
47
|
return (
|
|
47
48
|
<CreationsProvider config={props.config} t={props.t}>
|
|
@@ -62,6 +63,7 @@ function CreationsGalleryScreenContent({
|
|
|
62
63
|
emptyActionLabel,
|
|
63
64
|
showFilter = config.showFilter ?? true,
|
|
64
65
|
}: CreationsGalleryScreenProps) {
|
|
66
|
+
const insets = useSafeAreaInsets();
|
|
65
67
|
const tokens = useAppDesignTokens();
|
|
66
68
|
const { share } = useSharing();
|
|
67
69
|
const alert = useAlert();
|
|
@@ -76,14 +78,12 @@ function CreationsGalleryScreenContent({
|
|
|
76
78
|
const deleteMutation = useDeleteCreation({ userId, repository });
|
|
77
79
|
const { filtered, selectedIds, toggleFilter, clearFilters, isFiltered } = useCreationsFilter({ creations });
|
|
78
80
|
|
|
79
|
-
// Refetch creations when screen comes into focus
|
|
80
81
|
useFocusEffect(
|
|
81
82
|
useCallback(() => {
|
|
82
83
|
void refetch();
|
|
83
84
|
}, [refetch])
|
|
84
85
|
);
|
|
85
86
|
|
|
86
|
-
// Prepare data for UI using utils
|
|
87
87
|
const allCategories = useMemo(
|
|
88
88
|
() => getFilterCategoriesFromConfig(config, t),
|
|
89
89
|
[config, t],
|
|
@@ -118,27 +118,62 @@ function CreationsGalleryScreenContent({
|
|
|
118
118
|
);
|
|
119
119
|
}, [alert, config, deleteMutation, t]);
|
|
120
120
|
|
|
121
|
-
// Handle viewing a creation - shows detail screen
|
|
122
121
|
const handleView = useCallback((creation: Creation) => {
|
|
123
122
|
setSelectedCreation(creation);
|
|
124
123
|
}, []);
|
|
125
124
|
|
|
126
|
-
// Handle favorite toggle
|
|
127
125
|
const handleFavorite = useCallback((creation: Creation, isFavorite: boolean) => {
|
|
128
126
|
void (async () => {
|
|
129
127
|
if (!userId) return;
|
|
130
|
-
const success = await repository.updateFavorite(
|
|
131
|
-
userId,
|
|
132
|
-
creation.id,
|
|
133
|
-
isFavorite,
|
|
134
|
-
);
|
|
128
|
+
const success = await repository.updateFavorite(userId, creation.id, isFavorite);
|
|
135
129
|
if (success) {
|
|
136
130
|
void refetch();
|
|
137
131
|
}
|
|
138
132
|
})();
|
|
139
133
|
}, [userId, repository, refetch]);
|
|
140
134
|
|
|
141
|
-
const
|
|
135
|
+
const styles = useStyles(tokens, insets);
|
|
136
|
+
|
|
137
|
+
const renderItem = useCallback(
|
|
138
|
+
({ item }: { item: Creation }) => (
|
|
139
|
+
<CreationCard
|
|
140
|
+
creation={item}
|
|
141
|
+
callbacks={{
|
|
142
|
+
onPress: () => handleView(item),
|
|
143
|
+
onShare: async () => handleShare(item),
|
|
144
|
+
onDelete: () => handleDelete(item),
|
|
145
|
+
onFavorite: () => handleFavorite(item, !item.isFavorite),
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
148
|
+
),
|
|
149
|
+
[handleView, handleShare, handleDelete, handleFavorite]
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const renderHeader = useMemo(() => {
|
|
153
|
+
if ((!creations || creations.length === 0) && !isLoading) return null;
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<View style={styles.header}>
|
|
157
|
+
<GalleryHeader
|
|
158
|
+
title={t(config.translations.title)}
|
|
159
|
+
count={filtered.length}
|
|
160
|
+
countLabel={t(config.translations.photoCount)}
|
|
161
|
+
isFiltered={isFiltered}
|
|
162
|
+
showFilter={showFilter}
|
|
163
|
+
filterLabel={t(config.translations.filterLabel)}
|
|
164
|
+
onFilterPress={() => {
|
|
165
|
+
if (__DEV__) {
|
|
166
|
+
// eslint-disable-next-line no-console
|
|
167
|
+
console.log('[CreationsGallery] Filter button pressed');
|
|
168
|
+
}
|
|
169
|
+
filterSheetRef.current?.present();
|
|
170
|
+
}}
|
|
171
|
+
/>
|
|
172
|
+
</View>
|
|
173
|
+
);
|
|
174
|
+
}, [creations, isLoading, filtered.length, isFiltered, showFilter, t, config, styles.header]);
|
|
175
|
+
|
|
176
|
+
const renderEmpty = useMemo(() => (
|
|
142
177
|
<GalleryEmptyStates
|
|
143
178
|
isLoading={isLoading}
|
|
144
179
|
creations={creations}
|
|
@@ -166,63 +201,37 @@ function CreationsGalleryScreenContent({
|
|
|
166
201
|
}
|
|
167
202
|
|
|
168
203
|
return (
|
|
169
|
-
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// eslint-disable-next-line no-console
|
|
188
|
-
console.log('[CreationsGallery] filterSheetRef.current:', filterSheetRef.current);
|
|
189
|
-
// eslint-disable-next-line no-console
|
|
190
|
-
console.log('[CreationsGallery] allCategories:', allCategories);
|
|
191
|
-
}
|
|
192
|
-
filterSheetRef.current?.present();
|
|
193
|
-
}}
|
|
194
|
-
/>
|
|
195
|
-
)
|
|
204
|
+
<View style={[styles.container, { backgroundColor: tokens.colors.background }]}>
|
|
205
|
+
<FlatList
|
|
206
|
+
data={filtered}
|
|
207
|
+
renderItem={renderItem}
|
|
208
|
+
keyExtractor={(item) => item.id}
|
|
209
|
+
ListHeaderComponent={renderHeader}
|
|
210
|
+
ListEmptyComponent={renderEmpty}
|
|
211
|
+
contentContainerStyle={[
|
|
212
|
+
styles.listContent,
|
|
213
|
+
(!filtered || filtered.length === 0) && styles.emptyContent,
|
|
214
|
+
]}
|
|
215
|
+
showsVerticalScrollIndicator={false}
|
|
216
|
+
refreshControl={
|
|
217
|
+
<RefreshControl
|
|
218
|
+
refreshing={isLoading}
|
|
219
|
+
onRefresh={() => void refetch()}
|
|
220
|
+
tintColor={tokens.colors.primary}
|
|
221
|
+
/>
|
|
196
222
|
}
|
|
197
|
-
|
|
198
|
-
{/* Main Content Grid - handles empty/loading via ListEmptyComponent */}
|
|
199
|
-
<CreationsGrid
|
|
200
|
-
creations={filtered}
|
|
201
|
-
isLoading={isLoading}
|
|
202
|
-
onRefresh={() => void refetch()}
|
|
203
|
-
onPress={(creation) => handleView(creation as Creation)}
|
|
204
|
-
onShare={async (creation) => handleShare(creation as Creation)}
|
|
205
|
-
onDelete={(creation) => handleDelete(creation as Creation)}
|
|
206
|
-
onFavorite={(creation) => {
|
|
207
|
-
const c = creation as Creation;
|
|
208
|
-
handleFavorite(c, !c.isFavorite);
|
|
209
|
-
}}
|
|
210
|
-
contentContainerStyle={{ paddingBottom: tokens.spacing.xl }}
|
|
211
|
-
ListEmptyComponent={renderEmptyComponent}
|
|
212
|
-
/>
|
|
223
|
+
/>
|
|
213
224
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
</ScreenLayout>
|
|
225
|
+
<CreationImageViewer
|
|
226
|
+
creations={filtered}
|
|
227
|
+
visible={viewerVisible}
|
|
228
|
+
index={viewerIndex}
|
|
229
|
+
onDismiss={() => setViewerVisible(false)}
|
|
230
|
+
onIndexChange={setViewerIndex}
|
|
231
|
+
enableEditing={enableEditing}
|
|
232
|
+
onImageEdit={onImageEdit}
|
|
233
|
+
/>
|
|
224
234
|
|
|
225
|
-
{/* FilterBottomSheet must be outside ScreenLayout for proper portal rendering */}
|
|
226
235
|
<FilterBottomSheet
|
|
227
236
|
ref={filterSheetRef}
|
|
228
237
|
categories={allCategories}
|
|
@@ -234,7 +243,27 @@ function CreationsGalleryScreenContent({
|
|
|
234
243
|
onClearFilters={clearFilters}
|
|
235
244
|
title={t(config.translations.filterTitle)}
|
|
236
245
|
/>
|
|
237
|
-
|
|
246
|
+
</View>
|
|
238
247
|
);
|
|
239
248
|
}
|
|
240
249
|
|
|
250
|
+
const useStyles = (tokens: DesignTokens, insets: { top: number; bottom: number }) =>
|
|
251
|
+
StyleSheet.create({
|
|
252
|
+
container: {
|
|
253
|
+
flex: 1,
|
|
254
|
+
},
|
|
255
|
+
header: {
|
|
256
|
+
paddingTop: insets.top + tokens.spacing.md,
|
|
257
|
+
backgroundColor: tokens.colors.surface,
|
|
258
|
+
borderBottomWidth: 1,
|
|
259
|
+
borderBottomColor: tokens.colors.border,
|
|
260
|
+
},
|
|
261
|
+
listContent: {
|
|
262
|
+
paddingHorizontal: tokens.spacing.md,
|
|
263
|
+
paddingTop: tokens.spacing.md,
|
|
264
|
+
paddingBottom: insets.bottom + 100,
|
|
265
|
+
},
|
|
266
|
+
emptyContent: {
|
|
267
|
+
flexGrow: 1,
|
|
268
|
+
},
|
|
269
|
+
});
|