@umituz/react-native-ai-generation-content 1.17.271 → 1.17.273
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/domains/creations/presentation/components/CreationRating.tsx +1 -1
- package/src/domains/creations/presentation/components/index.ts +0 -6
- package/src/domains/creations/presentation/hooks/useCreationRating.ts +33 -8
- package/src/domains/creations/presentation/hooks/useCreations.ts +2 -2
- package/src/domains/creations/presentation/hooks/useDeleteCreation.ts +25 -4
- package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +1 -1
- package/src/features/scenarios/presentation/components/ScenarioGrid.tsx +4 -6
- package/src/features/scenarios/presentation/components/StyleSelector.tsx +1 -1
- package/src/features/scenarios/presentation/screens/ScenarioSelectorScreen.tsx +1 -1
- package/src/domains/creations/presentation/components/CreationDetail/DetailActions.tsx +0 -95
- package/src/domains/creations/presentation/components/CreationDetail/DetailHeader.tsx +0 -35
- package/src/domains/creations/presentation/components/CreationDetail/DetailImage.tsx +0 -53
- package/src/domains/creations/presentation/components/CreationDetail/DetailInfo.tsx +0 -51
- package/src/domains/creations/presentation/components/CreationDetail/DetailStory.tsx +0 -64
- package/src/domains/creations/presentation/components/CreationDetail/DetailVideo.tsx +0 -48
- package/src/domains/creations/presentation/components/CreationDetail/index.ts +0 -5
- package/src/domains/creations/presentation/screens/CreationDetailScreen.tsx +0 -111
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.273",
|
|
4
4
|
"description": "Provider-agnostic AI generation orchestration for React Native with result preview components",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -42,7 +42,7 @@ export const CreationRating: React.FC<CreationRatingProps> = ({
|
|
|
42
42
|
})}
|
|
43
43
|
</View>
|
|
44
44
|
{!readonly && rating > 0 && (
|
|
45
|
-
<AtomicText
|
|
45
|
+
<AtomicText type="bodySmall" color="textSecondary" style={styles.valueText}>
|
|
46
46
|
{rating} / {max}
|
|
47
47
|
</AtomicText>
|
|
48
48
|
)}
|
|
@@ -32,9 +32,3 @@ export { GalleryEmptyStates } from "./GalleryEmptyStates";
|
|
|
32
32
|
export { CreationsHomeCard } from "./CreationsHomeCard";
|
|
33
33
|
export { CreationRating } from "./CreationRating";
|
|
34
34
|
export { CreationsGrid } from "./CreationsGrid";
|
|
35
|
-
|
|
36
|
-
// Detail Components
|
|
37
|
-
export { DetailHeader } from "./CreationDetail/DetailHeader";
|
|
38
|
-
export { DetailImage } from "./CreationDetail/DetailImage";
|
|
39
|
-
export { DetailStory } from "./CreationDetail/DetailStory";
|
|
40
|
-
export { DetailActions } from "./CreationDetail/DetailActions";
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Handles rating of creations with optimistic update
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { useMutation, useQueryClient } from "@umituz/react-native-design-system";
|
|
7
7
|
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
8
8
|
import type { Creation } from "../../domain/entities/Creation";
|
|
9
9
|
|
|
@@ -12,19 +12,44 @@ interface UseCreationRatingProps {
|
|
|
12
12
|
readonly repository: ICreationsRepository;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
interface RatingVariables {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly rating: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
export function useCreationRating({
|
|
16
21
|
userId,
|
|
17
22
|
repository,
|
|
18
23
|
}: UseCreationRatingProps) {
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
const queryClient = useQueryClient();
|
|
25
|
+
const queryKey = ["creations", userId ?? ""];
|
|
26
|
+
|
|
27
|
+
return useMutation({
|
|
28
|
+
mutationFn: async ({ id, rating }: RatingVariables) => {
|
|
21
29
|
if (!userId) return false;
|
|
22
30
|
return repository.rate(userId, id, rating);
|
|
23
31
|
},
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
)
|
|
32
|
+
onMutate: async ({ id, rating }: RatingVariables) => {
|
|
33
|
+
await queryClient.cancelQueries({ queryKey });
|
|
34
|
+
const previousData = queryClient.getQueryData<Creation[]>(queryKey);
|
|
35
|
+
|
|
36
|
+
if (previousData) {
|
|
37
|
+
queryClient.setQueryData<Creation[]>(queryKey, (old) =>
|
|
38
|
+
old?.map((c) =>
|
|
39
|
+
c.id === id ? { ...c, rating, ratedAt: new Date() } : c
|
|
40
|
+
) ?? []
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { previousData };
|
|
45
|
+
},
|
|
46
|
+
onError: (_error, _variables, context) => {
|
|
47
|
+
if (context?.previousData) {
|
|
48
|
+
queryClient.setQueryData(queryKey, context.previousData);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
onSettled: () => {
|
|
52
|
+
void queryClient.invalidateQueries({ queryKey });
|
|
53
|
+
},
|
|
29
54
|
});
|
|
30
55
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Fetches user's creations from repository
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { useQuery } from "@umituz/react-native-design-system";
|
|
7
7
|
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
8
8
|
import type { Creation } from "../../domain/entities/Creation";
|
|
9
9
|
|
|
@@ -23,7 +23,7 @@ export function useCreations({
|
|
|
23
23
|
repository,
|
|
24
24
|
enabled = true,
|
|
25
25
|
}: UseCreationsProps) {
|
|
26
|
-
return
|
|
26
|
+
return useQuery<Creation[]>({
|
|
27
27
|
queryKey: ["creations", userId ?? ""],
|
|
28
28
|
queryFn: async () => {
|
|
29
29
|
if (!userId) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Handles deletion of user creations with optimistic update
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { useMutation, useQueryClient } from "@umituz/react-native-design-system";
|
|
7
7
|
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
8
8
|
import type { Creation } from "../../domain/entities/Creation";
|
|
9
9
|
|
|
@@ -16,12 +16,33 @@ export function useDeleteCreation({
|
|
|
16
16
|
userId,
|
|
17
17
|
repository,
|
|
18
18
|
}: UseDeleteCreationProps) {
|
|
19
|
-
|
|
19
|
+
const queryClient = useQueryClient();
|
|
20
|
+
const queryKey = ["creations", userId ?? ""];
|
|
21
|
+
|
|
22
|
+
return useMutation({
|
|
20
23
|
mutationFn: async (creationId: string) => {
|
|
21
24
|
if (!userId) return false;
|
|
22
25
|
return repository.delete(userId, creationId);
|
|
23
26
|
},
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
onMutate: async (creationId: string) => {
|
|
28
|
+
await queryClient.cancelQueries({ queryKey });
|
|
29
|
+
const previousData = queryClient.getQueryData<Creation[]>(queryKey);
|
|
30
|
+
|
|
31
|
+
if (previousData) {
|
|
32
|
+
queryClient.setQueryData<Creation[]>(queryKey, (old) =>
|
|
33
|
+
old?.filter((c) => c.id !== creationId) ?? []
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { previousData };
|
|
38
|
+
},
|
|
39
|
+
onError: (_error, _variables, context) => {
|
|
40
|
+
if (context?.previousData) {
|
|
41
|
+
queryClient.setQueryData(queryKey, context.previousData);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
onSettled: () => {
|
|
45
|
+
void queryClient.invalidateQueries({ queryKey });
|
|
46
|
+
},
|
|
26
47
|
});
|
|
27
48
|
}
|
|
@@ -146,7 +146,7 @@ export function CreationsGalleryScreen({
|
|
|
146
146
|
), [isLoading, creations, filters.isFiltered, tokens, t, config, emptyActionLabel, onEmptyAction, filters.clearAllFilters]);
|
|
147
147
|
|
|
148
148
|
return (
|
|
149
|
-
<ScreenLayout>
|
|
149
|
+
<ScreenLayout scrollable={false}>
|
|
150
150
|
<FlatList
|
|
151
151
|
data={filters.filtered}
|
|
152
152
|
renderItem={renderItem}
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
import React, { useMemo, useCallback, useState } from "react";
|
|
7
7
|
import {
|
|
8
|
-
View
|
|
9
|
-
FlatList
|
|
8
|
+
View,
|
|
9
|
+
FlatList,
|
|
10
10
|
StyleSheet,
|
|
11
|
+
type ListRenderItemInfo,
|
|
11
12
|
} from "react-native";
|
|
12
13
|
import {
|
|
13
14
|
useAppDesignTokens,
|
|
@@ -22,9 +23,6 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
|
22
23
|
import type { ScenarioData, ScenarioCategory } from "../../domain/types";
|
|
23
24
|
import { SCENARIO_DEFAULTS } from "../../domain/types";
|
|
24
25
|
|
|
25
|
-
const View = RNView as any;
|
|
26
|
-
const FlatList = RNFlatList as any;
|
|
27
|
-
|
|
28
26
|
export interface ScenarioGridProps {
|
|
29
27
|
readonly scenarios: readonly ScenarioData[];
|
|
30
28
|
readonly selectedScenarioId: string | null;
|
|
@@ -148,7 +146,7 @@ export const ScenarioGrid: React.FC<ScenarioGridProps> = ({
|
|
|
148
146
|
);
|
|
149
147
|
|
|
150
148
|
const renderItem = useCallback(
|
|
151
|
-
({ item }:
|
|
149
|
+
({ item }: ListRenderItemInfo<ScenarioData>) => {
|
|
152
150
|
const title = t(`scenario.${item.id}.title`);
|
|
153
151
|
const description = t(`scenario.${item.id}.description`);
|
|
154
152
|
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { View, StyleSheet, TouchableOpacity } from 'react-native';
|
|
4
|
-
import { AtomicText, AtomicIcon, useAppDesignTokens, type DesignTokens } from "@umituz/react-native-design-system";
|
|
5
|
-
|
|
6
|
-
interface DetailActionsProps {
|
|
7
|
-
readonly onShare: () => void;
|
|
8
|
-
readonly onDelete: () => void;
|
|
9
|
-
readonly onViewResult?: () => void;
|
|
10
|
-
readonly shareLabel: string;
|
|
11
|
-
readonly deleteLabel: string;
|
|
12
|
-
readonly viewResultLabel?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const DetailActions: React.FC<DetailActionsProps> = ({
|
|
16
|
-
onShare,
|
|
17
|
-
onDelete,
|
|
18
|
-
onViewResult,
|
|
19
|
-
shareLabel,
|
|
20
|
-
deleteLabel,
|
|
21
|
-
viewResultLabel
|
|
22
|
-
}) => {
|
|
23
|
-
const tokens = useAppDesignTokens();
|
|
24
|
-
const styles = useStyles(tokens);
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<View style={styles.container}>
|
|
28
|
-
{onViewResult && viewResultLabel && (
|
|
29
|
-
<TouchableOpacity
|
|
30
|
-
style={[styles.button, styles.viewResultButton]}
|
|
31
|
-
onPress={onViewResult}
|
|
32
|
-
activeOpacity={0.7}
|
|
33
|
-
>
|
|
34
|
-
<AtomicIcon name="eye-outline" size="sm" color="onPrimary" />
|
|
35
|
-
<AtomicText style={styles.buttonText}>{viewResultLabel}</AtomicText>
|
|
36
|
-
</TouchableOpacity>
|
|
37
|
-
)}
|
|
38
|
-
|
|
39
|
-
<TouchableOpacity
|
|
40
|
-
style={[styles.button, styles.shareButton]}
|
|
41
|
-
onPress={onShare}
|
|
42
|
-
activeOpacity={0.7}
|
|
43
|
-
>
|
|
44
|
-
<AtomicIcon name="share-social-outline" size="sm" color="onPrimary" />
|
|
45
|
-
<AtomicText style={styles.buttonText}>{shareLabel}</AtomicText>
|
|
46
|
-
</TouchableOpacity>
|
|
47
|
-
|
|
48
|
-
<TouchableOpacity
|
|
49
|
-
style={[styles.button, styles.deleteButton]}
|
|
50
|
-
onPress={onDelete}
|
|
51
|
-
activeOpacity={0.7}
|
|
52
|
-
>
|
|
53
|
-
<AtomicIcon name="trash-outline" size="sm" color="error" />
|
|
54
|
-
<AtomicText style={[styles.buttonText, { color: tokens.colors.error }]}>
|
|
55
|
-
{deleteLabel}
|
|
56
|
-
</AtomicText>
|
|
57
|
-
</TouchableOpacity>
|
|
58
|
-
</View>
|
|
59
|
-
);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const useStyles = (tokens: DesignTokens) => StyleSheet.create({
|
|
63
|
-
container: {
|
|
64
|
-
flexDirection: 'row',
|
|
65
|
-
justifyContent: 'center',
|
|
66
|
-
flexWrap: 'wrap',
|
|
67
|
-
gap: tokens.spacing.md,
|
|
68
|
-
paddingHorizontal: tokens.spacing.lg,
|
|
69
|
-
marginBottom: tokens.spacing.xxl,
|
|
70
|
-
},
|
|
71
|
-
button: {
|
|
72
|
-
flexDirection: 'row',
|
|
73
|
-
alignItems: 'center',
|
|
74
|
-
justifyContent: 'center',
|
|
75
|
-
paddingVertical: 12,
|
|
76
|
-
paddingHorizontal: 20,
|
|
77
|
-
borderRadius: 16,
|
|
78
|
-
gap: 8,
|
|
79
|
-
minWidth: 110,
|
|
80
|
-
},
|
|
81
|
-
viewResultButton: {
|
|
82
|
-
backgroundColor: tokens.colors.primary,
|
|
83
|
-
},
|
|
84
|
-
shareButton: {
|
|
85
|
-
backgroundColor: tokens.colors.primary,
|
|
86
|
-
},
|
|
87
|
-
deleteButton: {
|
|
88
|
-
backgroundColor: tokens.colors.error + '10',
|
|
89
|
-
},
|
|
90
|
-
buttonText: {
|
|
91
|
-
fontWeight: '700',
|
|
92
|
-
color: tokens.colors.textInverse,
|
|
93
|
-
fontSize: 15,
|
|
94
|
-
},
|
|
95
|
-
});
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { View, StyleSheet, TouchableOpacity } from 'react-native';
|
|
3
|
-
import { AtomicIcon, useAppDesignTokens, type DesignTokens } from "@umituz/react-native-design-system";
|
|
4
|
-
|
|
5
|
-
interface DetailHeaderProps {
|
|
6
|
-
readonly onClose: () => void;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const DetailHeader: React.FC<DetailHeaderProps> = ({ onClose }) => {
|
|
10
|
-
const tokens = useAppDesignTokens();
|
|
11
|
-
const styles = useStyles(tokens);
|
|
12
|
-
|
|
13
|
-
return (
|
|
14
|
-
<View style={styles.headerContainer}>
|
|
15
|
-
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
|
|
16
|
-
<AtomicIcon name="arrow-back" size="md" color="onSurface" />
|
|
17
|
-
</TouchableOpacity>
|
|
18
|
-
</View>
|
|
19
|
-
);
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const useStyles = (tokens: DesignTokens) => StyleSheet.create({
|
|
23
|
-
headerContainer: {
|
|
24
|
-
paddingVertical: tokens.spacing.xs,
|
|
25
|
-
paddingHorizontal: tokens.spacing.sm,
|
|
26
|
-
backgroundColor: tokens.colors.background,
|
|
27
|
-
},
|
|
28
|
-
closeButton: {
|
|
29
|
-
padding: tokens.spacing.xs,
|
|
30
|
-
width: 40,
|
|
31
|
-
height: 40,
|
|
32
|
-
alignItems: 'center',
|
|
33
|
-
justifyContent: 'center',
|
|
34
|
-
},
|
|
35
|
-
});
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { View, StyleSheet, useWindowDimensions, TouchableOpacity } from 'react-native';
|
|
3
|
-
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
4
|
-
import { Image } from 'expo-image';
|
|
5
|
-
|
|
6
|
-
interface DetailImageProps {
|
|
7
|
-
readonly uri: string;
|
|
8
|
-
readonly onPress?: () => void;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const HORIZONTAL_PADDING = 16;
|
|
12
|
-
const ASPECT_RATIO = 16 / 9;
|
|
13
|
-
|
|
14
|
-
export const DetailImage: React.FC<DetailImageProps> = ({ uri, onPress }) => {
|
|
15
|
-
const tokens = useAppDesignTokens();
|
|
16
|
-
const { width } = useWindowDimensions();
|
|
17
|
-
const imageWidth = width - (HORIZONTAL_PADDING * 2);
|
|
18
|
-
const imageHeight = imageWidth / ASPECT_RATIO;
|
|
19
|
-
|
|
20
|
-
const content = (
|
|
21
|
-
<View style={[styles.frame, { width: imageWidth, height: imageHeight, backgroundColor: tokens.colors.surface }]}>
|
|
22
|
-
<Image source={{ uri }} style={styles.image} contentFit="cover" />
|
|
23
|
-
</View>
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<View style={styles.container}>
|
|
28
|
-
{onPress ? (
|
|
29
|
-
<TouchableOpacity activeOpacity={0.9} onPress={onPress}>
|
|
30
|
-
{content}
|
|
31
|
-
</TouchableOpacity>
|
|
32
|
-
) : (
|
|
33
|
-
content
|
|
34
|
-
)}
|
|
35
|
-
</View>
|
|
36
|
-
);
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const styles = StyleSheet.create({
|
|
40
|
-
container: {
|
|
41
|
-
paddingHorizontal: HORIZONTAL_PADDING,
|
|
42
|
-
marginTop: 8,
|
|
43
|
-
marginBottom: 12,
|
|
44
|
-
},
|
|
45
|
-
frame: {
|
|
46
|
-
borderRadius: 16,
|
|
47
|
-
overflow: 'hidden',
|
|
48
|
-
},
|
|
49
|
-
image: {
|
|
50
|
-
width: '100%',
|
|
51
|
-
height: '100%',
|
|
52
|
-
},
|
|
53
|
-
});
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { View, StyleSheet } from 'react-native';
|
|
3
|
-
import { AtomicText, AtomicIcon, useAppDesignTokens, type DesignTokens } from "@umituz/react-native-design-system";
|
|
4
|
-
|
|
5
|
-
interface DetailInfoProps {
|
|
6
|
-
readonly title: string;
|
|
7
|
-
readonly date: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const DetailInfo: React.FC<DetailInfoProps> = ({ title, date }) => {
|
|
11
|
-
const tokens = useAppDesignTokens();
|
|
12
|
-
const styles = useStyles(tokens);
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<View style={styles.container}>
|
|
16
|
-
<AtomicText style={styles.title}>{title}</AtomicText>
|
|
17
|
-
<View style={styles.dateBadge}>
|
|
18
|
-
<AtomicIcon name="calendar-outline" size="sm" color="primary" />
|
|
19
|
-
<AtomicText style={styles.dateText}>{date}</AtomicText>
|
|
20
|
-
</View>
|
|
21
|
-
</View>
|
|
22
|
-
);
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const useStyles = (tokens: DesignTokens) => StyleSheet.create({
|
|
26
|
-
container: {
|
|
27
|
-
paddingHorizontal: tokens.spacing.md,
|
|
28
|
-
paddingVertical: tokens.spacing.sm,
|
|
29
|
-
gap: tokens.spacing.xs,
|
|
30
|
-
},
|
|
31
|
-
title: {
|
|
32
|
-
fontSize: 18,
|
|
33
|
-
fontWeight: '700',
|
|
34
|
-
color: tokens.colors.textPrimary,
|
|
35
|
-
},
|
|
36
|
-
dateBadge: {
|
|
37
|
-
flexDirection: 'row',
|
|
38
|
-
alignItems: 'center',
|
|
39
|
-
gap: 6,
|
|
40
|
-
alignSelf: 'flex-start',
|
|
41
|
-
paddingHorizontal: 12,
|
|
42
|
-
paddingVertical: 6,
|
|
43
|
-
borderRadius: 12,
|
|
44
|
-
backgroundColor: tokens.colors.primary + '15',
|
|
45
|
-
},
|
|
46
|
-
dateText: {
|
|
47
|
-
fontSize: 13,
|
|
48
|
-
fontWeight: '600',
|
|
49
|
-
color: tokens.colors.primary,
|
|
50
|
-
},
|
|
51
|
-
});
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { View, StyleSheet } from 'react-native';
|
|
4
|
-
import { AtomicText, useAppDesignTokens, type DesignTokens } from "@umituz/react-native-design-system";
|
|
5
|
-
|
|
6
|
-
interface DetailStoryProps {
|
|
7
|
-
readonly story: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const DetailStory: React.FC<DetailStoryProps> = ({ story }) => {
|
|
11
|
-
const tokens = useAppDesignTokens();
|
|
12
|
-
const styles = useStyles(tokens);
|
|
13
|
-
|
|
14
|
-
if (!story) return null;
|
|
15
|
-
|
|
16
|
-
return (
|
|
17
|
-
<View style={styles.container}>
|
|
18
|
-
<View style={styles.storyContainer}>
|
|
19
|
-
<AtomicText style={styles.quoteMark}>"</AtomicText>
|
|
20
|
-
<AtomicText style={styles.text}>{story}</AtomicText>
|
|
21
|
-
<View style={styles.quoteEndRow}>
|
|
22
|
-
<AtomicText style={[styles.quoteMark, styles.quoteEnd]}>"</AtomicText>
|
|
23
|
-
</View>
|
|
24
|
-
</View>
|
|
25
|
-
</View>
|
|
26
|
-
);
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const useStyles = (tokens: DesignTokens) => StyleSheet.create({
|
|
30
|
-
container: {
|
|
31
|
-
paddingHorizontal: tokens.spacing.lg,
|
|
32
|
-
marginBottom: tokens.spacing.lg,
|
|
33
|
-
},
|
|
34
|
-
storyContainer: {
|
|
35
|
-
padding: tokens.spacing.lg,
|
|
36
|
-
borderRadius: 20,
|
|
37
|
-
borderWidth: 1,
|
|
38
|
-
borderColor: tokens.colors.border,
|
|
39
|
-
backgroundColor: tokens.colors.surface,
|
|
40
|
-
},
|
|
41
|
-
quoteMark: {
|
|
42
|
-
fontSize: 48,
|
|
43
|
-
lineHeight: 48,
|
|
44
|
-
color: tokens.colors.primary,
|
|
45
|
-
opacity: 0.4,
|
|
46
|
-
marginBottom: -16,
|
|
47
|
-
},
|
|
48
|
-
quoteEndRow: {
|
|
49
|
-
alignItems: 'flex-end',
|
|
50
|
-
marginTop: -16,
|
|
51
|
-
},
|
|
52
|
-
quoteEnd: {
|
|
53
|
-
marginBottom: 0,
|
|
54
|
-
},
|
|
55
|
-
text: {
|
|
56
|
-
fontSize: 16,
|
|
57
|
-
lineHeight: 26,
|
|
58
|
-
textAlign: 'center',
|
|
59
|
-
fontStyle: 'italic',
|
|
60
|
-
fontWeight: '500',
|
|
61
|
-
color: tokens.colors.textPrimary,
|
|
62
|
-
paddingBottom: 4,
|
|
63
|
-
},
|
|
64
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DetailVideo Component
|
|
3
|
-
* Video player with thumbnail and play controls for creation detail view
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import React, { useMemo } from "react";
|
|
7
|
-
import { View } from "react-native";
|
|
8
|
-
import { VideoView, useVideoPlayer } from "expo-video";
|
|
9
|
-
import { useResponsive } from "@umituz/react-native-design-system";
|
|
10
|
-
|
|
11
|
-
interface DetailVideoProps {
|
|
12
|
-
readonly videoUrl: string;
|
|
13
|
-
readonly _thumbnailUrl?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const DetailVideo: React.FC<DetailVideoProps> = ({
|
|
17
|
-
videoUrl,
|
|
18
|
-
_thumbnailUrl,
|
|
19
|
-
}) => {
|
|
20
|
-
const { width, horizontalPadding, spacingMultiplier } = useResponsive();
|
|
21
|
-
const videoWidth = width - (horizontalPadding * 2);
|
|
22
|
-
|
|
23
|
-
const player = useVideoPlayer(videoUrl, (player) => {
|
|
24
|
-
player.loop = true;
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const containerStyle = useMemo(() => ({
|
|
28
|
-
paddingHorizontal: horizontalPadding,
|
|
29
|
-
marginVertical: 16 * spacingMultiplier,
|
|
30
|
-
}), [horizontalPadding, spacingMultiplier]);
|
|
31
|
-
|
|
32
|
-
const videoStyle = useMemo(() => ({
|
|
33
|
-
width: videoWidth,
|
|
34
|
-
height: (videoWidth * 9) / 16, // 16:9 aspect ratio
|
|
35
|
-
}), [videoWidth]);
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<View style={containerStyle}>
|
|
39
|
-
<VideoView
|
|
40
|
-
style={videoStyle}
|
|
41
|
-
player={player}
|
|
42
|
-
allowsFullscreen
|
|
43
|
-
allowsPictureInPicture
|
|
44
|
-
nativeControls
|
|
45
|
-
/>
|
|
46
|
-
</View>
|
|
47
|
-
);
|
|
48
|
-
};
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import React, { useMemo } from 'react';
|
|
2
|
-
import { } from 'react-native';
|
|
3
|
-
import { useAppDesignTokens, ScreenLayout } from "@umituz/react-native-design-system";
|
|
4
|
-
import type { Creation } from '../../domain/entities/Creation';
|
|
5
|
-
import type { CreationsConfig } from '../../domain/value-objects/CreationsConfig';
|
|
6
|
-
import type { ICreationsRepository } from '../../domain/repositories/ICreationsRepository';
|
|
7
|
-
import { hasVideoContent, getPreviewUrl } from '../../domain/utils';
|
|
8
|
-
import { useCreationRating } from '../hooks/useCreationRating';
|
|
9
|
-
import { DetailHeader } from '../components/CreationDetail/DetailHeader';
|
|
10
|
-
import { DetailInfo } from '../components/CreationDetail/DetailInfo';
|
|
11
|
-
import { DetailImage } from '../components/CreationDetail/DetailImage';
|
|
12
|
-
import { DetailVideo } from '../components/CreationDetail/DetailVideo';
|
|
13
|
-
import { DetailStory } from '../components/CreationDetail/DetailStory';
|
|
14
|
-
import { DetailActions } from '../components/CreationDetail/DetailActions';
|
|
15
|
-
import { CreationRating } from '../components/CreationRating';
|
|
16
|
-
import { getLocalizedTitle } from '../utils/filterUtils';
|
|
17
|
-
|
|
18
|
-
/** Video creation types */
|
|
19
|
-
const VIDEO_TYPES = ['text-to-video', 'image-to-video'] as const;
|
|
20
|
-
|
|
21
|
-
interface CreationDetailScreenProps {
|
|
22
|
-
readonly userId: string | null;
|
|
23
|
-
readonly repository: ICreationsRepository;
|
|
24
|
-
readonly creation: Creation;
|
|
25
|
-
readonly config: CreationsConfig;
|
|
26
|
-
readonly onClose: () => void;
|
|
27
|
-
readonly onShare: (creation: Creation) => void;
|
|
28
|
-
readonly onDelete: (creation: Creation) => void;
|
|
29
|
-
readonly onViewResult?: (creation: Creation) => void;
|
|
30
|
-
readonly t: (key: string) => string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface CreationMetadata {
|
|
34
|
-
readonly names?: string;
|
|
35
|
-
readonly story?: string;
|
|
36
|
-
readonly description?: string;
|
|
37
|
-
readonly date?: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export const CreationDetailScreen: React.FC<CreationDetailScreenProps> = ({
|
|
41
|
-
userId,
|
|
42
|
-
repository,
|
|
43
|
-
creation,
|
|
44
|
-
config,
|
|
45
|
-
onClose,
|
|
46
|
-
onShare,
|
|
47
|
-
onDelete,
|
|
48
|
-
onViewResult,
|
|
49
|
-
t
|
|
50
|
-
}) => {
|
|
51
|
-
const tokens = useAppDesignTokens();
|
|
52
|
-
const rateMutation = useCreationRating({ userId, repository });
|
|
53
|
-
|
|
54
|
-
const handleRate = async (rating: number) => {
|
|
55
|
-
await rateMutation.mutateAsync({ id: creation.id, rating });
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// Extract data safely
|
|
59
|
-
const metadata = (creation.metadata || {}) as CreationMetadata;
|
|
60
|
-
|
|
61
|
-
// Resolve title:
|
|
62
|
-
// 1. Manually set names in metadata
|
|
63
|
-
// 2. Localized title from config types mapping
|
|
64
|
-
// 3. Fallback to raw creation type (formatted)
|
|
65
|
-
const title = metadata.names || getLocalizedTitle(config, t, creation.type);
|
|
66
|
-
const story = metadata.story || metadata.description || "";
|
|
67
|
-
const date = metadata.date || new Date(creation.createdAt).toLocaleDateString();
|
|
68
|
-
|
|
69
|
-
// Detect if this is a video creation
|
|
70
|
-
const isVideo = useMemo(() => {
|
|
71
|
-
if (VIDEO_TYPES.includes(creation.type as typeof VIDEO_TYPES[number])) return true;
|
|
72
|
-
if (hasVideoContent(creation.output)) return true;
|
|
73
|
-
return false;
|
|
74
|
-
}, [creation.type, creation.output]);
|
|
75
|
-
|
|
76
|
-
// Get video URL and thumbnail for video content
|
|
77
|
-
const videoUrl = creation.output?.videoUrl || creation.uri;
|
|
78
|
-
const thumbnailUrl = getPreviewUrl(creation.output) || undefined;
|
|
79
|
-
|
|
80
|
-
return (
|
|
81
|
-
<ScreenLayout
|
|
82
|
-
header={<DetailHeader onClose={onClose} />}
|
|
83
|
-
>
|
|
84
|
-
{isVideo ? (
|
|
85
|
-
<DetailVideo videoUrl={videoUrl} _thumbnailUrl={thumbnailUrl} />
|
|
86
|
-
) : (
|
|
87
|
-
<DetailImage uri={creation.uri} />
|
|
88
|
-
)}
|
|
89
|
-
|
|
90
|
-
<DetailInfo title={title} date={date} />
|
|
91
|
-
|
|
92
|
-
<CreationRating
|
|
93
|
-
rating={creation.rating || 0}
|
|
94
|
-
onRate={handleRate}
|
|
95
|
-
/>
|
|
96
|
-
|
|
97
|
-
{story ? (
|
|
98
|
-
<DetailStory story={story} />
|
|
99
|
-
) : null}
|
|
100
|
-
|
|
101
|
-
<DetailActions
|
|
102
|
-
onShare={() => onShare(creation)}
|
|
103
|
-
onDelete={() => onDelete(creation)}
|
|
104
|
-
onViewResult={onViewResult ? () => onViewResult(creation) : undefined}
|
|
105
|
-
shareLabel={t("result.shareButton")}
|
|
106
|
-
deleteLabel={t("common.delete")}
|
|
107
|
-
viewResultLabel={onViewResult ? t("result.viewResult") : undefined}
|
|
108
|
-
/>
|
|
109
|
-
</ScreenLayout>
|
|
110
|
-
);
|
|
111
|
-
};
|