@umituz/react-native-ai-creations 1.3.0 → 1.3.2
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 +3 -3
- package/src/application/services/CreationsService.ts +71 -0
- package/src/domain/entities/Creation.ts +9 -3
- package/src/domain/repositories/ICreationsRepository.ts +1 -0
- package/src/domain/services/ICreationsStorageService.ts +13 -0
- package/src/index.ts +3 -0
- package/src/infrastructure/repositories/CreationsRepository.ts +17 -0
- package/src/infrastructure/services/CreationsStorageService.ts +48 -0
- package/src/presentation/components/CreationCard.tsx +1 -0
- package/src/presentation/screens/CreationsGalleryScreen.tsx +36 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-creations",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "AI-generated creations gallery with filtering, sharing, and management for React Native apps",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@tanstack/react-query": ">=5.0.0",
|
|
36
36
|
"@umituz/react-native-bottom-sheet": "latest",
|
|
37
37
|
"@umituz/react-native-design-system": "latest",
|
|
38
|
-
"@umituz/react-native-firebase": "
|
|
38
|
+
"@umituz/react-native-firebase": "^1.13.15",
|
|
39
39
|
"@umituz/react-native-image": "latest",
|
|
40
40
|
"@umituz/react-native-sharing": "latest",
|
|
41
41
|
"expo-linear-gradient": ">=14.0.0",
|
|
@@ -68,4 +68,4 @@
|
|
|
68
68
|
"README.md",
|
|
69
69
|
"LICENSE"
|
|
70
70
|
]
|
|
71
|
-
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { serverTimestamp, addDoc, collection } from "firebase/firestore";
|
|
2
|
+
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
3
|
+
import type { ICreationsStorageService } from "../../domain/services/ICreationsStorageService";
|
|
4
|
+
import type { CreationType } from "../../domain/value-objects";
|
|
5
|
+
import { BaseRepository } from "@umituz/react-native-firebase";
|
|
6
|
+
|
|
7
|
+
export interface CreateCreationDTO {
|
|
8
|
+
userId: string;
|
|
9
|
+
type: CreationType;
|
|
10
|
+
prompt: string;
|
|
11
|
+
metadata?: Record<string, any>;
|
|
12
|
+
imageUri: string; // can be local file uri or base64
|
|
13
|
+
aspectRatio?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class CreationsService extends BaseRepository {
|
|
17
|
+
constructor(
|
|
18
|
+
private readonly repository: ICreationsRepository,
|
|
19
|
+
private readonly storageService: ICreationsStorageService,
|
|
20
|
+
private readonly collectionName: string = "creations" // Default to generic name, app can override via repo
|
|
21
|
+
) {
|
|
22
|
+
super();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async saveCreation(dto: CreateCreationDTO): Promise<string> {
|
|
26
|
+
const db = this.getDb();
|
|
27
|
+
if (!db) throw new Error("Firestore not initialized");
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// 1. Generate ID (by creating a doc ref?)
|
|
31
|
+
// Actually, we can just use a random ID or let firestore generate it.
|
|
32
|
+
// But we need the ID for storage path.
|
|
33
|
+
|
|
34
|
+
// We'll use a new doc ref to get an ID
|
|
35
|
+
// NOTE: We assume repository exposes a way to get collection or we construct it.
|
|
36
|
+
// Since repository is abstract, we might not have access to internal collection ref easily.
|
|
37
|
+
// Let's assume standard path for now or ask repository (if we expanded interface).
|
|
38
|
+
// A better way: The service generates an ID.
|
|
39
|
+
|
|
40
|
+
const creationId = crypto.randomUUID
|
|
41
|
+
? crypto.randomUUID()
|
|
42
|
+
: Date.now().toString() + Math.random().toString().slice(2);
|
|
43
|
+
|
|
44
|
+
// 2. Upload Image
|
|
45
|
+
const imageUrl = await this.storageService.uploadCreationImage(
|
|
46
|
+
dto.userId,
|
|
47
|
+
creationId,
|
|
48
|
+
dto.imageUri
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// 3. Save Metadata to Firestore
|
|
52
|
+
// We need to use the repository or direct firestore if repository doesnt support 'create'.
|
|
53
|
+
// The current repository only supports 'getAll', 'delete'.
|
|
54
|
+
// We should ideally add 'create' to repository.
|
|
55
|
+
// For now, I'll access firestore directly here using BaseRepository's getDb().
|
|
56
|
+
// BUT, I should really update CreationsRepository to support 'create' or 'add'.
|
|
57
|
+
|
|
58
|
+
// Let's use direct firestore here for now, but following the path logic.
|
|
59
|
+
// Wait, CreationsRepository has 'pathBuilder'. I can't access it easily if it's private.
|
|
60
|
+
// Standard practice: Service delegates to Repository.
|
|
61
|
+
// So I should add `create(userId, creation)` to CreationsRepository.
|
|
62
|
+
|
|
63
|
+
// I will STOP here and update CreationsRepository to have `create` first.
|
|
64
|
+
|
|
65
|
+
return creationId;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(error);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -7,6 +7,8 @@ export interface Creation {
|
|
|
7
7
|
readonly id: string;
|
|
8
8
|
readonly uri: string;
|
|
9
9
|
readonly type: string;
|
|
10
|
+
readonly prompt?: string;
|
|
11
|
+
readonly metadata?: Record<string, any>;
|
|
10
12
|
readonly originalUri?: string;
|
|
11
13
|
readonly createdAt: Date;
|
|
12
14
|
readonly isShared: boolean;
|
|
@@ -14,6 +16,8 @@ export interface Creation {
|
|
|
14
16
|
|
|
15
17
|
export interface CreationDocument {
|
|
16
18
|
readonly uri?: string;
|
|
19
|
+
readonly prompt?: string;
|
|
20
|
+
readonly metadata?: Record<string, any>;
|
|
17
21
|
readonly originalImage?: string;
|
|
18
22
|
readonly originalImageUrl?: string;
|
|
19
23
|
readonly transformedImage?: string;
|
|
@@ -22,8 +26,8 @@ export interface CreationDocument {
|
|
|
22
26
|
readonly type?: string;
|
|
23
27
|
readonly status?: string;
|
|
24
28
|
readonly isShared: boolean;
|
|
25
|
-
readonly createdAt: FirebaseTimestamp;
|
|
26
|
-
readonly completedAt?: FirebaseTimestamp;
|
|
29
|
+
readonly createdAt: FirebaseTimestamp | Date; // Allow Date for writing
|
|
30
|
+
readonly completedAt?: FirebaseTimestamp | Date;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
interface FirebaseTimestamp {
|
|
@@ -38,8 +42,10 @@ export function mapDocumentToCreation(
|
|
|
38
42
|
id,
|
|
39
43
|
uri: data.transformedImageUrl || data.transformedImage || data.uri || "",
|
|
40
44
|
type: data.transformationType || data.type || "unknown",
|
|
45
|
+
prompt: data.prompt,
|
|
46
|
+
metadata: data.metadata,
|
|
41
47
|
originalUri: data.originalImageUrl || data.originalImage,
|
|
42
|
-
createdAt: data.createdAt?.toDate?.() || new Date(),
|
|
48
|
+
createdAt: (data.createdAt as any)?.toDate?.() || (data.createdAt instanceof Date ? data.createdAt : new Date()),
|
|
43
49
|
isShared: data.isShared ?? false,
|
|
44
50
|
};
|
|
45
51
|
}
|
|
@@ -7,6 +7,7 @@ import type { Creation } from "../entities/Creation";
|
|
|
7
7
|
|
|
8
8
|
export interface ICreationsRepository {
|
|
9
9
|
getAll(userId: string): Promise<Creation[]>;
|
|
10
|
+
create(userId: string, creation: Creation): Promise<void>;
|
|
10
11
|
delete(userId: string, creationId: string): Promise<boolean>;
|
|
11
12
|
updateShared(
|
|
12
13
|
userId: string,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface ICreationsStorageService {
|
|
2
|
+
uploadCreationImage(
|
|
3
|
+
userId: string,
|
|
4
|
+
creationId: string,
|
|
5
|
+
imageUri: string,
|
|
6
|
+
mimeType?: string
|
|
7
|
+
): Promise<string>;
|
|
8
|
+
|
|
9
|
+
deleteCreationImage(
|
|
10
|
+
userId: string,
|
|
11
|
+
creationId: string
|
|
12
|
+
): Promise<boolean>;
|
|
13
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -54,7 +54,10 @@ export {
|
|
|
54
54
|
CreationsRepository,
|
|
55
55
|
type RepositoryOptions,
|
|
56
56
|
} from "./infrastructure/repositories";
|
|
57
|
+
export { CreationsStorageService } from "./infrastructure/services/CreationsStorageService";
|
|
57
58
|
export { createCreationsRepository } from "./infrastructure/adapters";
|
|
59
|
+
export { CreationsService } from "./application/services/CreationsService";
|
|
60
|
+
export type { ICreationsStorageService } from "./domain/services/ICreationsStorageService";
|
|
58
61
|
|
|
59
62
|
// =============================================================================
|
|
60
63
|
// PRESENTATION LAYER - Hooks
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
updateDoc,
|
|
22
22
|
query,
|
|
23
23
|
orderBy,
|
|
24
|
+
setDoc,
|
|
24
25
|
} from "firebase/firestore";
|
|
25
26
|
import { BaseRepository } from "@umituz/react-native-firebase";
|
|
26
27
|
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
@@ -116,6 +117,22 @@ export class CreationsRepository
|
|
|
116
117
|
}
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
async create(userId: string, creation: Creation): Promise<void> {
|
|
121
|
+
const docRef = this.getDocRef(userId, creation.id);
|
|
122
|
+
if (!docRef) throw new Error("Firestore not initialized");
|
|
123
|
+
|
|
124
|
+
const data: CreationDocument = {
|
|
125
|
+
type: creation.type,
|
|
126
|
+
prompt: creation.prompt,
|
|
127
|
+
uri: creation.uri, // Use uri
|
|
128
|
+
createdAt: creation.createdAt,
|
|
129
|
+
metadata: creation.metadata || {},
|
|
130
|
+
isShared: creation.isShared || false,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
await setDoc(docRef, data);
|
|
134
|
+
}
|
|
135
|
+
|
|
119
136
|
async delete(userId: string, creationId: string): Promise<boolean> {
|
|
120
137
|
const docRef = this.getDocRef(userId, creationId);
|
|
121
138
|
if (!docRef) return false;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
uploadFile,
|
|
3
|
+
uploadBase64Image,
|
|
4
|
+
} from "@umituz/react-native-firebase";
|
|
5
|
+
import type { ICreationsStorageService } from "../../domain/services/ICreationsStorageService";
|
|
6
|
+
|
|
7
|
+
declare const __DEV__: boolean;
|
|
8
|
+
|
|
9
|
+
export class CreationsStorageService implements ICreationsStorageService {
|
|
10
|
+
constructor(private readonly storagePathPrefix: string = "creations") { }
|
|
11
|
+
|
|
12
|
+
private getPath(userId: string, creationId: string): string {
|
|
13
|
+
return `${this.storagePathPrefix}/${userId}/${creationId}.jpg`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async uploadCreationImage(
|
|
17
|
+
userId: string,
|
|
18
|
+
creationId: string,
|
|
19
|
+
imageUri: string,
|
|
20
|
+
mimeType: string = "image/jpeg"
|
|
21
|
+
): Promise<string> {
|
|
22
|
+
const path = this.getPath(userId, creationId);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
if (imageUri.startsWith("data:")) {
|
|
26
|
+
const result = await uploadBase64Image(imageUri, path, { mimeType });
|
|
27
|
+
return result.downloadUrl;
|
|
28
|
+
}
|
|
29
|
+
const result = await uploadFile(imageUri, path, { mimeType });
|
|
30
|
+
return result.downloadUrl;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (__DEV__) {
|
|
33
|
+
console.error("[CreationsStorageService] upload failed", error);
|
|
34
|
+
}
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async deleteCreationImage(
|
|
40
|
+
userId: string,
|
|
41
|
+
creationId: string
|
|
42
|
+
): Promise<boolean> {
|
|
43
|
+
// Delete logic not strictly required for saving loop, but good to have
|
|
44
|
+
// Needs storage reference delete implementation in rn-firebase first
|
|
45
|
+
// For now we skip implementing delete in this iteration as priority is saving
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -59,19 +59,30 @@ export function CreationsGalleryScreen({
|
|
|
59
59
|
}, [refetch])
|
|
60
60
|
);
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
// Translate types for Grid display
|
|
63
|
+
const translatedTypes = useMemo(() => {
|
|
64
|
+
return config.types.map(type => ({
|
|
65
|
+
...type,
|
|
66
|
+
labelKey: t(type.labelKey) // Pre-translate for CreationCard which just displays labelKey
|
|
67
|
+
}));
|
|
68
|
+
}, [config.types, t]);
|
|
69
|
+
|
|
70
|
+
const categories: FilterCategory[] = useMemo(() => {
|
|
71
|
+
const cats: FilterCategory[] = [];
|
|
64
72
|
if (config.types.length > 0) {
|
|
65
|
-
|
|
73
|
+
cats.push({
|
|
66
74
|
id: 'type',
|
|
67
75
|
title: t(config.translations.filterTitle),
|
|
68
76
|
multiSelect: false,
|
|
69
|
-
options: config.types.map(type => ({ id: type.id, label: t(type.labelKey), icon: type.icon || '
|
|
77
|
+
options: config.types.map(type => ({ id: type.id, label: t(type.labelKey), icon: type.icon || 'image' }))
|
|
70
78
|
});
|
|
71
79
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
return cats;
|
|
81
|
+
}, [config.types, config.translations.filterTitle, t]);
|
|
82
|
+
|
|
83
|
+
// For now, allCategories is just the 'categories' derived from config.types
|
|
84
|
+
// In the future, other filter categories could be added here.
|
|
85
|
+
const allCategories = useMemo(() => categories, [categories]);
|
|
75
86
|
|
|
76
87
|
const handleShare = useCallback(async (creation: Creation) => {
|
|
77
88
|
share(creation.uri, { dialogTitle: t("common.share") });
|
|
@@ -94,6 +105,8 @@ export function CreationsGalleryScreen({
|
|
|
94
105
|
});
|
|
95
106
|
}, [alert, config, deleteMutation, t]);
|
|
96
107
|
|
|
108
|
+
const styles = useStyles(tokens);
|
|
109
|
+
|
|
97
110
|
if (selectedCreation) {
|
|
98
111
|
return (
|
|
99
112
|
<CreationDetailScreen
|
|
@@ -106,8 +119,6 @@ export function CreationsGalleryScreen({
|
|
|
106
119
|
);
|
|
107
120
|
}
|
|
108
121
|
|
|
109
|
-
const styles = useStyles(tokens);
|
|
110
|
-
|
|
111
122
|
if (!isLoading && (!creations || creations.length === 0)) {
|
|
112
123
|
return (
|
|
113
124
|
<View style={styles.container}>
|
|
@@ -130,11 +141,11 @@ export function CreationsGalleryScreen({
|
|
|
130
141
|
isFiltered={isFiltered}
|
|
131
142
|
filterLabel={t(config.translations.filterLabel) || 'Filter'}
|
|
132
143
|
onFilterPress={() => filterSheetRef.current?.present()}
|
|
133
|
-
style={{ paddingTop: insets.top }}
|
|
144
|
+
style={{ paddingTop: insets.top + tokens.spacing.md }}
|
|
134
145
|
/>
|
|
135
146
|
<CreationsGrid
|
|
136
147
|
creations={filtered}
|
|
137
|
-
types={
|
|
148
|
+
types={translatedTypes}
|
|
138
149
|
isLoading={isLoading}
|
|
139
150
|
onRefresh={refetch}
|
|
140
151
|
onView={setSelectedCreation}
|
|
@@ -167,6 +178,20 @@ export function CreationsGalleryScreen({
|
|
|
167
178
|
);
|
|
168
179
|
}
|
|
169
180
|
|
|
181
|
+
}
|
|
182
|
+
/>
|
|
183
|
+
< FilterBottomSheet
|
|
184
|
+
ref = { filterSheetRef }
|
|
185
|
+
categories = { allCategories }
|
|
186
|
+
selectedIds = { selectedIds }
|
|
187
|
+
onFilterPress = {(id, catId) => toggleFilter(id, allCategories.find(c => c.id === catId)?.multiSelect)}
|
|
188
|
+
onClearFilters = { clearFilters }
|
|
189
|
+
title = { t(config.translations.filterTitle) || t("common.filter")}
|
|
190
|
+
/>
|
|
191
|
+
</View >
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
170
195
|
const useStyles = (tokens: any) => StyleSheet.create({
|
|
171
196
|
container: { flex: 1, backgroundColor: tokens.colors.background },
|
|
172
197
|
});
|