@umituz/react-native-ai-generation-content 1.12.24 → 1.12.26
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/application/services/CreationsService.ts +73 -0
- package/src/domains/creations/domain/entities/Creation.ts +60 -0
- package/src/domains/creations/domain/entities/index.ts +6 -0
- package/src/domains/creations/domain/repositories/ICreationsRepository.ts +23 -0
- package/src/domains/creations/domain/repositories/index.ts +5 -0
- package/src/domains/creations/domain/services/ICreationsStorageService.ts +13 -0
- package/src/domains/creations/domain/value-objects/CreationsConfig.ts +75 -0
- package/src/domains/creations/domain/value-objects/index.ts +12 -0
- package/src/domains/creations/index.ts +84 -0
- package/src/domains/creations/infrastructure/adapters/createRepository.ts +54 -0
- package/src/domains/creations/infrastructure/adapters/index.ts +5 -0
- package/src/domains/creations/infrastructure/repositories/CreationsRepository.ts +241 -0
- package/src/domains/creations/infrastructure/repositories/index.ts +8 -0
- package/src/domains/creations/infrastructure/services/CreationsStorageService.ts +49 -0
- package/src/domains/creations/presentation/components/CreationCard.tsx +136 -0
- package/src/domains/creations/presentation/components/CreationDetail/DetailActions.tsx +76 -0
- package/src/domains/creations/presentation/components/CreationDetail/DetailHeader.tsx +81 -0
- package/src/domains/creations/presentation/components/CreationDetail/DetailImage.tsx +41 -0
- package/src/domains/creations/presentation/components/CreationDetail/DetailStory.tsx +67 -0
- package/src/domains/creations/presentation/components/CreationDetail/index.ts +4 -0
- package/src/domains/creations/presentation/components/CreationImageViewer.tsx +43 -0
- package/src/domains/creations/presentation/components/CreationThumbnail.tsx +63 -0
- package/src/domains/creations/presentation/components/CreationsGrid.tsx +75 -0
- package/src/domains/creations/presentation/components/CreationsHomeCard.tsx +176 -0
- package/src/domains/creations/presentation/components/EmptyState.tsx +82 -0
- package/src/domains/creations/presentation/components/FilterBottomSheet.tsx +160 -0
- package/src/domains/creations/presentation/components/FilterChips.tsx +105 -0
- package/src/domains/creations/presentation/components/GalleryHeader.tsx +106 -0
- package/src/domains/creations/presentation/components/index.ts +19 -0
- package/src/domains/creations/presentation/hooks/index.ts +7 -0
- package/src/domains/creations/presentation/hooks/useCreations.ts +38 -0
- package/src/domains/creations/presentation/hooks/useCreationsFilter.ts +77 -0
- package/src/domains/creations/presentation/hooks/useDeleteCreation.ts +51 -0
- package/src/domains/creations/presentation/screens/CreationDetailScreen.tsx +78 -0
- package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +217 -0
- package/src/domains/creations/presentation/screens/index.ts +5 -0
- package/src/domains/creations/presentation/utils/filterUtils.ts +52 -0
- package/src/domains/creations/types.d.ts +42 -0
- package/src/features/background/presentation/components/ComparisonSlider.tsx +4 -4
- package/src/features/background/presentation/components/ErrorDisplay.tsx +3 -3
- package/src/features/background/presentation/components/FeatureHeader.tsx +1 -1
- package/src/features/background/presentation/components/GenerateButton.tsx +0 -2
- package/src/features/background/presentation/components/ImagePicker.tsx +3 -3
- package/src/features/background/presentation/components/ProcessingModal.tsx +2 -2
- package/src/features/background/presentation/components/PromptInput.tsx +5 -5
- package/src/features/background/presentation/components/ResultDisplay.tsx +4 -4
- package/src/index.ts +5 -0
- package/src/presentation/components/GenerationProgressContent.tsx +4 -4
- package/src/presentation/components/PendingJobCard.tsx +2 -2
- package/src/presentation/components/PendingJobCardActions.tsx +2 -2
- package/src/presentation/components/result/GenerationResultContent.tsx +2 -3
- package/src/presentation/hooks/usePhotoGeneration.ts +7 -5
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useCreationsFilter Hook
|
|
3
|
+
* Handles filtering of creations by type
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useMemo, useCallback } from "react";
|
|
7
|
+
import type { Creation } from "../../domain/entities/Creation";
|
|
8
|
+
|
|
9
|
+
interface UseCreationsFilterProps {
|
|
10
|
+
readonly creations: Creation[] | undefined;
|
|
11
|
+
readonly defaultFilterId?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface CreationWithTags extends Creation {
|
|
15
|
+
readonly metadata?: {
|
|
16
|
+
readonly tags?: string[];
|
|
17
|
+
readonly [key: string]: unknown;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useCreationsFilter({
|
|
22
|
+
creations,
|
|
23
|
+
defaultFilterId = "all"
|
|
24
|
+
}: UseCreationsFilterProps) {
|
|
25
|
+
const [selectedIds, setSelectedIds] = useState<string[]>([defaultFilterId]);
|
|
26
|
+
|
|
27
|
+
const filtered = useMemo(() => {
|
|
28
|
+
if (!creations) return [];
|
|
29
|
+
if (selectedIds.includes(defaultFilterId)) return creations;
|
|
30
|
+
|
|
31
|
+
return creations.filter((c) => {
|
|
32
|
+
const creation = c as CreationWithTags;
|
|
33
|
+
return (
|
|
34
|
+
selectedIds.includes(creation.type) ||
|
|
35
|
+
selectedIds.some(id => creation.metadata?.tags?.includes(id))
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
}, [creations, selectedIds, defaultFilterId]);
|
|
39
|
+
|
|
40
|
+
const toggleFilter = useCallback((filterId: string, multiSelect = false) => {
|
|
41
|
+
setSelectedIds(prev => {
|
|
42
|
+
// If selecting 'all', clear everything else
|
|
43
|
+
if (filterId === defaultFilterId) return [defaultFilterId];
|
|
44
|
+
|
|
45
|
+
let newIds: string[];
|
|
46
|
+
if (!multiSelect) {
|
|
47
|
+
// Single select
|
|
48
|
+
if (prev.includes(filterId) && prev.length === 1) return prev;
|
|
49
|
+
newIds = [filterId];
|
|
50
|
+
} else {
|
|
51
|
+
// Multi select
|
|
52
|
+
if (prev.includes(filterId)) {
|
|
53
|
+
newIds = prev.filter(id => id !== filterId);
|
|
54
|
+
} else {
|
|
55
|
+
// Remove 'all' if present
|
|
56
|
+
newIds = [...prev.filter(id => id !== defaultFilterId), filterId];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// If nothing selected, revert to 'all'
|
|
61
|
+
if (newIds.length === 0) return [defaultFilterId];
|
|
62
|
+
return newIds;
|
|
63
|
+
});
|
|
64
|
+
}, [defaultFilterId]);
|
|
65
|
+
|
|
66
|
+
const clearFilters = useCallback(() => {
|
|
67
|
+
setSelectedIds([defaultFilterId]);
|
|
68
|
+
}, [defaultFilterId]);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
filtered,
|
|
72
|
+
selectedIds,
|
|
73
|
+
toggleFilter,
|
|
74
|
+
clearFilters,
|
|
75
|
+
isFiltered: !selectedIds.includes(defaultFilterId),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useDeleteCreation Hook
|
|
3
|
+
* Handles 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 UseDeleteCreationProps {
|
|
11
|
+
readonly userId: string | null;
|
|
12
|
+
readonly repository: ICreationsRepository;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useDeleteCreation({
|
|
16
|
+
userId,
|
|
17
|
+
repository,
|
|
18
|
+
}: UseDeleteCreationProps) {
|
|
19
|
+
const queryClient = useQueryClient();
|
|
20
|
+
|
|
21
|
+
return useMutation({
|
|
22
|
+
mutationFn: async (creationId: string) => {
|
|
23
|
+
if (!userId) return false;
|
|
24
|
+
return repository.delete(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) => old?.filter((c) => c.id !== creationId) ?? [],
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return { previous };
|
|
44
|
+
},
|
|
45
|
+
onError: (_err, _id, rollback) => {
|
|
46
|
+
if (userId && rollback?.previous) {
|
|
47
|
+
queryClient.setQueryData(["creations", userId], rollback.previous);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { View, StyleSheet, ScrollView } from 'react-native';
|
|
4
|
+
import { useAppDesignTokens, type DesignTokens } from "@umituz/react-native-design-system";
|
|
5
|
+
import type { Creation } from '../../domain/entities/Creation';
|
|
6
|
+
import { DetailHeader } from '../components/CreationDetail/DetailHeader';
|
|
7
|
+
import { DetailImage } from '../components/CreationDetail/DetailImage';
|
|
8
|
+
import { DetailStory } from '../components/CreationDetail/DetailStory';
|
|
9
|
+
import { DetailActions } from '../components/CreationDetail/DetailActions';
|
|
10
|
+
|
|
11
|
+
interface CreationDetailScreenProps {
|
|
12
|
+
readonly creation: Creation;
|
|
13
|
+
readonly onClose: () => void;
|
|
14
|
+
readonly onShare: (creation: Creation) => void;
|
|
15
|
+
readonly onDelete: (creation: Creation) => void;
|
|
16
|
+
readonly t: (key: string) => string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface CreationMetadata {
|
|
20
|
+
readonly names?: string;
|
|
21
|
+
readonly story?: string;
|
|
22
|
+
readonly description?: string;
|
|
23
|
+
readonly date?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const CreationDetailScreen: React.FC<CreationDetailScreenProps> = ({
|
|
27
|
+
creation,
|
|
28
|
+
onClose,
|
|
29
|
+
onShare,
|
|
30
|
+
onDelete,
|
|
31
|
+
t
|
|
32
|
+
}) => {
|
|
33
|
+
const tokens = useAppDesignTokens();
|
|
34
|
+
|
|
35
|
+
// Extract data safely
|
|
36
|
+
const metadata = (creation.metadata || {}) as CreationMetadata;
|
|
37
|
+
const title = metadata.names || creation.type;
|
|
38
|
+
const story = metadata.story || metadata.description || "";
|
|
39
|
+
const date = metadata.date || new Date(creation.createdAt).toLocaleDateString();
|
|
40
|
+
|
|
41
|
+
const styles = useStyles(tokens);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<View style={styles.container}>
|
|
45
|
+
<DetailHeader
|
|
46
|
+
title={title}
|
|
47
|
+
date={date}
|
|
48
|
+
onClose={onClose}
|
|
49
|
+
/>
|
|
50
|
+
|
|
51
|
+
<ScrollView
|
|
52
|
+
contentContainerStyle={styles.scrollContent}
|
|
53
|
+
showsVerticalScrollIndicator={false}
|
|
54
|
+
>
|
|
55
|
+
<DetailImage uri={creation.uri} />
|
|
56
|
+
|
|
57
|
+
<DetailStory story={story} />
|
|
58
|
+
|
|
59
|
+
<DetailActions
|
|
60
|
+
onShare={() => onShare(creation)}
|
|
61
|
+
onDelete={() => onDelete(creation)}
|
|
62
|
+
shareLabel={t("result.shareButton") || "Share"}
|
|
63
|
+
deleteLabel={t("common.delete") || "Delete"}
|
|
64
|
+
/>
|
|
65
|
+
</ScrollView>
|
|
66
|
+
</View>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const useStyles = (tokens: DesignTokens) => StyleSheet.create({
|
|
71
|
+
container: {
|
|
72
|
+
flex: 1,
|
|
73
|
+
backgroundColor: tokens.colors.background,
|
|
74
|
+
},
|
|
75
|
+
scrollContent: {
|
|
76
|
+
paddingBottom: tokens.spacing.xxl,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import React, { useMemo, useCallback, useState } from "react";
|
|
2
|
+
import { View, StyleSheet, ActivityIndicator, type LayoutChangeEvent } from "react-native";
|
|
3
|
+
import { useAppDesignTokens, useAlert, AlertType, AlertMode, useSharing, type DesignTokens } from "@umituz/react-native-design-system";
|
|
4
|
+
import { BottomSheetModal } from '@gorhom/bottom-sheet';
|
|
5
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
6
|
+
import { useFocusEffect } from "@react-navigation/native";
|
|
7
|
+
import { useCreations } from "../hooks/useCreations";
|
|
8
|
+
import { useDeleteCreation } from "../hooks/useDeleteCreation";
|
|
9
|
+
import { useCreationsFilter } from "../hooks/useCreationsFilter";
|
|
10
|
+
import { GalleryHeader, EmptyState, CreationsGrid, FilterBottomSheet, CreationImageViewer } from "../components";
|
|
11
|
+
import { getTranslatedTypes, getFilterCategoriesFromConfig } from "../utils/filterUtils";
|
|
12
|
+
import type { Creation } from "../../domain/entities/Creation";
|
|
13
|
+
import type { CreationsConfig } from "../../domain/value-objects/CreationsConfig";
|
|
14
|
+
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
15
|
+
import { CreationDetailScreen } from "./CreationDetailScreen";
|
|
16
|
+
|
|
17
|
+
interface CreationsGalleryScreenProps {
|
|
18
|
+
readonly userId: string | null;
|
|
19
|
+
readonly repository: ICreationsRepository;
|
|
20
|
+
readonly config: CreationsConfig;
|
|
21
|
+
readonly t: (key: string) => string;
|
|
22
|
+
readonly enableEditing?: boolean;
|
|
23
|
+
readonly onImageEdit?: (uri: string, creationId: string) => void | Promise<void>;
|
|
24
|
+
readonly onEmptyAction?: () => void;
|
|
25
|
+
readonly emptyActionLabel?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function CreationsGalleryScreen({
|
|
29
|
+
userId,
|
|
30
|
+
repository,
|
|
31
|
+
config,
|
|
32
|
+
t,
|
|
33
|
+
enableEditing = false,
|
|
34
|
+
onImageEdit,
|
|
35
|
+
onEmptyAction,
|
|
36
|
+
emptyActionLabel,
|
|
37
|
+
}: CreationsGalleryScreenProps) {
|
|
38
|
+
const tokens = useAppDesignTokens();
|
|
39
|
+
const insets = useSafeAreaInsets();
|
|
40
|
+
const { share } = useSharing();
|
|
41
|
+
const alert = useAlert();
|
|
42
|
+
|
|
43
|
+
const [viewerVisible, setViewerVisible] = useState(false);
|
|
44
|
+
const [viewerIndex, setViewerIndex] = useState(0);
|
|
45
|
+
const [selectedCreation, setSelectedCreation] = useState<Creation | null>(null);
|
|
46
|
+
const filterSheetRef = React.useRef<BottomSheetModal>(null);
|
|
47
|
+
|
|
48
|
+
const { data: creationsData, isLoading, refetch } = useCreations({ userId, repository });
|
|
49
|
+
const creations = creationsData;
|
|
50
|
+
const deleteMutation = useDeleteCreation({ userId, repository });
|
|
51
|
+
const { filtered, selectedIds, toggleFilter, clearFilters, isFiltered } = useCreationsFilter({ creations });
|
|
52
|
+
|
|
53
|
+
// Refetch creations when screen comes into focus
|
|
54
|
+
useFocusEffect(
|
|
55
|
+
useCallback(() => {
|
|
56
|
+
void refetch();
|
|
57
|
+
}, [refetch])
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Prepare data for UI using utils
|
|
61
|
+
const translatedTypes = useMemo(() => getTranslatedTypes(config, t), [config, t]);
|
|
62
|
+
const allCategories = useMemo(() => getFilterCategoriesFromConfig(config, t), [config, t]);
|
|
63
|
+
|
|
64
|
+
const handleShare = useCallback((creation: Creation) => {
|
|
65
|
+
void share(creation.uri, { dialogTitle: t("common.share") });
|
|
66
|
+
}, [share, t]);
|
|
67
|
+
|
|
68
|
+
const handleDelete = useCallback((creation: Creation) => {
|
|
69
|
+
alert.show(
|
|
70
|
+
AlertType.WARNING,
|
|
71
|
+
AlertMode.MODAL,
|
|
72
|
+
t(config.translations.deleteTitle),
|
|
73
|
+
t(config.translations.deleteMessage),
|
|
74
|
+
{
|
|
75
|
+
actions: [
|
|
76
|
+
{ id: 'cancel', label: t("common.cancel"), onPress: () => { } },
|
|
77
|
+
{
|
|
78
|
+
id: 'delete',
|
|
79
|
+
label: t("common.delete"),
|
|
80
|
+
style: 'destructive',
|
|
81
|
+
onPress: async () => {
|
|
82
|
+
const success = await deleteMutation.mutateAsync(creation.id);
|
|
83
|
+
if (success) {
|
|
84
|
+
setSelectedCreation(null);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
}, [alert, config, deleteMutation, t]);
|
|
92
|
+
|
|
93
|
+
// Handle viewing a creation - shows detail screen
|
|
94
|
+
const handleView = useCallback((creation: Creation) => {
|
|
95
|
+
setSelectedCreation(creation);
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
const styles = useStyles(tokens);
|
|
99
|
+
|
|
100
|
+
// Define empty state content based on state
|
|
101
|
+
const renderEmptyComponent = useMemo(() => {
|
|
102
|
+
// 1. Loading State
|
|
103
|
+
if (isLoading && (!creations || creations?.length === 0)) {
|
|
104
|
+
return (
|
|
105
|
+
<View style={styles.centerContainer}>
|
|
106
|
+
<ActivityIndicator size="large" color={tokens.colors.primary} />
|
|
107
|
+
</View>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 2. System Empty State (User has NO creations at all)
|
|
112
|
+
if (!creations || creations?.length === 0) {
|
|
113
|
+
return (
|
|
114
|
+
<View style={styles.centerContainer}>
|
|
115
|
+
<EmptyState
|
|
116
|
+
title={t(config.translations.empty)}
|
|
117
|
+
description={t(config.translations.emptyDescription)}
|
|
118
|
+
actionLabel={emptyActionLabel}
|
|
119
|
+
onAction={onEmptyAction}
|
|
120
|
+
/>
|
|
121
|
+
</View>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 3. Filter Empty State (User has creations, but filter returns none)
|
|
126
|
+
return (
|
|
127
|
+
<View style={styles.centerContainer}>
|
|
128
|
+
<EmptyState
|
|
129
|
+
title={t("common.no_results") || "No results"}
|
|
130
|
+
description={t("common.no_results_description") || "Try changing your filters"}
|
|
131
|
+
actionLabel={t("common.clear_all") || "Clear All"}
|
|
132
|
+
onAction={clearFilters}
|
|
133
|
+
/>
|
|
134
|
+
</View>
|
|
135
|
+
);
|
|
136
|
+
}, [isLoading, creations, config, t, emptyActionLabel, onEmptyAction, clearFilters, styles.centerContainer, tokens.colors.primary]);
|
|
137
|
+
|
|
138
|
+
if (selectedCreation) {
|
|
139
|
+
return (
|
|
140
|
+
<CreationDetailScreen
|
|
141
|
+
creation={selectedCreation}
|
|
142
|
+
onClose={() => setSelectedCreation(null)}
|
|
143
|
+
onShare={handleShare}
|
|
144
|
+
onDelete={handleDelete}
|
|
145
|
+
t={t}
|
|
146
|
+
/>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const handleLayout = (event: LayoutChangeEvent) => {
|
|
151
|
+
// Keep internal logic if needed, currently empty but handles the event correctly
|
|
152
|
+
void event;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<View style={styles.container} onLayout={handleLayout}>
|
|
157
|
+
{(!creations || creations?.length === 0) && !isLoading ? null : (
|
|
158
|
+
<GalleryHeader
|
|
159
|
+
title={t(config.translations.title) || 'My Creations'}
|
|
160
|
+
count={filtered.length}
|
|
161
|
+
countLabel={t(config.translations.photoCount) || 'photos'}
|
|
162
|
+
isFiltered={isFiltered}
|
|
163
|
+
filterLabel={t(config.translations.filterLabel) || 'Filter'}
|
|
164
|
+
onFilterPress={() => filterSheetRef.current?.present()}
|
|
165
|
+
style={{ paddingTop: insets.top + tokens.spacing.md }}
|
|
166
|
+
/>
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
{/* Main Content Grid - handles empty/loading via ListEmptyComponent */}
|
|
170
|
+
<CreationsGrid
|
|
171
|
+
creations={filtered}
|
|
172
|
+
types={translatedTypes}
|
|
173
|
+
isLoading={isLoading}
|
|
174
|
+
onRefresh={() => void refetch()}
|
|
175
|
+
onView={handleView}
|
|
176
|
+
onShare={handleShare}
|
|
177
|
+
onDelete={handleDelete}
|
|
178
|
+
contentContainerStyle={{ paddingBottom: insets.bottom + tokens.spacing.xl }}
|
|
179
|
+
ListEmptyComponent={renderEmptyComponent}
|
|
180
|
+
/>
|
|
181
|
+
|
|
182
|
+
<CreationImageViewer
|
|
183
|
+
creations={filtered}
|
|
184
|
+
visible={viewerVisible}
|
|
185
|
+
index={viewerIndex}
|
|
186
|
+
onDismiss={() => setViewerVisible(false)}
|
|
187
|
+
onIndexChange={setViewerIndex}
|
|
188
|
+
enableEditing={enableEditing}
|
|
189
|
+
onImageEdit={onImageEdit}
|
|
190
|
+
selectedCreationId={selectedCreation?.id}
|
|
191
|
+
/>
|
|
192
|
+
|
|
193
|
+
<FilterBottomSheet
|
|
194
|
+
ref={filterSheetRef}
|
|
195
|
+
categories={allCategories}
|
|
196
|
+
selectedIds={selectedIds}
|
|
197
|
+
onFilterPress={(id, catId) => {
|
|
198
|
+
const category = allCategories.find(c => c.id === catId);
|
|
199
|
+
toggleFilter(id, category?.multiSelect);
|
|
200
|
+
}}
|
|
201
|
+
onClearFilters={clearFilters}
|
|
202
|
+
title={t(config.translations.filterTitle) || t("common.filter")}
|
|
203
|
+
/>
|
|
204
|
+
</View>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const useStyles = (tokens: DesignTokens) => StyleSheet.create({
|
|
209
|
+
container: { flex: 1, backgroundColor: tokens.colors.background },
|
|
210
|
+
centerContainer: {
|
|
211
|
+
flex: 1,
|
|
212
|
+
justifyContent: 'center',
|
|
213
|
+
alignItems: 'center',
|
|
214
|
+
minHeight: 400,
|
|
215
|
+
paddingHorizontal: tokens.spacing.xl
|
|
216
|
+
},
|
|
217
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { CreationsConfig } from "../../domain/value-objects/CreationsConfig";
|
|
2
|
+
import { FilterCategory } from "../components/FilterBottomSheet";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Transforms the creations configuration into filter categories for the UI.
|
|
6
|
+
*
|
|
7
|
+
* @param config The creations configuration object
|
|
8
|
+
* @param t Translation function
|
|
9
|
+
* @returns Array of FilterCategory
|
|
10
|
+
*/
|
|
11
|
+
export const getFilterCategoriesFromConfig = (
|
|
12
|
+
config: CreationsConfig,
|
|
13
|
+
t: (key: string) => string
|
|
14
|
+
): FilterCategory[] => {
|
|
15
|
+
const categories: FilterCategory[] = [];
|
|
16
|
+
|
|
17
|
+
if (config.types.length > 0) {
|
|
18
|
+
categories.push({
|
|
19
|
+
id: 'type',
|
|
20
|
+
title: t(config.translations.filterTitle),
|
|
21
|
+
multiSelect: false,
|
|
22
|
+
options: config.types.map(type => ({
|
|
23
|
+
id: type.id,
|
|
24
|
+
label: t(type.labelKey),
|
|
25
|
+
icon: type.icon || 'image'
|
|
26
|
+
}))
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (config.filterCategories) {
|
|
31
|
+
categories.push(...config.filterCategories);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return categories;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Translates the creation types for display.
|
|
39
|
+
*
|
|
40
|
+
* @param config The creations configuration object
|
|
41
|
+
* @param t Translation function
|
|
42
|
+
* @returns Array of types with translated labels
|
|
43
|
+
*/
|
|
44
|
+
export const getTranslatedTypes = (
|
|
45
|
+
config: CreationsConfig,
|
|
46
|
+
t: (key: string) => string
|
|
47
|
+
) => {
|
|
48
|
+
return config.types.map(type => ({
|
|
49
|
+
...type,
|
|
50
|
+
labelKey: t(type.labelKey)
|
|
51
|
+
}));
|
|
52
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for external modules
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
declare module "@umituz/react-native-firestore" {
|
|
6
|
+
import type { Firestore } from "firebase/firestore";
|
|
7
|
+
|
|
8
|
+
export class BaseRepository {
|
|
9
|
+
protected getDb(): Firestore | null;
|
|
10
|
+
protected getDbOrThrow(): Firestore;
|
|
11
|
+
protected isDbInitialized(): boolean;
|
|
12
|
+
protected isQuotaError(error: unknown): boolean;
|
|
13
|
+
protected handleQuotaError(error: unknown): never;
|
|
14
|
+
protected executeWithQuotaHandling<T>(
|
|
15
|
+
operation: () => Promise<T>,
|
|
16
|
+
): Promise<T>;
|
|
17
|
+
protected trackRead(
|
|
18
|
+
collection: string,
|
|
19
|
+
count: number,
|
|
20
|
+
cached: boolean,
|
|
21
|
+
): void;
|
|
22
|
+
protected trackWrite(
|
|
23
|
+
collection: string,
|
|
24
|
+
docId: string,
|
|
25
|
+
count: number,
|
|
26
|
+
): void;
|
|
27
|
+
protected trackDelete(
|
|
28
|
+
collection: string,
|
|
29
|
+
docId: string,
|
|
30
|
+
count: number,
|
|
31
|
+
): void;
|
|
32
|
+
destroy(): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getFirestore(): Firestore | null;
|
|
36
|
+
export function initializeFirestore(app: unknown): void;
|
|
37
|
+
export function isFirestoreInitialized(): boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
declare module "@umituz/react-native-sentry";
|
|
41
|
+
declare module "expo-apple-authentication";
|
|
42
|
+
declare module "@umituz/react-native-filesystem";
|
|
@@ -128,8 +128,8 @@ export const ComparisonSlider: React.FC<ComparisonSliderProps> = memo(
|
|
|
128
128
|
{beforeLabel && (
|
|
129
129
|
<View style={[styles.label, styles.labelLeft, themedStyles.labelLeft]}>
|
|
130
130
|
<AtomicText
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
|
|
132
|
+
|
|
133
133
|
>
|
|
134
134
|
{beforeLabel}
|
|
135
135
|
</AtomicText>
|
|
@@ -139,8 +139,8 @@ export const ComparisonSlider: React.FC<ComparisonSliderProps> = memo(
|
|
|
139
139
|
{afterLabel && (
|
|
140
140
|
<View style={[styles.label, styles.labelRight, themedStyles.labelRight]}>
|
|
141
141
|
<AtomicText
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
|
|
143
|
+
|
|
144
144
|
>
|
|
145
145
|
{afterLabel}
|
|
146
146
|
</AtomicText>
|
|
@@ -45,7 +45,7 @@ export const ImagePicker: React.FC<ImagePickerProps> = memo(
|
|
|
45
45
|
<AtomicIcon
|
|
46
46
|
name="image-plus"
|
|
47
47
|
size="md"
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
/>
|
|
50
50
|
</View>
|
|
51
51
|
</View>
|
|
@@ -66,11 +66,11 @@ export const ImagePicker: React.FC<ImagePickerProps> = memo(
|
|
|
66
66
|
<AtomicIcon
|
|
67
67
|
name="upload"
|
|
68
68
|
size="lg"
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
/>
|
|
71
71
|
</View>
|
|
72
72
|
<AtomicText
|
|
73
|
-
|
|
73
|
+
|
|
74
74
|
style={[
|
|
75
75
|
styles.placeholderText,
|
|
76
76
|
{ color: tokens.colors.primary },
|
|
@@ -35,7 +35,7 @@ export const ProcessingModal: React.FC<ProcessingModalProps> = memo(
|
|
|
35
35
|
/>
|
|
36
36
|
{title && (
|
|
37
37
|
<AtomicText
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
style={[
|
|
40
40
|
styles.title,
|
|
41
41
|
{ color: tokens.colors.textPrimary },
|
|
@@ -63,7 +63,7 @@ export const ProcessingModal: React.FC<ProcessingModalProps> = memo(
|
|
|
63
63
|
/>
|
|
64
64
|
</View>
|
|
65
65
|
<AtomicText
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
style={{ color: tokens.colors.textSecondary }}
|
|
68
68
|
>
|
|
69
69
|
{Math.round(progress)}%
|
|
@@ -28,7 +28,7 @@ export const PromptInput: React.FC<PromptInputProps> = memo(
|
|
|
28
28
|
<View style={styles.container}>
|
|
29
29
|
{label && (
|
|
30
30
|
<AtomicText
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
style={[
|
|
33
33
|
styles.label,
|
|
34
34
|
{
|
|
@@ -47,7 +47,7 @@ export const PromptInput: React.FC<PromptInputProps> = memo(
|
|
|
47
47
|
placeholder={placeholder}
|
|
48
48
|
placeholderTextColor={tokens.colors.textTertiary}
|
|
49
49
|
multiline
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
editable={!isProcessing}
|
|
52
52
|
style={[
|
|
53
53
|
styles.input,
|
|
@@ -63,7 +63,7 @@ export const PromptInput: React.FC<PromptInputProps> = memo(
|
|
|
63
63
|
<>
|
|
64
64
|
{samplePromptsLabel && (
|
|
65
65
|
<AtomicText
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
style={[
|
|
68
68
|
styles.sampleLabel,
|
|
69
69
|
{
|
|
@@ -92,9 +92,9 @@ export const PromptInput: React.FC<PromptInputProps> = memo(
|
|
|
92
92
|
disabled={isProcessing}
|
|
93
93
|
>
|
|
94
94
|
<AtomicText
|
|
95
|
-
|
|
95
|
+
|
|
96
96
|
style={{ color: tokens.colors.textSecondary }}
|
|
97
|
-
|
|
97
|
+
|
|
98
98
|
>
|
|
99
99
|
{prompt.text}
|
|
100
100
|
</AtomicText>
|