@umituz/react-native-ai-generation-content 1.27.7 → 1.27.9
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/generation/wizard/infrastructure/strategies/image-generation.strategy.ts +3 -30
- package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts +3 -30
- package/src/domains/generation/wizard/infrastructure/strategies/wizard-strategy.constants.ts +0 -2
- package/src/domains/scenarios/configs/wizard-configs.ts +28 -28
- package/src/domains/scenarios/index.ts +3 -3
- package/src/features/image-to-video/README.md +414 -0
- package/src/features/image-to-video/domain/constants/animation.constants.ts +47 -0
- package/src/features/image-to-video/domain/constants/duration.constants.ts +13 -0
- package/src/features/image-to-video/domain/constants/form.constants.ts +22 -0
- package/src/features/image-to-video/domain/constants/index.ts +23 -0
- package/src/features/image-to-video/domain/constants/music.constants.ts +53 -0
- package/src/features/image-to-video/domain/index.ts +5 -0
- package/src/features/image-to-video/domain/types/animation.types.ts +20 -0
- package/src/features/image-to-video/domain/types/config.types.ts +56 -0
- package/src/features/image-to-video/domain/types/duration.types.ts +11 -0
- package/src/features/image-to-video/domain/types/form.types.ts +35 -0
- package/src/features/image-to-video/domain/types/image-to-video.types.ts +122 -0
- package/src/features/image-to-video/domain/types/index.ts +39 -0
- package/src/features/image-to-video/domain/types/music.types.ts +21 -0
- package/src/features/image-to-video/index.ts +116 -0
- package/src/features/image-to-video/infrastructure/index.ts +1 -0
- package/src/features/image-to-video/infrastructure/services/image-to-video-executor.ts +165 -0
- package/src/features/image-to-video/infrastructure/services/index.ts +5 -0
- package/src/features/image-to-video/presentation/components/AddMoreCard.tsx +52 -0
- package/src/features/image-to-video/presentation/components/AnimationStyleSelector.tsx +135 -0
- package/src/features/image-to-video/presentation/components/DurationSelector.tsx +110 -0
- package/src/features/image-to-video/presentation/components/EmptyGridState.tsx +69 -0
- package/src/features/image-to-video/presentation/components/GridImageItem.tsx +64 -0
- package/src/features/image-to-video/presentation/components/ImageSelectionGrid.styles.ts +84 -0
- package/src/features/image-to-video/presentation/components/ImageSelectionGrid.tsx +77 -0
- package/src/features/image-to-video/presentation/components/ImageSelectionGrid.types.ts +18 -0
- package/src/features/image-to-video/presentation/components/MusicMoodSelector.tsx +181 -0
- package/src/features/image-to-video/presentation/components/index.ts +30 -0
- package/src/features/image-to-video/presentation/hooks/index.ts +27 -0
- package/src/features/image-to-video/presentation/hooks/useFormState.ts +116 -0
- package/src/features/image-to-video/presentation/hooks/useGeneration.ts +85 -0
- package/src/features/image-to-video/presentation/hooks/useGenerationExecution.ts +143 -0
- package/src/features/image-to-video/presentation/hooks/useImageToVideoFeature.ts +107 -0
- package/src/features/image-to-video/presentation/hooks/useImageToVideoForm.ts +119 -0
- package/src/features/image-to-video/presentation/hooks/useImageToVideoValidation.ts +46 -0
- package/src/features/image-to-video/presentation/index.ts +5 -0
- package/src/features/text-to-image/README.md +394 -0
- package/src/features/text-to-image/domain/constants/index.ts +25 -0
- package/src/features/text-to-image/domain/constants/options.constants.ts +39 -0
- package/src/features/text-to-image/domain/constants/styles.constants.ts +34 -0
- package/src/features/text-to-image/domain/index.ts +7 -0
- package/src/features/text-to-image/domain/types/config.types.ts +75 -0
- package/src/features/text-to-image/domain/types/form.types.ts +58 -0
- package/src/features/text-to-image/domain/types/index.ts +38 -0
- package/src/features/text-to-image/domain/types/text-to-image.types.ts +58 -0
- package/src/features/text-to-image/index.ts +116 -0
- package/src/features/text-to-image/infrastructure/index.ts +1 -0
- package/src/features/text-to-image/infrastructure/services/index.ts +5 -0
- package/src/features/text-to-image/infrastructure/services/text-to-image-executor.ts +147 -0
- package/src/features/text-to-image/presentation/components/index.ts +30 -0
- package/src/features/text-to-image/presentation/hooks/index.ts +30 -0
- package/src/features/text-to-image/presentation/hooks/useFormState.ts +103 -0
- package/src/features/text-to-image/presentation/hooks/useGeneration.ts +134 -0
- package/src/features/text-to-image/presentation/hooks/useTextToImageFeature.ts +111 -0
- package/src/features/text-to-image/presentation/hooks/useTextToImageForm.ts +58 -0
- package/src/features/text-to-image/presentation/index.ts +7 -0
- package/src/features/text-to-video/README.md +412 -0
- package/src/features/text-to-video/domain/index.ts +1 -0
- package/src/features/text-to-video/domain/types/callback.types.ts +69 -0
- package/src/features/text-to-video/domain/types/component.types.ts +106 -0
- package/src/features/text-to-video/domain/types/config.types.ts +61 -0
- package/src/features/text-to-video/domain/types/index.ts +56 -0
- package/src/features/text-to-video/domain/types/request.types.ts +36 -0
- package/src/features/text-to-video/domain/types/state.types.ts +53 -0
- package/src/features/text-to-video/index.ts +68 -0
- package/src/features/text-to-video/infrastructure/index.ts +1 -0
- package/src/features/text-to-video/infrastructure/services/index.ts +5 -0
- package/src/features/text-to-video/infrastructure/services/text-to-video-executor.ts +141 -0
- package/src/features/text-to-video/presentation/components/FrameSelector.tsx +153 -0
- package/src/features/text-to-video/presentation/components/GenerationTabs.tsx +73 -0
- package/src/features/text-to-video/presentation/components/HeroSection.tsx +61 -0
- package/src/features/text-to-video/presentation/components/HintCarousel.tsx +96 -0
- package/src/features/text-to-video/presentation/components/OptionsPanel.tsx +121 -0
- package/src/features/text-to-video/presentation/components/index.ts +10 -0
- package/src/features/text-to-video/presentation/hooks/index.ts +17 -0
- package/src/features/text-to-video/presentation/hooks/useTextToVideoFeature.ts +187 -0
- package/src/features/text-to-video/presentation/hooks/useTextToVideoForm.ts +134 -0
- package/src/features/text-to-video/presentation/index.ts +7 -0
- package/src/index.ts +5 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image-to-Video Executor
|
|
3
|
+
* Provider-agnostic image-to-video execution using active AI provider
|
|
4
|
+
* Uses progress mapper for consistent progress reporting
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { providerRegistry } from "../../../../infrastructure/services";
|
|
8
|
+
|
|
9
|
+
/** Map job status to progress percentage */
|
|
10
|
+
const getProgressFromJobStatus = (status: string): number => {
|
|
11
|
+
switch (status.toLowerCase()) {
|
|
12
|
+
case "queued": return 10;
|
|
13
|
+
case "in_queue": return 15;
|
|
14
|
+
case "processing": return 50;
|
|
15
|
+
case "in_progress": return 60;
|
|
16
|
+
case "completed": return 100;
|
|
17
|
+
default: return 30;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
import type {
|
|
21
|
+
ImageToVideoRequest,
|
|
22
|
+
ImageToVideoResult,
|
|
23
|
+
ImageToVideoInputBuilder,
|
|
24
|
+
ImageToVideoResultExtractor,
|
|
25
|
+
} from "../../domain/types";
|
|
26
|
+
|
|
27
|
+
declare const __DEV__: boolean;
|
|
28
|
+
|
|
29
|
+
export interface ExecuteImageToVideoOptions {
|
|
30
|
+
model: string;
|
|
31
|
+
buildInput: ImageToVideoInputBuilder;
|
|
32
|
+
extractResult?: ImageToVideoResultExtractor;
|
|
33
|
+
onProgress?: (progress: number) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function defaultExtractResult(
|
|
37
|
+
result: unknown,
|
|
38
|
+
): { videoUrl?: string; thumbnailUrl?: string } | undefined {
|
|
39
|
+
if (typeof result !== "object" || result === null) return undefined;
|
|
40
|
+
|
|
41
|
+
const r = result as Record<string, unknown>;
|
|
42
|
+
|
|
43
|
+
if (typeof r.video === "string") {
|
|
44
|
+
return { videoUrl: r.video };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (r.video && typeof r.video === "object") {
|
|
48
|
+
const video = r.video as Record<string, unknown>;
|
|
49
|
+
if (typeof video.url === "string") {
|
|
50
|
+
return {
|
|
51
|
+
videoUrl: video.url,
|
|
52
|
+
thumbnailUrl:
|
|
53
|
+
typeof r.thumbnail === "string" ? r.thumbnail : undefined,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function executeImageToVideo(
|
|
62
|
+
request: ImageToVideoRequest,
|
|
63
|
+
options: ExecuteImageToVideoOptions,
|
|
64
|
+
): Promise<ImageToVideoResult> {
|
|
65
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
66
|
+
|
|
67
|
+
console.log("[ImageToVideoExecutor] executeImageToVideo() called");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const provider = providerRegistry.getActiveProvider();
|
|
71
|
+
|
|
72
|
+
if (!provider) {
|
|
73
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
74
|
+
|
|
75
|
+
console.error("[ImageToVideoExecutor] No AI provider configured");
|
|
76
|
+
}
|
|
77
|
+
return { success: false, error: "No AI provider configured" };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!provider.isInitialized()) {
|
|
81
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
82
|
+
|
|
83
|
+
console.error("[ImageToVideoExecutor] AI provider not initialized");
|
|
84
|
+
}
|
|
85
|
+
return { success: false, error: "AI provider not initialized" };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!request.imageBase64) {
|
|
89
|
+
return { success: false, error: "Image base64 is required" };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const { model, buildInput, extractResult, onProgress } = options;
|
|
93
|
+
|
|
94
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
95
|
+
|
|
96
|
+
console.log(`[ImageToVideoExecutor] Provider: ${provider.providerId}, Model: ${model}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
onProgress?.(5);
|
|
101
|
+
|
|
102
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
103
|
+
|
|
104
|
+
console.log("[ImageToVideoExecutor] Starting provider.subscribe()...");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Build input directly - let buildInput handle base64 format
|
|
108
|
+
const input = buildInput(request.imageBase64, request.motionPrompt, request.options);
|
|
109
|
+
|
|
110
|
+
// Use subscribe for video generation (long-running operation with queue)
|
|
111
|
+
// subscribe provides progress updates unlike run()
|
|
112
|
+
const result = await provider.subscribe(model, input, {
|
|
113
|
+
onQueueUpdate: (status) => {
|
|
114
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
115
|
+
|
|
116
|
+
console.log("[ImageToVideoExecutor] Queue status:", status.status, "position:", status.queuePosition);
|
|
117
|
+
}
|
|
118
|
+
// Map provider status to progress using centralized mapper
|
|
119
|
+
const progress = getProgressFromJobStatus(status.status);
|
|
120
|
+
onProgress?.(progress);
|
|
121
|
+
},
|
|
122
|
+
timeoutMs: 300000, // 5 minutes timeout for video generation
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
126
|
+
|
|
127
|
+
console.log("[ImageToVideoExecutor] Subscribe resolved, result keys:", result ? Object.keys(result as object) : "null");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
131
|
+
|
|
132
|
+
console.log("[ImageToVideoExecutor] provider.subscribe() completed");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const extractor = extractResult || defaultExtractResult;
|
|
136
|
+
const extracted = extractor(result);
|
|
137
|
+
onProgress?.(100);
|
|
138
|
+
|
|
139
|
+
if (!extracted?.videoUrl) {
|
|
140
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
141
|
+
|
|
142
|
+
console.error("[ImageToVideoExecutor] No video URL in response");
|
|
143
|
+
}
|
|
144
|
+
return { success: false, error: "No video in response" };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
success: true,
|
|
149
|
+
videoUrl: extracted.videoUrl,
|
|
150
|
+
thumbnailUrl: extracted.thumbnailUrl,
|
|
151
|
+
};
|
|
152
|
+
} catch (error) {
|
|
153
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
154
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
155
|
+
|
|
156
|
+
console.error("[ImageToVideoExecutor] Error:", message);
|
|
157
|
+
}
|
|
158
|
+
return { success: false, error: message };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function hasImageToVideoSupport(): boolean {
|
|
163
|
+
const provider = providerRegistry.getActiveProvider();
|
|
164
|
+
return provider !== null && provider.isInitialized();
|
|
165
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AddMoreCard Component
|
|
3
|
+
* Displays "Add More" button in the image grid
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { TouchableOpacity } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicIcon,
|
|
10
|
+
AtomicText,
|
|
11
|
+
useAppDesignTokens,
|
|
12
|
+
} from "@umituz/react-native-design-system";
|
|
13
|
+
import type { ImageSelectionGridStyles } from "./ImageSelectionGrid.styles";
|
|
14
|
+
|
|
15
|
+
interface AddMoreCardProps {
|
|
16
|
+
styles: ImageSelectionGridStyles;
|
|
17
|
+
addMoreText: string;
|
|
18
|
+
onPress: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function AddMoreCard({
|
|
22
|
+
styles,
|
|
23
|
+
addMoreText,
|
|
24
|
+
onPress,
|
|
25
|
+
}: AddMoreCardProps) {
|
|
26
|
+
const tokens = useAppDesignTokens();
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<TouchableOpacity
|
|
30
|
+
style={[
|
|
31
|
+
styles.addMoreCard,
|
|
32
|
+
{
|
|
33
|
+
backgroundColor: tokens.colors.surface,
|
|
34
|
+
borderColor: tokens.colors.borderLight,
|
|
35
|
+
},
|
|
36
|
+
]}
|
|
37
|
+
onPress={onPress}
|
|
38
|
+
activeOpacity={0.7}
|
|
39
|
+
>
|
|
40
|
+
<AtomicIcon name="add" size="lg" color="primary" />
|
|
41
|
+
<AtomicText
|
|
42
|
+
type="labelSmall"
|
|
43
|
+
style={[
|
|
44
|
+
styles.addMoreText,
|
|
45
|
+
{ color: tokens.colors.primary },
|
|
46
|
+
]}
|
|
47
|
+
>
|
|
48
|
+
{addMoreText}
|
|
49
|
+
</AtomicText>
|
|
50
|
+
</TouchableOpacity>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animation Style Selector Component
|
|
3
|
+
* Generic component for animation style selection
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
AtomicIcon,
|
|
11
|
+
useAppDesignTokens,
|
|
12
|
+
} from "@umituz/react-native-design-system";
|
|
13
|
+
import type { AnimationStyle, AnimationStyleId } from "../../domain/types";
|
|
14
|
+
|
|
15
|
+
export interface AnimationStyleSelectorProps {
|
|
16
|
+
styles: AnimationStyle[];
|
|
17
|
+
selectedStyle: AnimationStyleId;
|
|
18
|
+
onStyleSelect: (styleId: AnimationStyleId) => void;
|
|
19
|
+
label: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const AnimationStyleSelector: React.FC<AnimationStyleSelectorProps> = ({
|
|
23
|
+
styles,
|
|
24
|
+
selectedStyle,
|
|
25
|
+
onStyleSelect,
|
|
26
|
+
label,
|
|
27
|
+
}) => {
|
|
28
|
+
const tokens = useAppDesignTokens();
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<View style={componentStyles.section}>
|
|
32
|
+
<AtomicText
|
|
33
|
+
type="bodyMedium"
|
|
34
|
+
style={[componentStyles.label, { color: tokens.colors.textPrimary }]}
|
|
35
|
+
>
|
|
36
|
+
{label}
|
|
37
|
+
</AtomicText>
|
|
38
|
+
{styles.map((style) => {
|
|
39
|
+
const isSelected = selectedStyle === style.id;
|
|
40
|
+
return (
|
|
41
|
+
<TouchableOpacity
|
|
42
|
+
key={style.id}
|
|
43
|
+
style={[
|
|
44
|
+
componentStyles.card,
|
|
45
|
+
{
|
|
46
|
+
backgroundColor: isSelected
|
|
47
|
+
? tokens.colors.primary + "20"
|
|
48
|
+
: tokens.colors.surface,
|
|
49
|
+
borderColor: isSelected
|
|
50
|
+
? tokens.colors.primary
|
|
51
|
+
: tokens.colors.borderLight,
|
|
52
|
+
},
|
|
53
|
+
]}
|
|
54
|
+
onPress={() => onStyleSelect(style.id)}
|
|
55
|
+
activeOpacity={0.7}
|
|
56
|
+
>
|
|
57
|
+
<View style={componentStyles.cardContent}>
|
|
58
|
+
<View
|
|
59
|
+
style={[
|
|
60
|
+
componentStyles.iconContainer,
|
|
61
|
+
{
|
|
62
|
+
backgroundColor: isSelected
|
|
63
|
+
? tokens.colors.primary
|
|
64
|
+
: tokens.colors.primary + "20",
|
|
65
|
+
},
|
|
66
|
+
]}
|
|
67
|
+
>
|
|
68
|
+
<AtomicIcon
|
|
69
|
+
name={style.icon as never}
|
|
70
|
+
size="md"
|
|
71
|
+
color={isSelected ? "onSurface" : "primary"}
|
|
72
|
+
/>
|
|
73
|
+
</View>
|
|
74
|
+
<View style={componentStyles.textContainer}>
|
|
75
|
+
<AtomicText
|
|
76
|
+
type="bodyMedium"
|
|
77
|
+
style={[
|
|
78
|
+
componentStyles.styleName,
|
|
79
|
+
{ color: tokens.colors.textPrimary },
|
|
80
|
+
]}
|
|
81
|
+
>
|
|
82
|
+
{style.name}
|
|
83
|
+
</AtomicText>
|
|
84
|
+
<AtomicText
|
|
85
|
+
type="labelSmall"
|
|
86
|
+
style={{ color: tokens.colors.textSecondary }}
|
|
87
|
+
>
|
|
88
|
+
{style.description}
|
|
89
|
+
</AtomicText>
|
|
90
|
+
</View>
|
|
91
|
+
{isSelected && (
|
|
92
|
+
<AtomicIcon name="checkmark-outline" size="md" color="primary" />
|
|
93
|
+
)}
|
|
94
|
+
</View>
|
|
95
|
+
</TouchableOpacity>
|
|
96
|
+
);
|
|
97
|
+
})}
|
|
98
|
+
</View>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const componentStyles = StyleSheet.create({
|
|
103
|
+
section: {
|
|
104
|
+
padding: 16,
|
|
105
|
+
marginBottom: 8,
|
|
106
|
+
},
|
|
107
|
+
label: {
|
|
108
|
+
fontWeight: "600",
|
|
109
|
+
marginBottom: 12,
|
|
110
|
+
},
|
|
111
|
+
card: {
|
|
112
|
+
padding: 16,
|
|
113
|
+
borderRadius: 12,
|
|
114
|
+
borderWidth: 2,
|
|
115
|
+
marginBottom: 12,
|
|
116
|
+
},
|
|
117
|
+
cardContent: {
|
|
118
|
+
flexDirection: "row",
|
|
119
|
+
alignItems: "center",
|
|
120
|
+
},
|
|
121
|
+
iconContainer: {
|
|
122
|
+
width: 48,
|
|
123
|
+
height: 48,
|
|
124
|
+
borderRadius: 24,
|
|
125
|
+
alignItems: "center",
|
|
126
|
+
justifyContent: "center",
|
|
127
|
+
},
|
|
128
|
+
textContainer: {
|
|
129
|
+
flex: 1,
|
|
130
|
+
marginLeft: 12,
|
|
131
|
+
},
|
|
132
|
+
styleName: {
|
|
133
|
+
fontWeight: "600",
|
|
134
|
+
},
|
|
135
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Duration Selector Component
|
|
3
|
+
* Generic component for video duration selection
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
useAppDesignTokens,
|
|
11
|
+
} from "@umituz/react-native-design-system";
|
|
12
|
+
import type { VideoDuration, DurationOption } from "../../domain/types";
|
|
13
|
+
|
|
14
|
+
export interface DurationSelectorProps {
|
|
15
|
+
options: DurationOption[];
|
|
16
|
+
selectedDuration: VideoDuration;
|
|
17
|
+
onDurationSelect: (duration: VideoDuration) => void;
|
|
18
|
+
label: string;
|
|
19
|
+
imageCount: number;
|
|
20
|
+
totalVideoLabel: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const DurationSelector: React.FC<DurationSelectorProps> = ({
|
|
24
|
+
options,
|
|
25
|
+
selectedDuration,
|
|
26
|
+
onDurationSelect,
|
|
27
|
+
label,
|
|
28
|
+
imageCount,
|
|
29
|
+
totalVideoLabel,
|
|
30
|
+
}) => {
|
|
31
|
+
const tokens = useAppDesignTokens();
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<View style={componentStyles.section}>
|
|
35
|
+
<AtomicText
|
|
36
|
+
type="bodyMedium"
|
|
37
|
+
style={[componentStyles.label, { color: tokens.colors.textPrimary }]}
|
|
38
|
+
>
|
|
39
|
+
{label}
|
|
40
|
+
</AtomicText>
|
|
41
|
+
<View style={componentStyles.grid}>
|
|
42
|
+
{options.map((option) => {
|
|
43
|
+
const isSelected = selectedDuration === option.value;
|
|
44
|
+
return (
|
|
45
|
+
<TouchableOpacity
|
|
46
|
+
key={option.value}
|
|
47
|
+
style={[
|
|
48
|
+
componentStyles.button,
|
|
49
|
+
{
|
|
50
|
+
backgroundColor: isSelected
|
|
51
|
+
? tokens.colors.primary
|
|
52
|
+
: tokens.colors.surface,
|
|
53
|
+
borderColor: isSelected
|
|
54
|
+
? tokens.colors.primary
|
|
55
|
+
: tokens.colors.borderLight,
|
|
56
|
+
},
|
|
57
|
+
]}
|
|
58
|
+
onPress={() => onDurationSelect(option.value)}
|
|
59
|
+
activeOpacity={0.7}
|
|
60
|
+
>
|
|
61
|
+
<AtomicText
|
|
62
|
+
type="bodyMedium"
|
|
63
|
+
style={{
|
|
64
|
+
color: isSelected
|
|
65
|
+
? tokens.colors.textInverse
|
|
66
|
+
: tokens.colors.textPrimary,
|
|
67
|
+
fontWeight: isSelected ? "600" : "400",
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
{option.label ?? `${option.value}s`}
|
|
71
|
+
</AtomicText>
|
|
72
|
+
</TouchableOpacity>
|
|
73
|
+
);
|
|
74
|
+
})}
|
|
75
|
+
</View>
|
|
76
|
+
<AtomicText
|
|
77
|
+
type="labelSmall"
|
|
78
|
+
style={[componentStyles.hint, { color: tokens.colors.textSecondary }]}
|
|
79
|
+
>
|
|
80
|
+
{totalVideoLabel.replace("{duration}", String(imageCount * selectedDuration))}
|
|
81
|
+
</AtomicText>
|
|
82
|
+
</View>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const componentStyles = StyleSheet.create({
|
|
87
|
+
section: {
|
|
88
|
+
padding: 16,
|
|
89
|
+
marginBottom: 8,
|
|
90
|
+
},
|
|
91
|
+
label: {
|
|
92
|
+
fontWeight: "600",
|
|
93
|
+
marginBottom: 12,
|
|
94
|
+
},
|
|
95
|
+
grid: {
|
|
96
|
+
flexDirection: "row",
|
|
97
|
+
gap: 12,
|
|
98
|
+
},
|
|
99
|
+
button: {
|
|
100
|
+
flex: 1,
|
|
101
|
+
paddingVertical: 16,
|
|
102
|
+
borderRadius: 12,
|
|
103
|
+
borderWidth: 1,
|
|
104
|
+
alignItems: "center",
|
|
105
|
+
justifyContent: "center",
|
|
106
|
+
},
|
|
107
|
+
hint: {
|
|
108
|
+
marginTop: 8,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EmptyGridState Component
|
|
3
|
+
* Displays empty state when no images are selected
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, TouchableOpacity } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
AtomicIcon,
|
|
11
|
+
useAppDesignTokens,
|
|
12
|
+
} from "@umituz/react-native-design-system";
|
|
13
|
+
import type { ImageSelectionGridStyles } from "./ImageSelectionGrid.styles";
|
|
14
|
+
import type { ImageSelectionGridTranslations } from "./ImageSelectionGrid.types";
|
|
15
|
+
|
|
16
|
+
interface EmptyGridStateProps {
|
|
17
|
+
styles: ImageSelectionGridStyles;
|
|
18
|
+
maxImages: number;
|
|
19
|
+
translations: ImageSelectionGridTranslations;
|
|
20
|
+
onSelectImages: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function EmptyGridState({
|
|
24
|
+
styles,
|
|
25
|
+
maxImages,
|
|
26
|
+
translations,
|
|
27
|
+
onSelectImages,
|
|
28
|
+
}: EmptyGridStateProps) {
|
|
29
|
+
const tokens = useAppDesignTokens();
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<View style={styles.section}>
|
|
33
|
+
<AtomicText
|
|
34
|
+
type="bodyMedium"
|
|
35
|
+
style={[styles.label, { color: tokens.colors.textPrimary }]}
|
|
36
|
+
>
|
|
37
|
+
{translations.selectedImages} (0/{maxImages})
|
|
38
|
+
</AtomicText>
|
|
39
|
+
<TouchableOpacity
|
|
40
|
+
style={[
|
|
41
|
+
styles.uploadBox,
|
|
42
|
+
{
|
|
43
|
+
backgroundColor: tokens.colors.surface,
|
|
44
|
+
borderColor: tokens.colors.borderLight,
|
|
45
|
+
},
|
|
46
|
+
]}
|
|
47
|
+
onPress={onSelectImages}
|
|
48
|
+
activeOpacity={0.7}
|
|
49
|
+
>
|
|
50
|
+
<AtomicIcon name="cloud-upload-outline" size="xl" color="primary" />
|
|
51
|
+
<AtomicText
|
|
52
|
+
type="bodyMedium"
|
|
53
|
+
style={[
|
|
54
|
+
styles.uploadText,
|
|
55
|
+
{ color: tokens.colors.primary },
|
|
56
|
+
]}
|
|
57
|
+
>
|
|
58
|
+
{translations.selectImages}
|
|
59
|
+
</AtomicText>
|
|
60
|
+
<AtomicText
|
|
61
|
+
type="labelSmall"
|
|
62
|
+
style={{ color: tokens.colors.textSecondary }}
|
|
63
|
+
>
|
|
64
|
+
{translations.chooseUpTo.replace("{max}", String(maxImages))}
|
|
65
|
+
</AtomicText>
|
|
66
|
+
</TouchableOpacity>
|
|
67
|
+
</View>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GridImageItem Component
|
|
3
|
+
* Displays a single image in the grid with remove button and badge
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, TouchableOpacity, Image } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicIcon,
|
|
10
|
+
AtomicText,
|
|
11
|
+
useAppDesignTokens,
|
|
12
|
+
} from "@umituz/react-native-design-system";
|
|
13
|
+
import type { ImageSelectionGridStyles } from "./ImageSelectionGrid.styles";
|
|
14
|
+
|
|
15
|
+
interface GridImageItemProps {
|
|
16
|
+
styles: ImageSelectionGridStyles;
|
|
17
|
+
uri: string;
|
|
18
|
+
index: number;
|
|
19
|
+
onRemove: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function GridImageItem({
|
|
23
|
+
styles,
|
|
24
|
+
uri,
|
|
25
|
+
index,
|
|
26
|
+
onRemove,
|
|
27
|
+
}: GridImageItemProps) {
|
|
28
|
+
const tokens = useAppDesignTokens();
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<View style={styles.imageCard}>
|
|
32
|
+
<Image
|
|
33
|
+
source={{ uri }}
|
|
34
|
+
style={styles.imagePreview}
|
|
35
|
+
resizeMode="cover"
|
|
36
|
+
/>
|
|
37
|
+
<TouchableOpacity
|
|
38
|
+
style={[
|
|
39
|
+
styles.removeButton,
|
|
40
|
+
{ backgroundColor: tokens.colors.error },
|
|
41
|
+
]}
|
|
42
|
+
onPress={onRemove}
|
|
43
|
+
>
|
|
44
|
+
<AtomicIcon name="close-circle" size="sm" color="onSurface" />
|
|
45
|
+
</TouchableOpacity>
|
|
46
|
+
<View
|
|
47
|
+
style={[
|
|
48
|
+
styles.imageBadge,
|
|
49
|
+
{ backgroundColor: tokens.colors.primary },
|
|
50
|
+
]}
|
|
51
|
+
>
|
|
52
|
+
<AtomicText
|
|
53
|
+
type="labelSmall"
|
|
54
|
+
style={[
|
|
55
|
+
styles.badgeText,
|
|
56
|
+
{ color: tokens.colors.textInverse },
|
|
57
|
+
]}
|
|
58
|
+
>
|
|
59
|
+
{index + 1}
|
|
60
|
+
</AtomicText>
|
|
61
|
+
</View>
|
|
62
|
+
</View>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImageSelectionGrid Styles
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { StyleSheet } from "react-native";
|
|
6
|
+
import type { DesignTokens } from "@umituz/react-native-design-system";
|
|
7
|
+
|
|
8
|
+
export function createImageSelectionGridStyles(_tokens: DesignTokens) {
|
|
9
|
+
return StyleSheet.create({
|
|
10
|
+
section: {
|
|
11
|
+
padding: 16,
|
|
12
|
+
marginBottom: 8,
|
|
13
|
+
},
|
|
14
|
+
label: {
|
|
15
|
+
fontWeight: "600",
|
|
16
|
+
marginBottom: 12,
|
|
17
|
+
},
|
|
18
|
+
uploadBox: {
|
|
19
|
+
padding: 48,
|
|
20
|
+
borderRadius: 16,
|
|
21
|
+
alignItems: "center",
|
|
22
|
+
justifyContent: "center",
|
|
23
|
+
borderWidth: 2,
|
|
24
|
+
borderStyle: "dashed",
|
|
25
|
+
},
|
|
26
|
+
uploadText: {
|
|
27
|
+
fontWeight: "600",
|
|
28
|
+
marginTop: 12,
|
|
29
|
+
},
|
|
30
|
+
scroll: {
|
|
31
|
+
marginHorizontal: -16,
|
|
32
|
+
paddingHorizontal: 16,
|
|
33
|
+
},
|
|
34
|
+
imageCard: {
|
|
35
|
+
width: 120,
|
|
36
|
+
height: 120,
|
|
37
|
+
borderRadius: 12,
|
|
38
|
+
marginRight: 12,
|
|
39
|
+
position: "relative",
|
|
40
|
+
},
|
|
41
|
+
imagePreview: {
|
|
42
|
+
width: "100%",
|
|
43
|
+
height: "100%",
|
|
44
|
+
borderRadius: 12,
|
|
45
|
+
},
|
|
46
|
+
removeButton: {
|
|
47
|
+
position: "absolute",
|
|
48
|
+
top: 6,
|
|
49
|
+
right: 6,
|
|
50
|
+
width: 24,
|
|
51
|
+
height: 24,
|
|
52
|
+
borderRadius: 12,
|
|
53
|
+
alignItems: "center",
|
|
54
|
+
justifyContent: "center",
|
|
55
|
+
},
|
|
56
|
+
imageBadge: {
|
|
57
|
+
position: "absolute",
|
|
58
|
+
bottom: 6,
|
|
59
|
+
left: 6,
|
|
60
|
+
width: 24,
|
|
61
|
+
height: 24,
|
|
62
|
+
borderRadius: 12,
|
|
63
|
+
alignItems: "center",
|
|
64
|
+
justifyContent: "center",
|
|
65
|
+
},
|
|
66
|
+
badgeText: {
|
|
67
|
+
fontSize: 10,
|
|
68
|
+
},
|
|
69
|
+
addMoreCard: {
|
|
70
|
+
width: 120,
|
|
71
|
+
height: 120,
|
|
72
|
+
borderRadius: 12,
|
|
73
|
+
alignItems: "center",
|
|
74
|
+
justifyContent: "center",
|
|
75
|
+
borderWidth: 2,
|
|
76
|
+
borderStyle: "dashed",
|
|
77
|
+
},
|
|
78
|
+
addMoreText: {
|
|
79
|
+
marginTop: 4,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type ImageSelectionGridStyles = ReturnType<typeof createImageSelectionGridStyles>;
|