@umituz/react-native-ai-generation-content 1.12.11 → 1.12.12
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 +2 -2
- package/src/domains/creations/domain/entities/Creation.ts +3 -0
- package/src/domains/creations/domain/repositories/ICreationsRepository.ts +2 -0
- package/src/domains/creations/infrastructure/repositories/CreationsRepository.ts +30 -0
- package/src/domains/creations/presentation/components/CreationCard.tsx +81 -16
- package/src/domains/creations/presentation/components/CreationsGrid.tsx +12 -0
- package/src/domains/creations/presentation/components/GalleryHeader.tsx +61 -18
- package/src/domains/creations/presentation/hooks/index.ts +2 -0
- package/src/domains/creations/presentation/hooks/useCreationsFilter.ts +36 -16
- package/src/domains/creations/presentation/hooks/useDeleteMultipleCreations.ts +57 -0
- package/src/domains/creations/presentation/hooks/useToggleFavorite.ts +59 -0
- package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +125 -34
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.12.
|
|
3
|
+
"version": "1.12.12",
|
|
4
4
|
"description": "Provider-agnostic AI generation orchestration for React Native",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -78,4 +78,4 @@
|
|
|
78
78
|
"publishConfig": {
|
|
79
79
|
"access": "public"
|
|
80
80
|
}
|
|
81
|
-
}
|
|
81
|
+
}
|
|
@@ -12,6 +12,7 @@ export interface Creation {
|
|
|
12
12
|
readonly originalUri?: string;
|
|
13
13
|
readonly createdAt: Date;
|
|
14
14
|
readonly isShared: boolean;
|
|
15
|
+
readonly isFavorite: boolean;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export interface CreationDocument {
|
|
@@ -26,6 +27,7 @@ export interface CreationDocument {
|
|
|
26
27
|
readonly type?: string;
|
|
27
28
|
readonly status?: string;
|
|
28
29
|
readonly isShared: boolean;
|
|
30
|
+
readonly isFavorite: boolean;
|
|
29
31
|
readonly createdAt: FirebaseTimestamp | Date; // Allow Date for writing
|
|
30
32
|
readonly completedAt?: FirebaseTimestamp | Date;
|
|
31
33
|
}
|
|
@@ -47,5 +49,6 @@ export function mapDocumentToCreation(
|
|
|
47
49
|
originalUri: data.originalImageUrl || data.originalImage,
|
|
48
50
|
createdAt: (data.createdAt as any)?.toDate?.() || (data.createdAt instanceof Date ? data.createdAt : new Date()),
|
|
49
51
|
isShared: data.isShared ?? false,
|
|
52
|
+
isFavorite: data.isFavorite ?? false,
|
|
50
53
|
};
|
|
51
54
|
}
|
|
@@ -15,6 +15,8 @@ export interface ICreationsRepository {
|
|
|
15
15
|
updates: Partial<Creation>,
|
|
16
16
|
): Promise<boolean>;
|
|
17
17
|
delete(userId: string, creationId: string): Promise<boolean>;
|
|
18
|
+
deleteMultiple(userId: string, creationIds: string[]): Promise<boolean>;
|
|
19
|
+
toggleFavorite(userId: string, creationId: string): Promise<boolean>;
|
|
18
20
|
updateShared(
|
|
19
21
|
userId: string,
|
|
20
22
|
creationId: string,
|
|
@@ -157,6 +157,7 @@ export class CreationsRepository
|
|
|
157
157
|
createdAt: creation.createdAt,
|
|
158
158
|
metadata: creation.metadata || {},
|
|
159
159
|
isShared: creation.isShared || false,
|
|
160
|
+
isFavorite: creation.isFavorite || false,
|
|
160
161
|
};
|
|
161
162
|
|
|
162
163
|
await setDoc(docRef, data);
|
|
@@ -183,6 +184,9 @@ export class CreationsRepository
|
|
|
183
184
|
if (updates.isShared !== undefined) {
|
|
184
185
|
updateData.isShared = updates.isShared;
|
|
185
186
|
}
|
|
187
|
+
if (updates.isFavorite !== undefined) {
|
|
188
|
+
updateData.isFavorite = updates.isFavorite;
|
|
189
|
+
}
|
|
186
190
|
if (updates.uri !== undefined) {
|
|
187
191
|
updateData.uri = updates.uri;
|
|
188
192
|
}
|
|
@@ -215,6 +219,32 @@ export class CreationsRepository
|
|
|
215
219
|
}
|
|
216
220
|
}
|
|
217
221
|
|
|
222
|
+
async deleteMultiple(userId: string, creationIds: string[]): Promise<boolean> {
|
|
223
|
+
try {
|
|
224
|
+
const promises = creationIds.map((id) => this.delete(userId, id));
|
|
225
|
+
const results = await Promise.all(promises);
|
|
226
|
+
return results.every((r) => r);
|
|
227
|
+
} catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async toggleFavorite(userId: string, creationId: string): Promise<boolean> {
|
|
233
|
+
const docRef = this.getDocRef(userId, creationId);
|
|
234
|
+
if (!docRef) return false;
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const docSnap = await getDoc(docRef);
|
|
238
|
+
if (!docSnap.exists()) return false;
|
|
239
|
+
|
|
240
|
+
const current = docSnap.data() as CreationDocument;
|
|
241
|
+
await updateDoc(docRef, { isFavorite: !current.isFavorite });
|
|
242
|
+
return true;
|
|
243
|
+
} catch {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
218
248
|
async updateShared(
|
|
219
249
|
userId: string,
|
|
220
250
|
creationId: string,
|
|
@@ -19,6 +19,10 @@ interface CreationCardProps {
|
|
|
19
19
|
readonly onView?: (creation: Creation) => void;
|
|
20
20
|
readonly onShare: (creation: Creation) => void;
|
|
21
21
|
readonly onDelete: (creation: Creation) => void;
|
|
22
|
+
readonly onToggleFavorite?: (creation: Creation) => void;
|
|
23
|
+
readonly isSelected?: boolean;
|
|
24
|
+
readonly onSelect?: (creation: Creation) => void;
|
|
25
|
+
readonly isSelectionMode?: boolean;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export function CreationCard({
|
|
@@ -27,6 +31,10 @@ export function CreationCard({
|
|
|
27
31
|
onView,
|
|
28
32
|
onShare,
|
|
29
33
|
onDelete,
|
|
34
|
+
onToggleFavorite,
|
|
35
|
+
isSelected,
|
|
36
|
+
onSelect,
|
|
37
|
+
isSelectionMode,
|
|
30
38
|
}: CreationCardProps) {
|
|
31
39
|
const tokens = useAppDesignTokens();
|
|
32
40
|
|
|
@@ -40,6 +48,14 @@ export function CreationCard({
|
|
|
40
48
|
() => onDelete(creation),
|
|
41
49
|
[creation, onDelete],
|
|
42
50
|
);
|
|
51
|
+
const handleToggleFavorite = useCallback(
|
|
52
|
+
() => onToggleFavorite?.(creation),
|
|
53
|
+
[creation, onToggleFavorite],
|
|
54
|
+
);
|
|
55
|
+
const handleSelect = useCallback(
|
|
56
|
+
() => onSelect?.(creation),
|
|
57
|
+
[creation, onSelect],
|
|
58
|
+
);
|
|
43
59
|
|
|
44
60
|
const formattedDate = useMemo(() => {
|
|
45
61
|
const date =
|
|
@@ -63,11 +79,42 @@ export function CreationCard({
|
|
|
63
79
|
borderRadius: tokens.spacing.md,
|
|
64
80
|
overflow: "hidden",
|
|
65
81
|
marginBottom: tokens.spacing.md,
|
|
82
|
+
borderWidth: 1,
|
|
83
|
+
borderColor: isSelected ? tokens.colors.primary : "transparent",
|
|
84
|
+
},
|
|
85
|
+
imageContainer: {
|
|
86
|
+
width: 100,
|
|
87
|
+
height: 100,
|
|
88
|
+
position: "relative",
|
|
66
89
|
},
|
|
67
90
|
thumbnail: {
|
|
68
91
|
width: 100,
|
|
69
92
|
height: 100,
|
|
70
93
|
},
|
|
94
|
+
favoriteBtn: {
|
|
95
|
+
position: "absolute",
|
|
96
|
+
top: 4,
|
|
97
|
+
right: 4,
|
|
98
|
+
width: 28,
|
|
99
|
+
height: 28,
|
|
100
|
+
borderRadius: 14,
|
|
101
|
+
backgroundColor: "rgba(255,255,255,0.7)",
|
|
102
|
+
justifyContent: "center",
|
|
103
|
+
alignItems: "center",
|
|
104
|
+
},
|
|
105
|
+
selectionOverlay: {
|
|
106
|
+
position: "absolute",
|
|
107
|
+
top: 4,
|
|
108
|
+
left: 4,
|
|
109
|
+
width: 24,
|
|
110
|
+
height: 24,
|
|
111
|
+
borderRadius: 12,
|
|
112
|
+
backgroundColor: isSelected ? tokens.colors.primary : "rgba(255,255,255,0.5)",
|
|
113
|
+
justifyContent: "center",
|
|
114
|
+
alignItems: "center",
|
|
115
|
+
borderWidth: 1,
|
|
116
|
+
borderColor: tokens.colors.primary,
|
|
117
|
+
},
|
|
71
118
|
content: {
|
|
72
119
|
flex: 1,
|
|
73
120
|
padding: tokens.spacing.md,
|
|
@@ -103,12 +150,33 @@ export function CreationCard({
|
|
|
103
150
|
alignItems: "center",
|
|
104
151
|
},
|
|
105
152
|
}),
|
|
106
|
-
[tokens],
|
|
153
|
+
[tokens, isSelected],
|
|
107
154
|
);
|
|
108
155
|
|
|
109
156
|
return (
|
|
110
|
-
<
|
|
111
|
-
|
|
157
|
+
<TouchableOpacity
|
|
158
|
+
activeOpacity={0.9}
|
|
159
|
+
style={styles.container}
|
|
160
|
+
onPress={isSelectionMode ? handleSelect : handleView}
|
|
161
|
+
onLongPress={handleSelect}
|
|
162
|
+
>
|
|
163
|
+
<View style={styles.imageContainer}>
|
|
164
|
+
<Image source={{ uri: creation.uri }} style={styles.thumbnail} />
|
|
165
|
+
{isSelectionMode && (
|
|
166
|
+
<View style={styles.selectionOverlay}>
|
|
167
|
+
{isSelected && <AtomicIcon name="checkmark-circle" size="xs" color="white" />}
|
|
168
|
+
</View>
|
|
169
|
+
)}
|
|
170
|
+
{!isSelectionMode && onToggleFavorite && (
|
|
171
|
+
<TouchableOpacity style={styles.favoriteBtn} onPress={handleToggleFavorite}>
|
|
172
|
+
<AtomicIcon
|
|
173
|
+
name={creation.isFavorite ? "heart" : "heart-outline"}
|
|
174
|
+
size="xs"
|
|
175
|
+
color={creation.isFavorite ? "error" : "secondary"}
|
|
176
|
+
/>
|
|
177
|
+
</TouchableOpacity>
|
|
178
|
+
)}
|
|
179
|
+
</View>
|
|
112
180
|
<View style={styles.content}>
|
|
113
181
|
<View>
|
|
114
182
|
<View style={styles.typeRow}>
|
|
@@ -117,20 +185,17 @@ export function CreationCard({
|
|
|
117
185
|
</View>
|
|
118
186
|
<AtomicText style={styles.dateText}>{formattedDate}</AtomicText>
|
|
119
187
|
</View>
|
|
120
|
-
|
|
121
|
-
{
|
|
122
|
-
<TouchableOpacity style={styles.actionBtn} onPress={
|
|
123
|
-
<AtomicIcon name="
|
|
188
|
+
{!isSelectionMode && (
|
|
189
|
+
<View style={styles.actions}>
|
|
190
|
+
<TouchableOpacity style={styles.actionBtn} onPress={handleShare}>
|
|
191
|
+
<AtomicIcon name="share-social" size="sm" color="primary" />
|
|
124
192
|
</TouchableOpacity>
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
</
|
|
129
|
-
|
|
130
|
-
<AtomicIcon name="trash" size="sm" color="error" />
|
|
131
|
-
</TouchableOpacity>
|
|
132
|
-
</View>
|
|
193
|
+
<TouchableOpacity style={styles.actionBtn} onPress={handleDelete}>
|
|
194
|
+
<AtomicIcon name="trash" size="sm" color="error" />
|
|
195
|
+
</TouchableOpacity>
|
|
196
|
+
</View>
|
|
197
|
+
)}
|
|
133
198
|
</View>
|
|
134
|
-
</
|
|
199
|
+
</TouchableOpacity>
|
|
135
200
|
);
|
|
136
201
|
}
|
|
@@ -13,6 +13,10 @@ interface CreationsGridProps {
|
|
|
13
13
|
readonly onView: (creation: Creation) => void;
|
|
14
14
|
readonly onShare: (creation: Creation) => void;
|
|
15
15
|
readonly onDelete: (creation: Creation) => void;
|
|
16
|
+
readonly onToggleFavorite?: (creation: Creation) => void;
|
|
17
|
+
readonly selectedIds?: string[];
|
|
18
|
+
readonly onSelect?: (creation: Creation) => void;
|
|
19
|
+
readonly isSelectionMode?: boolean;
|
|
16
20
|
readonly contentContainerStyle?: any;
|
|
17
21
|
readonly ListEmptyComponent?: React.ReactElement | null;
|
|
18
22
|
readonly ListHeaderComponent?: React.ComponentType<any> | React.ReactElement | null;
|
|
@@ -26,6 +30,10 @@ export const CreationsGrid: React.FC<CreationsGridProps> = ({
|
|
|
26
30
|
onView,
|
|
27
31
|
onShare,
|
|
28
32
|
onDelete,
|
|
33
|
+
onToggleFavorite,
|
|
34
|
+
selectedIds = [],
|
|
35
|
+
onSelect,
|
|
36
|
+
isSelectionMode = false,
|
|
29
37
|
contentContainerStyle,
|
|
30
38
|
ListEmptyComponent,
|
|
31
39
|
ListHeaderComponent,
|
|
@@ -40,6 +48,10 @@ export const CreationsGrid: React.FC<CreationsGridProps> = ({
|
|
|
40
48
|
onView={() => onView(item)}
|
|
41
49
|
onShare={() => onShare(item)}
|
|
42
50
|
onDelete={() => onDelete(item)}
|
|
51
|
+
onToggleFavorite={onToggleFavorite}
|
|
52
|
+
isSelected={selectedIds.includes(item.id)}
|
|
53
|
+
onSelect={onSelect}
|
|
54
|
+
isSelectionMode={isSelectionMode}
|
|
43
55
|
/>
|
|
44
56
|
);
|
|
45
57
|
|
|
@@ -10,6 +10,8 @@ interface GalleryHeaderProps {
|
|
|
10
10
|
readonly onFilterPress: () => void;
|
|
11
11
|
readonly filterLabel?: string;
|
|
12
12
|
readonly filterIcon?: any;
|
|
13
|
+
readonly onFavoritesPress?: () => void;
|
|
14
|
+
readonly showOnlyFavorites?: boolean;
|
|
13
15
|
readonly style?: any;
|
|
14
16
|
}
|
|
15
17
|
|
|
@@ -21,6 +23,8 @@ export const GalleryHeader: React.FC<GalleryHeaderProps> = ({
|
|
|
21
23
|
onFilterPress,
|
|
22
24
|
filterLabel = 'Filter',
|
|
23
25
|
filterIcon = 'filter-outline',
|
|
26
|
+
onFavoritesPress,
|
|
27
|
+
showOnlyFavorites = false,
|
|
24
28
|
style,
|
|
25
29
|
}) => {
|
|
26
30
|
const tokens = useAppDesignTokens();
|
|
@@ -28,29 +32,44 @@ export const GalleryHeader: React.FC<GalleryHeaderProps> = ({
|
|
|
28
32
|
|
|
29
33
|
return (
|
|
30
34
|
<View style={[styles.headerArea, style]}>
|
|
31
|
-
<View>
|
|
32
|
-
<AtomicText style={styles.title}>{title}</AtomicText>
|
|
35
|
+
<View style={styles.titleArea}>
|
|
36
|
+
{!!title && <AtomicText style={styles.title}>{title}</AtomicText>}
|
|
33
37
|
<AtomicText style={styles.subtitle}>
|
|
34
38
|
{count} {countLabel}
|
|
35
39
|
</AtomicText>
|
|
36
40
|
</View>
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
{isFiltered && (
|
|
51
|
-
<View style={styles.badge} />
|
|
41
|
+
<View style={styles.actions}>
|
|
42
|
+
{onFavoritesPress && (
|
|
43
|
+
<TouchableOpacity
|
|
44
|
+
onPress={onFavoritesPress}
|
|
45
|
+
style={[styles.actionButton, showOnlyFavorites && styles.actionButtonActive]}
|
|
46
|
+
activeOpacity={0.7}
|
|
47
|
+
>
|
|
48
|
+
<AtomicIcon
|
|
49
|
+
name={showOnlyFavorites ? "heart" : "heart-outline"}
|
|
50
|
+
size="sm"
|
|
51
|
+
color={showOnlyFavorites ? "error" : "secondary"}
|
|
52
|
+
/>
|
|
53
|
+
</TouchableOpacity>
|
|
52
54
|
)}
|
|
53
|
-
|
|
55
|
+
<TouchableOpacity
|
|
56
|
+
onPress={onFilterPress}
|
|
57
|
+
style={[styles.filterButton, isFiltered && styles.filterButtonActive]}
|
|
58
|
+
activeOpacity={0.7}
|
|
59
|
+
>
|
|
60
|
+
<AtomicIcon
|
|
61
|
+
name={filterIcon}
|
|
62
|
+
size="sm"
|
|
63
|
+
color={isFiltered ? "primary" : "secondary"}
|
|
64
|
+
/>
|
|
65
|
+
<AtomicText style={[styles.filterText, { color: isFiltered ? tokens.colors.primary : tokens.colors.textSecondary }]}>
|
|
66
|
+
{filterLabel}
|
|
67
|
+
</AtomicText>
|
|
68
|
+
{isFiltered && (
|
|
69
|
+
<View style={styles.badge} />
|
|
70
|
+
)}
|
|
71
|
+
</TouchableOpacity>
|
|
72
|
+
</View>
|
|
54
73
|
</View>
|
|
55
74
|
);
|
|
56
75
|
};
|
|
@@ -64,6 +83,10 @@ const useStyles = (tokens: any) => StyleSheet.create({
|
|
|
64
83
|
paddingVertical: tokens.spacing.sm,
|
|
65
84
|
marginBottom: tokens.spacing.sm,
|
|
66
85
|
},
|
|
86
|
+
titleArea: {
|
|
87
|
+
flex: 1,
|
|
88
|
+
marginRight: tokens.spacing.md,
|
|
89
|
+
},
|
|
67
90
|
title: {
|
|
68
91
|
fontSize: 20,
|
|
69
92
|
fontWeight: "700",
|
|
@@ -75,6 +98,25 @@ const useStyles = (tokens: any) => StyleSheet.create({
|
|
|
75
98
|
color: tokens.colors.textSecondary,
|
|
76
99
|
opacity: 0.6
|
|
77
100
|
},
|
|
101
|
+
actions: {
|
|
102
|
+
flexDirection: 'row',
|
|
103
|
+
alignItems: 'center',
|
|
104
|
+
gap: tokens.spacing.sm,
|
|
105
|
+
},
|
|
106
|
+
actionButton: {
|
|
107
|
+
width: 40,
|
|
108
|
+
height: 40,
|
|
109
|
+
borderRadius: 20,
|
|
110
|
+
backgroundColor: tokens.colors.surfaceVariant,
|
|
111
|
+
justifyContent: 'center',
|
|
112
|
+
alignItems: 'center',
|
|
113
|
+
borderWidth: 1,
|
|
114
|
+
borderColor: 'transparent',
|
|
115
|
+
},
|
|
116
|
+
actionButtonActive: {
|
|
117
|
+
backgroundColor: tokens.colors.primary + "15",
|
|
118
|
+
borderColor: tokens.colors.primary + "30",
|
|
119
|
+
},
|
|
78
120
|
filterButton: {
|
|
79
121
|
flexDirection: 'row',
|
|
80
122
|
alignItems: 'center',
|
|
@@ -85,6 +127,7 @@ const useStyles = (tokens: any) => StyleSheet.create({
|
|
|
85
127
|
backgroundColor: tokens.colors.surfaceVariant,
|
|
86
128
|
borderWidth: 1,
|
|
87
129
|
borderColor: 'transparent',
|
|
130
|
+
height: 40,
|
|
88
131
|
},
|
|
89
132
|
filterButtonActive: {
|
|
90
133
|
backgroundColor: tokens.colors.primary + "15",
|
|
@@ -5,3 +5,5 @@
|
|
|
5
5
|
export { useCreations } from "./useCreations";
|
|
6
6
|
export { useDeleteCreation } from "./useDeleteCreation";
|
|
7
7
|
export { useCreationsFilter } from "./useCreationsFilter";
|
|
8
|
+
export { useToggleFavorite } from "./useToggleFavorite";
|
|
9
|
+
export { useDeleteMultipleCreations } from "./useDeleteMultipleCreations";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useCreationsFilter Hook
|
|
3
|
-
* Handles filtering of creations by type
|
|
3
|
+
* Handles filtering of creations by type, search, and favorites
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { useState, useMemo, useCallback } from "react";
|
|
@@ -16,41 +16,55 @@ export function useCreationsFilter({
|
|
|
16
16
|
defaultFilterId = "all"
|
|
17
17
|
}: UseCreationsFilterProps) {
|
|
18
18
|
const [selectedIds, setSelectedIds] = useState<string[]>([defaultFilterId]);
|
|
19
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
20
|
+
const [showOnlyFavorites, setShowOnlyFavorites] = useState(false);
|
|
19
21
|
|
|
20
22
|
const filtered = useMemo(() => {
|
|
21
23
|
if (!creations) return [];
|
|
22
|
-
if (selectedIds.includes(defaultFilterId)) return creations;
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
)
|
|
28
|
-
|
|
25
|
+
let result = creations;
|
|
26
|
+
|
|
27
|
+
// 1. Filter by Favorites
|
|
28
|
+
if (showOnlyFavorites) {
|
|
29
|
+
result = result.filter(c => c.isFavorite);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 2. Filter by Category/Type
|
|
33
|
+
if (!selectedIds.includes(defaultFilterId)) {
|
|
34
|
+
result = result.filter((c) =>
|
|
35
|
+
selectedIds.includes(c.type) ||
|
|
36
|
+
selectedIds.some(id => (c as any).metadata?.tags?.includes(id))
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 3. Filter by Search Query
|
|
41
|
+
if (searchQuery.trim()) {
|
|
42
|
+
const query = searchQuery.toLowerCase();
|
|
43
|
+
result = result.filter(c =>
|
|
44
|
+
c.prompt?.toLowerCase().includes(query) ||
|
|
45
|
+
c.type.toLowerCase().includes(query)
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return result;
|
|
50
|
+
}, [creations, selectedIds, defaultFilterId, searchQuery, showOnlyFavorites]);
|
|
29
51
|
|
|
30
52
|
const toggleFilter = useCallback((filterId: string, multiSelect: boolean = false) => {
|
|
31
53
|
setSelectedIds(prev => {
|
|
32
|
-
// If selecting 'all', clear everything else
|
|
33
54
|
if (filterId === defaultFilterId) return [defaultFilterId];
|
|
34
55
|
|
|
35
56
|
let newIds: string[];
|
|
36
57
|
if (!multiSelect) {
|
|
37
|
-
// Single select
|
|
38
|
-
// If we tap the already selected item in single mode, should we Deselect it?
|
|
39
|
-
// Typically in radio-button style filters, no. But let's assume valid toggling behavior suitable for the UI.
|
|
40
|
-
// If single select, simply switch to the new one.
|
|
41
58
|
if (prev.includes(filterId) && prev.length === 1) return prev;
|
|
42
59
|
newIds = [filterId];
|
|
43
60
|
} else {
|
|
44
|
-
// Multi select
|
|
45
61
|
if (prev.includes(filterId)) {
|
|
46
62
|
newIds = prev.filter(id => id !== filterId);
|
|
47
63
|
} else {
|
|
48
|
-
// Remove 'all' if present
|
|
49
64
|
newIds = [...prev.filter(id => id !== defaultFilterId), filterId];
|
|
50
65
|
}
|
|
51
66
|
}
|
|
52
67
|
|
|
53
|
-
// If nothing selected, revert to 'all'
|
|
54
68
|
if (newIds.length === 0) return [defaultFilterId];
|
|
55
69
|
return newIds;
|
|
56
70
|
});
|
|
@@ -58,13 +72,19 @@ export function useCreationsFilter({
|
|
|
58
72
|
|
|
59
73
|
const clearFilters = useCallback(() => {
|
|
60
74
|
setSelectedIds([defaultFilterId]);
|
|
75
|
+
setSearchQuery("");
|
|
76
|
+
setShowOnlyFavorites(false);
|
|
61
77
|
}, [defaultFilterId]);
|
|
62
78
|
|
|
63
79
|
return {
|
|
64
80
|
filtered,
|
|
65
81
|
selectedIds,
|
|
82
|
+
searchQuery,
|
|
83
|
+
setSearchQuery,
|
|
84
|
+
showOnlyFavorites,
|
|
85
|
+
setShowOnlyFavorites,
|
|
66
86
|
toggleFilter,
|
|
67
87
|
clearFilters,
|
|
68
|
-
isFiltered: !selectedIds.includes(defaultFilterId),
|
|
88
|
+
isFiltered: !selectedIds.includes(defaultFilterId) || !!searchQuery || showOnlyFavorites,
|
|
69
89
|
};
|
|
70
90
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useDeleteMultipleCreations Hook
|
|
3
|
+
* Handles batch deletion of user creations with optimistic update
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
7
|
+
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
8
|
+
import type { Creation } from "../../domain/entities/Creation";
|
|
9
|
+
|
|
10
|
+
interface UseDeleteMultipleCreationsProps {
|
|
11
|
+
readonly userId: string | null;
|
|
12
|
+
readonly repository: ICreationsRepository;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useDeleteMultipleCreations({
|
|
16
|
+
userId,
|
|
17
|
+
repository,
|
|
18
|
+
}: UseDeleteMultipleCreationsProps) {
|
|
19
|
+
const queryClient = useQueryClient();
|
|
20
|
+
|
|
21
|
+
return useMutation({
|
|
22
|
+
mutationFn: async (creationIds: string[]) => {
|
|
23
|
+
if (!userId) return false;
|
|
24
|
+
return repository.deleteMultiple(userId, creationIds);
|
|
25
|
+
},
|
|
26
|
+
onMutate: async (creationIds) => {
|
|
27
|
+
if (!userId) return;
|
|
28
|
+
|
|
29
|
+
await queryClient.cancelQueries({
|
|
30
|
+
queryKey: ["creations", userId],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const previous = queryClient.getQueryData<Creation[]>([
|
|
34
|
+
"creations",
|
|
35
|
+
userId,
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
queryClient.setQueryData<Creation[]>(
|
|
39
|
+
["creations", userId],
|
|
40
|
+
(old) =>
|
|
41
|
+
old?.filter((c) => !creationIds.includes(c.id)) ?? [],
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return { previous };
|
|
45
|
+
},
|
|
46
|
+
onError: (_err, _ids, rollback) => {
|
|
47
|
+
if (userId && rollback?.previous) {
|
|
48
|
+
queryClient.setQueryData(["creations", userId], rollback.previous);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
onSettled: () => {
|
|
52
|
+
if (userId) {
|
|
53
|
+
queryClient.invalidateQueries({ queryKey: ["creations", userId] });
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useToggleFavorite Hook
|
|
3
|
+
* Handles favoriting/unfavoriting of user creations with optimistic update
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
7
|
+
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
8
|
+
import type { Creation } from "../../domain/entities/Creation";
|
|
9
|
+
|
|
10
|
+
interface UseToggleFavoriteProps {
|
|
11
|
+
readonly userId: string | null;
|
|
12
|
+
readonly repository: ICreationsRepository;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useToggleFavorite({
|
|
16
|
+
userId,
|
|
17
|
+
repository,
|
|
18
|
+
}: UseToggleFavoriteProps) {
|
|
19
|
+
const queryClient = useQueryClient();
|
|
20
|
+
|
|
21
|
+
return useMutation({
|
|
22
|
+
mutationFn: async (creationId: string) => {
|
|
23
|
+
if (!userId) return false;
|
|
24
|
+
return repository.toggleFavorite(userId, creationId);
|
|
25
|
+
},
|
|
26
|
+
onMutate: async (creationId) => {
|
|
27
|
+
if (!userId) return;
|
|
28
|
+
|
|
29
|
+
await queryClient.cancelQueries({
|
|
30
|
+
queryKey: ["creations", userId],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const previous = queryClient.getQueryData<Creation[]>([
|
|
34
|
+
"creations",
|
|
35
|
+
userId,
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
queryClient.setQueryData<Creation[]>(
|
|
39
|
+
["creations", userId],
|
|
40
|
+
(old) =>
|
|
41
|
+
old?.map((c) =>
|
|
42
|
+
c.id === creationId ? { ...c, isFavorite: !c.isFavorite } : c,
|
|
43
|
+
) ?? [],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return { previous };
|
|
47
|
+
},
|
|
48
|
+
onError: (_err, _id, rollback) => {
|
|
49
|
+
if (userId && rollback?.previous) {
|
|
50
|
+
queryClient.setQueryData(["creations", userId], rollback.previous);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
onSettled: () => {
|
|
54
|
+
if (userId) {
|
|
55
|
+
queryClient.invalidateQueries({ queryKey: ["creations", userId] });
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
import React, { useMemo, useCallback, useState } from "react";
|
|
2
|
-
import { View, StyleSheet
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { View, StyleSheet } from "react-native";
|
|
3
|
+
import {
|
|
4
|
+
useAppDesignTokens,
|
|
5
|
+
useSharing,
|
|
6
|
+
ScreenLayout,
|
|
7
|
+
ScreenHeader,
|
|
8
|
+
useAlert,
|
|
9
|
+
AlertMode,
|
|
10
|
+
type BottomSheetModalRef
|
|
11
|
+
} from "@umituz/react-native-design-system";
|
|
6
12
|
import { useFocusEffect } from "@react-navigation/native";
|
|
7
13
|
import { useCreations } from "../hooks/useCreations";
|
|
8
14
|
import { useDeleteCreation } from "../hooks/useDeleteCreation";
|
|
9
15
|
import { useCreationsFilter } from "../hooks/useCreationsFilter";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
16
|
+
import { useToggleFavorite } from "../hooks/useToggleFavorite";
|
|
17
|
+
import { useDeleteMultipleCreations } from "../hooks/useDeleteMultipleCreations";
|
|
18
|
+
import {
|
|
19
|
+
GalleryHeader,
|
|
20
|
+
CreationsGrid,
|
|
21
|
+
FilterBottomSheet,
|
|
22
|
+
CreationImageViewer,
|
|
23
|
+
CreationsGalleryEmptyState
|
|
24
|
+
} from "../components";
|
|
25
|
+
import { SearchBar } from "@umituz/react-native-design-system";
|
|
12
26
|
import { getTranslatedTypes, getFilterCategoriesFromConfig } from "../utils/filterUtils";
|
|
13
27
|
import type { Creation } from "../../domain/entities/Creation";
|
|
14
28
|
import type { CreationsConfig } from "../../domain/value-objects/CreationsConfig";
|
|
15
29
|
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
16
|
-
import { CreationDetailScreen } from "./CreationDetailScreen";
|
|
17
30
|
|
|
18
31
|
interface CreationsGalleryScreenProps {
|
|
19
32
|
readonly userId: string | null;
|
|
@@ -24,6 +37,8 @@ interface CreationsGalleryScreenProps {
|
|
|
24
37
|
readonly onImageEdit?: (uri: string, creationId: string) => void | Promise<void>;
|
|
25
38
|
readonly onEmptyAction?: () => void;
|
|
26
39
|
readonly emptyActionLabel?: string;
|
|
40
|
+
readonly onBackPress?: () => void;
|
|
41
|
+
readonly headerTitle?: string;
|
|
27
42
|
}
|
|
28
43
|
|
|
29
44
|
export function CreationsGalleryScreen({
|
|
@@ -35,9 +50,10 @@ export function CreationsGalleryScreen({
|
|
|
35
50
|
onImageEdit,
|
|
36
51
|
onEmptyAction,
|
|
37
52
|
emptyActionLabel,
|
|
53
|
+
onBackPress,
|
|
54
|
+
headerTitle,
|
|
38
55
|
}: CreationsGalleryScreenProps) {
|
|
39
56
|
const tokens = useAppDesignTokens();
|
|
40
|
-
const insets = useSafeAreaInsets();
|
|
41
57
|
const { share } = useSharing();
|
|
42
58
|
const alert = useAlert();
|
|
43
59
|
|
|
@@ -48,8 +64,27 @@ export function CreationsGalleryScreen({
|
|
|
48
64
|
|
|
49
65
|
const { data: creationsData, isLoading, refetch } = useCreations({ userId, repository });
|
|
50
66
|
const creations = creationsData as Creation[] | undefined;
|
|
67
|
+
|
|
51
68
|
const deleteMutation = useDeleteCreation({ userId, repository });
|
|
52
|
-
const
|
|
69
|
+
const toggleFavoriteMutation = useToggleFavorite({ userId, repository });
|
|
70
|
+
const deleteMultipleMutation = useDeleteMultipleCreations({ userId, repository });
|
|
71
|
+
|
|
72
|
+
const {
|
|
73
|
+
filtered,
|
|
74
|
+
selectedIds: selectedCategoryIds,
|
|
75
|
+
searchQuery,
|
|
76
|
+
setSearchQuery,
|
|
77
|
+
showOnlyFavorites,
|
|
78
|
+
setShowOnlyFavorites,
|
|
79
|
+
toggleFilter,
|
|
80
|
+
clearFilters,
|
|
81
|
+
isFiltered
|
|
82
|
+
} = useCreationsFilter({ creations });
|
|
83
|
+
|
|
84
|
+
const [selectedItemIds, setSelectedItemIds] = useState<string[]>([]);
|
|
85
|
+
const [isSelectionMode, setIsSelectionMode] = useState(false);
|
|
86
|
+
|
|
87
|
+
|
|
53
88
|
|
|
54
89
|
// Refetch creations when screen comes into focus
|
|
55
90
|
useFocusEffect(
|
|
@@ -85,37 +120,93 @@ export function CreationsGalleryScreen({
|
|
|
85
120
|
);
|
|
86
121
|
}, [alert, config, deleteMutation, t]);
|
|
87
122
|
|
|
123
|
+
const handleToggleFavorite = useCallback((creation: Creation) => {
|
|
124
|
+
toggleFavoriteMutation.mutate(creation.id);
|
|
125
|
+
}, [toggleFavoriteMutation]);
|
|
126
|
+
|
|
127
|
+
const toggleSelection = useCallback((creation: Creation) => {
|
|
128
|
+
setSelectedItemIds(prev => {
|
|
129
|
+
const isSelected = prev.includes(creation.id);
|
|
130
|
+
const next = isSelected ? prev.filter(id => id !== creation.id) : [...prev, creation.id];
|
|
131
|
+
if (next.length === 0) setIsSelectionMode(false);
|
|
132
|
+
else setIsSelectionMode(true);
|
|
133
|
+
return next;
|
|
134
|
+
});
|
|
135
|
+
}, []);
|
|
136
|
+
|
|
137
|
+
const handleDeleteSelected = useCallback(() => {
|
|
138
|
+
if (selectedItemIds.length === 0) return;
|
|
139
|
+
|
|
140
|
+
alert.showWarning(
|
|
141
|
+
t(config.translations.deleteTitle),
|
|
142
|
+
t("common.delete_selected_confirm") || `Are you sure you want to delete ${selectedItemIds.length} items?`,
|
|
143
|
+
{
|
|
144
|
+
mode: AlertMode.MODAL,
|
|
145
|
+
actions: [
|
|
146
|
+
{ id: 'cancel', label: t("common.cancel"), onPress: () => { } },
|
|
147
|
+
{
|
|
148
|
+
id: 'delete', label: t("common.delete"), style: 'destructive', onPress: async () => {
|
|
149
|
+
const success = await deleteMultipleMutation.mutateAsync(selectedItemIds);
|
|
150
|
+
if (success) {
|
|
151
|
+
setSelectedItemIds([]);
|
|
152
|
+
setIsSelectionMode(false);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
}, [alert, config, deleteMultipleMutation, selectedItemIds, t]);
|
|
160
|
+
|
|
88
161
|
// Handle viewing a creation - shows detail screen
|
|
89
162
|
const handleView = useCallback((creation: Creation) => {
|
|
90
163
|
setSelectedCreation(creation);
|
|
91
|
-
|
|
164
|
+
setViewerIndex(filtered.findIndex(c => c.id === creation.id));
|
|
165
|
+
setViewerVisible(true);
|
|
166
|
+
}, [filtered]);
|
|
92
167
|
|
|
93
|
-
|
|
94
|
-
const
|
|
168
|
+
const screenTitle = headerTitle || t(config.translations.title) || 'My Creations';
|
|
169
|
+
const showScreenHeader = !!onBackPress;
|
|
95
170
|
|
|
96
171
|
return (
|
|
97
|
-
<
|
|
98
|
-
{
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
172
|
+
<ScreenLayout
|
|
173
|
+
edges={['top']}
|
|
174
|
+
scrollable={false}
|
|
175
|
+
header={
|
|
176
|
+
<View>
|
|
177
|
+
{showScreenHeader && (
|
|
178
|
+
<ScreenHeader
|
|
179
|
+
title={isSelectionMode ? `${selectedItemIds.length} Selected` : screenTitle}
|
|
180
|
+
onBackPress={isSelectionMode ? () => { setIsSelectionMode(false); setSelectedItemIds([]); } : onBackPress}
|
|
181
|
+
rightAction={isSelectionMode ? {
|
|
182
|
+
icon: 'trash',
|
|
183
|
+
onPress: handleDeleteSelected,
|
|
184
|
+
} : undefined}
|
|
185
|
+
/>
|
|
186
|
+
)}
|
|
187
|
+
{!isSelectionMode && (
|
|
188
|
+
<View style={{ paddingHorizontal: tokens.spacing.md, paddingBottom: tokens.spacing.xs }}>
|
|
189
|
+
<SearchBar
|
|
190
|
+
placeholder={t("common.search") || "Search Prompt..."}
|
|
191
|
+
value={searchQuery}
|
|
192
|
+
onChangeText={setSearchQuery}
|
|
193
|
+
/>
|
|
194
|
+
</View>
|
|
195
|
+
)}
|
|
196
|
+
</View>
|
|
197
|
+
}
|
|
198
|
+
contentContainerStyle={{ paddingHorizontal: 0 }}
|
|
199
|
+
>
|
|
110
200
|
{(!creations || creations?.length === 0) && !isLoading ? null : (
|
|
111
201
|
<GalleryHeader
|
|
112
|
-
title={
|
|
202
|
+
title={showScreenHeader || isSelectionMode ? '' : screenTitle}
|
|
113
203
|
count={filtered.length}
|
|
114
204
|
countLabel={t(config.translations.photoCount) || 'photos'}
|
|
115
205
|
isFiltered={isFiltered}
|
|
116
206
|
filterLabel={t(config.translations.filterLabel) || 'Filter'}
|
|
117
207
|
onFilterPress={() => filterSheetRef.current?.present()}
|
|
118
|
-
|
|
208
|
+
onFavoritesPress={() => setShowOnlyFavorites(!showOnlyFavorites)}
|
|
209
|
+
showOnlyFavorites={showOnlyFavorites}
|
|
119
210
|
/>
|
|
120
211
|
)}
|
|
121
212
|
|
|
@@ -128,7 +219,11 @@ export function CreationsGalleryScreen({
|
|
|
128
219
|
onView={handleView}
|
|
129
220
|
onShare={handleShare}
|
|
130
221
|
onDelete={handleDelete}
|
|
131
|
-
|
|
222
|
+
onToggleFavorite={handleToggleFavorite}
|
|
223
|
+
isSelectionMode={isSelectionMode}
|
|
224
|
+
selectedIds={selectedItemIds}
|
|
225
|
+
onSelect={toggleSelection}
|
|
226
|
+
contentContainerStyle={{ paddingBottom: tokens.spacing.xl }}
|
|
132
227
|
ListEmptyComponent={
|
|
133
228
|
<CreationsGalleryEmptyState
|
|
134
229
|
isLoading={isLoading}
|
|
@@ -156,15 +251,11 @@ export function CreationsGalleryScreen({
|
|
|
156
251
|
<FilterBottomSheet
|
|
157
252
|
ref={filterSheetRef}
|
|
158
253
|
categories={allCategories}
|
|
159
|
-
selectedIds={
|
|
254
|
+
selectedIds={selectedCategoryIds}
|
|
160
255
|
onFilterPress={(id, catId) => toggleFilter(id, allCategories.find(c => c.id === catId)?.multiSelect)}
|
|
161
256
|
onClearFilters={clearFilters}
|
|
162
257
|
title={t(config.translations.filterTitle) || t("common.filter")}
|
|
163
258
|
/>
|
|
164
|
-
</
|
|
259
|
+
</ScreenLayout>
|
|
165
260
|
);
|
|
166
261
|
}
|
|
167
|
-
|
|
168
|
-
const useStyles = (tokens: any) => StyleSheet.create({
|
|
169
|
-
container: { flex: 1, backgroundColor: tokens.colors.background },
|
|
170
|
-
});
|