@umituz/react-native-ai-generation-content 1.17.21 → 1.17.23

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.
Files changed (44) hide show
  1. package/package.json +1 -1
  2. package/src/domains/creations/domain/types/creation-filter.ts +16 -18
  3. package/src/domains/creations/domain/value-objects/CreationsConfig.ts +12 -0
  4. package/src/domains/creations/presentation/components/FilterSheets.tsx +63 -0
  5. package/src/domains/creations/presentation/components/GalleryHeader.tsx +95 -93
  6. package/src/domains/creations/presentation/components/index.ts +1 -0
  7. package/src/domains/creations/presentation/hooks/index.ts +3 -0
  8. package/src/domains/creations/presentation/hooks/useCreationsFilter.ts +35 -48
  9. package/src/domains/creations/presentation/hooks/useGalleryFilters.ts +78 -0
  10. package/src/domains/creations/presentation/hooks/useMediaFilter.ts +54 -0
  11. package/src/domains/creations/presentation/hooks/useStatusFilter.ts +54 -0
  12. package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +81 -156
  13. package/src/features/image-to-video/index.ts +90 -3
  14. package/src/features/image-to-video/presentation/components/AnimationStyleSelector.tsx +135 -0
  15. package/src/features/image-to-video/presentation/components/DurationSelector.tsx +110 -0
  16. package/src/features/image-to-video/presentation/components/GenerateButton.tsx +95 -0
  17. package/src/features/image-to-video/presentation/components/HeroSection.tsx +89 -0
  18. package/src/features/image-to-video/presentation/components/ImageSelectionGrid.tsx +234 -0
  19. package/src/features/image-to-video/presentation/components/MusicMoodSelector.tsx +181 -0
  20. package/src/features/image-to-video/presentation/components/index.ts +30 -0
  21. package/src/features/image-to-video/presentation/hooks/index.ts +22 -0
  22. package/src/features/image-to-video/presentation/hooks/useFormState.ts +116 -0
  23. package/src/features/image-to-video/presentation/hooks/useGeneration.ts +85 -0
  24. package/src/features/image-to-video/presentation/hooks/useImageToVideoForm.ts +93 -0
  25. package/src/features/image-to-video/presentation/index.ts +4 -0
  26. package/src/features/text-to-video/domain/types/callback.types.ts +50 -0
  27. package/src/features/text-to-video/domain/types/component.types.ts +106 -0
  28. package/src/features/text-to-video/domain/types/config.types.ts +61 -0
  29. package/src/features/text-to-video/domain/types/index.ts +48 -4
  30. package/src/features/text-to-video/domain/types/request.types.ts +36 -0
  31. package/src/features/text-to-video/domain/types/state.types.ts +53 -0
  32. package/src/features/text-to-video/index.ts +41 -3
  33. package/src/features/text-to-video/infrastructure/services/text-to-video-executor.ts +1 -1
  34. package/src/features/text-to-video/presentation/components/FrameSelector.tsx +153 -0
  35. package/src/features/text-to-video/presentation/components/GenerationTabs.tsx +73 -0
  36. package/src/features/text-to-video/presentation/components/HeroSection.tsx +67 -0
  37. package/src/features/text-to-video/presentation/components/HintCarousel.tsx +96 -0
  38. package/src/features/text-to-video/presentation/components/OptionsPanel.tsx +123 -0
  39. package/src/features/text-to-video/presentation/components/index.ts +10 -0
  40. package/src/features/text-to-video/presentation/hooks/index.ts +11 -0
  41. package/src/features/text-to-video/presentation/hooks/useTextToVideoFeature.ts +77 -20
  42. package/src/features/text-to-video/presentation/hooks/useTextToVideoForm.ts +134 -0
  43. package/src/features/text-to-video/presentation/index.ts +6 -0
  44. package/src/features/text-to-video/domain/types/text-to-video.types.ts +0 -65
@@ -0,0 +1,54 @@
1
+ /**
2
+ * useStatusFilter Hook
3
+ * Handles status filtering (completed, pending, processing, failed)
4
+ * SOLID: Single Responsibility - Only handles status filter state
5
+ */
6
+
7
+ import { useState, useCallback, useMemo } from "react";
8
+ import type { FilterOption } from "../../domain/types/creation-filter";
9
+
10
+ interface UseStatusFilterProps {
11
+ readonly options: FilterOption[];
12
+ readonly t: (key: string) => string;
13
+ readonly defaultId?: string;
14
+ }
15
+
16
+ interface UseStatusFilterReturn {
17
+ readonly selectedId: string;
18
+ readonly filterOptions: FilterOption[];
19
+ readonly hasActiveFilter: boolean;
20
+ readonly selectFilter: (id: string) => void;
21
+ readonly clearFilter: () => void;
22
+ }
23
+
24
+ export function useStatusFilter({
25
+ options,
26
+ t,
27
+ defaultId = "all"
28
+ }: UseStatusFilterProps): UseStatusFilterReturn {
29
+ const [selectedId, setSelectedId] = useState(defaultId);
30
+
31
+ const filterOptions = useMemo(() =>
32
+ options.map(opt => ({
33
+ ...opt,
34
+ label: opt.labelKey ? t(opt.labelKey) : opt.label
35
+ })),
36
+ [options, t]
37
+ );
38
+
39
+ const selectFilter = useCallback((id: string) => {
40
+ setSelectedId(id);
41
+ }, []);
42
+
43
+ const clearFilter = useCallback(() => {
44
+ setSelectedId(defaultId);
45
+ }, [defaultId]);
46
+
47
+ return {
48
+ selectedId,
49
+ filterOptions,
50
+ hasActiveFilter: selectedId !== defaultId,
51
+ selectFilter,
52
+ clearFilter
53
+ };
54
+ }
@@ -1,5 +1,3 @@
1
- declare const __DEV__: boolean;
2
-
3
1
  import React, { useMemo, useCallback, useState } from "react";
4
2
  import { View, FlatList, RefreshControl, StyleSheet } from "react-native";
5
3
  import { useSafeAreaInsets } from "react-native-safe-area-context";
@@ -9,21 +7,14 @@ import {
9
7
  AlertType,
10
8
  AlertMode,
11
9
  useSharing,
12
- FilterBottomSheet,
13
- type BottomSheetModalRef,
14
- type DesignTokens,
10
+ FilterSheet,
15
11
  } from "@umituz/react-native-design-system";
16
12
  import { useFocusEffect } from "@react-navigation/native";
17
13
  import { useCreations } from "../hooks/useCreations";
18
14
  import { useDeleteCreation } from "../hooks/useDeleteCreation";
19
- import { useCreationsFilter } from "../hooks/useCreationsFilter";
20
- import {
21
- GalleryHeader,
22
- CreationCard,
23
- CreationImageViewer,
24
- GalleryEmptyStates,
25
- } from "../components";
26
- import { getFilterCategoriesFromConfig } from "../utils/filterUtils";
15
+ import { useGalleryFilters } from "../hooks/useGalleryFilters";
16
+ import { GalleryHeader, CreationCard, CreationImageViewer, GalleryEmptyStates } from "../components";
17
+ import { MEDIA_FILTER_OPTIONS, STATUS_FILTER_OPTIONS } from "../../domain/types/creation-filter";
27
18
  import type { Creation } from "../../domain/entities/Creation";
28
19
  import type { CreationsConfig } from "../../domain/value-objects/CreationsConfig";
29
20
  import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
@@ -56,7 +47,6 @@ function CreationsGalleryScreenContent({
56
47
  repository,
57
48
  config,
58
49
  t,
59
- locale: _locale = "en-US",
60
50
  enableEditing = false,
61
51
  onImageEdit,
62
52
  onEmptyAction,
@@ -71,199 +61,134 @@ function CreationsGalleryScreenContent({
71
61
  const [viewerVisible, setViewerVisible] = useState(false);
72
62
  const [viewerIndex, setViewerIndex] = useState(0);
73
63
  const [selectedCreation, setSelectedCreation] = useState<Creation | null>(null);
74
- const filterSheetRef = React.useRef<BottomSheetModalRef>(null);
75
64
 
76
- const { data: creationsData, isLoading, refetch } = useCreations({ userId, repository });
77
- const creations = creationsData;
65
+ const { data: creations, isLoading, refetch } = useCreations({ userId, repository });
78
66
  const deleteMutation = useDeleteCreation({ userId, repository });
79
- const { filtered, selectedIds, toggleFilter, clearFilters, isFiltered } = useCreationsFilter({ creations });
80
67
 
81
- useFocusEffect(
82
- useCallback(() => {
83
- void refetch();
84
- }, [refetch])
85
- );
68
+ const statusOptions = config.filterConfig?.statusOptions ?? STATUS_FILTER_OPTIONS;
69
+ const mediaOptions = config.filterConfig?.mediaOptions ?? MEDIA_FILTER_OPTIONS;
70
+ const showStatusFilter = config.filterConfig?.showStatusFilter ?? true;
71
+ const showMediaFilter = config.filterConfig?.showMediaFilter ?? true;
86
72
 
87
- const allCategories = useMemo(
88
- () => getFilterCategoriesFromConfig(config, t),
89
- [config, t],
90
- );
73
+ const filters = useGalleryFilters({ creations, statusOptions, mediaOptions, t });
74
+
75
+ useFocusEffect(useCallback(() => { void refetch(); }, [refetch]));
91
76
 
92
- const handleShare = useCallback((creation: Creation) => {
93
- void share(creation.uri, { dialogTitle: t("common.share") });
77
+ const handleShare = useCallback((c: Creation) => {
78
+ void share(c.uri, { dialogTitle: t("common.share") });
94
79
  }, [share, t]);
95
80
 
96
- const handleDelete = useCallback((creation: Creation) => {
97
- alert.show(
98
- AlertType.WARNING,
99
- AlertMode.MODAL,
100
- t(config.translations.deleteTitle),
101
- t(config.translations.deleteMessage),
102
- {
103
- actions: [
104
- { id: 'cancel', label: t("common.cancel"), onPress: () => { } },
105
- {
106
- id: 'delete',
107
- label: t("common.delete"),
108
- style: 'destructive',
109
- onPress: async () => {
110
- const success = await deleteMutation.mutateAsync(creation.id);
111
- if (success) {
112
- setSelectedCreation(null);
113
- }
114
- }
115
- }
116
- ]
117
- }
118
- );
81
+ const handleDelete = useCallback((c: Creation) => {
82
+ alert.show(AlertType.WARNING, AlertMode.MODAL, t(config.translations.deleteTitle), t(config.translations.deleteMessage), {
83
+ actions: [
84
+ { id: "cancel", label: t("common.cancel"), onPress: () => {} },
85
+ { id: "delete", label: t("common.delete"), style: "destructive", onPress: async () => {
86
+ const success = await deleteMutation.mutateAsync(c.id);
87
+ if (success) setSelectedCreation(null);
88
+ }}
89
+ ]
90
+ });
119
91
  }, [alert, config, deleteMutation, t]);
120
92
 
121
- const handleView = useCallback((creation: Creation) => {
122
- setSelectedCreation(creation);
123
- }, []);
124
-
125
- const handleFavorite = useCallback((creation: Creation, isFavorite: boolean) => {
93
+ const handleFavorite = useCallback((c: Creation, isFavorite: boolean) => {
126
94
  void (async () => {
127
95
  if (!userId) return;
128
- const success = await repository.updateFavorite(userId, creation.id, isFavorite);
129
- if (success) {
130
- void refetch();
131
- }
96
+ const success = await repository.updateFavorite(userId, c.id, isFavorite);
97
+ if (success) void refetch();
132
98
  })();
133
99
  }, [userId, repository, refetch]);
134
100
 
135
- const styles = useStyles(tokens, insets);
136
-
137
- const renderItem = useCallback(
138
- ({ item }: { item: Creation }) => (
139
- <CreationCard
140
- creation={item}
141
- callbacks={{
142
- onPress: () => handleView(item),
143
- onShare: async () => handleShare(item),
144
- onDelete: () => handleDelete(item),
145
- onFavorite: () => handleFavorite(item, !item.isFavorite),
146
- }}
147
- />
148
- ),
149
- [handleView, handleShare, handleDelete, handleFavorite]
150
- );
101
+ const filterButtons = useMemo(() => {
102
+ const buttons = [];
103
+ if (showStatusFilter) {
104
+ buttons.push({
105
+ id: "status",
106
+ label: t(config.translations.statusFilterTitle ?? "creations.filter.status"),
107
+ icon: "list-outline",
108
+ isActive: filters.statusFilter.hasActiveFilter,
109
+ onPress: filters.openStatusFilter,
110
+ });
111
+ }
112
+ if (showMediaFilter) {
113
+ buttons.push({
114
+ id: "media",
115
+ label: t(config.translations.mediaFilterTitle ?? "creations.filter.media"),
116
+ icon: "grid-outline",
117
+ isActive: filters.mediaFilter.hasActiveFilter,
118
+ onPress: filters.openMediaFilter,
119
+ });
120
+ }
121
+ return buttons;
122
+ }, [showStatusFilter, showMediaFilter, filters, t, config.translations]);
123
+
124
+ const renderItem = useCallback(({ item }: { item: Creation }) => (
125
+ <CreationCard
126
+ creation={item}
127
+ callbacks={{
128
+ onPress: () => setSelectedCreation(item),
129
+ onShare: async () => handleShare(item),
130
+ onDelete: () => handleDelete(item),
131
+ onFavorite: () => handleFavorite(item, !item.isFavorite),
132
+ }}
133
+ />
134
+ ), [handleShare, handleDelete, handleFavorite]);
151
135
 
152
136
  const renderHeader = useMemo(() => {
153
137
  if ((!creations || creations.length === 0) && !isLoading) return null;
154
-
155
138
  return (
156
- <View style={styles.header}>
139
+ <View style={[styles.header, { paddingTop: insets.top + tokens.spacing.md, backgroundColor: tokens.colors.surface, borderBottomColor: tokens.colors.border }]}>
157
140
  <GalleryHeader
158
141
  title={t(config.translations.title)}
159
- count={filtered.length}
142
+ count={filters.filtered.length}
160
143
  countLabel={t(config.translations.photoCount)}
161
- isFiltered={isFiltered}
162
144
  showFilter={showFilter}
163
- filterLabel={t(config.translations.filterLabel)}
164
- onFilterPress={() => {
165
- if (__DEV__) {
166
- // eslint-disable-next-line no-console
167
- console.log('[CreationsGallery] Filter button pressed');
168
- }
169
- filterSheetRef.current?.present();
170
- }}
145
+ filterButtons={filterButtons}
171
146
  />
172
147
  </View>
173
148
  );
174
- }, [creations, isLoading, filtered.length, isFiltered, showFilter, t, config, styles.header]);
149
+ }, [creations, isLoading, filters.filtered.length, showFilter, filterButtons, t, config, insets.top, tokens]);
175
150
 
176
151
  const renderEmpty = useMemo(() => (
177
152
  <GalleryEmptyStates
178
153
  isLoading={isLoading}
179
154
  creations={creations}
180
- isFiltered={isFiltered}
155
+ isFiltered={filters.isFiltered}
181
156
  tokens={tokens}
182
157
  t={t}
183
158
  emptyTitle={t(config.translations.empty)}
184
159
  emptyDescription={t(config.translations.emptyDescription)}
185
160
  emptyActionLabel={emptyActionLabel}
186
161
  onEmptyAction={onEmptyAction}
187
- onClearFilters={clearFilters}
162
+ onClearFilters={filters.clearAllFilters}
188
163
  />
189
- ), [isLoading, creations, isFiltered, tokens, t, config, emptyActionLabel, onEmptyAction, clearFilters]);
164
+ ), [isLoading, creations, filters.isFiltered, tokens, t, config, emptyActionLabel, onEmptyAction, filters.clearAllFilters]);
190
165
 
191
166
  if (selectedCreation) {
192
- return (
193
- <CreationDetailScreen
194
- creation={selectedCreation}
195
- onClose={() => setSelectedCreation(null)}
196
- onShare={handleShare}
197
- onDelete={handleDelete}
198
- t={t}
199
- />
200
- );
167
+ return <CreationDetailScreen creation={selectedCreation} onClose={() => setSelectedCreation(null)} onShare={handleShare} onDelete={handleDelete} t={t} />;
201
168
  }
202
169
 
203
170
  return (
204
171
  <View style={[styles.container, { backgroundColor: tokens.colors.background }]}>
205
172
  <FlatList
206
- data={filtered}
173
+ data={filters.filtered}
207
174
  renderItem={renderItem}
208
175
  keyExtractor={(item) => item.id}
209
176
  ListHeaderComponent={renderHeader}
210
177
  ListEmptyComponent={renderEmpty}
211
- contentContainerStyle={[
212
- styles.listContent,
213
- (!filtered || filtered.length === 0) && styles.emptyContent,
214
- ]}
178
+ contentContainerStyle={[styles.listContent, { paddingBottom: insets.bottom + 100 }, (!filters.filtered || filters.filtered.length === 0) && styles.emptyContent]}
215
179
  showsVerticalScrollIndicator={false}
216
- refreshControl={
217
- <RefreshControl
218
- refreshing={isLoading}
219
- onRefresh={() => void refetch()}
220
- tintColor={tokens.colors.primary}
221
- />
222
- }
223
- />
224
-
225
- <CreationImageViewer
226
- creations={filtered}
227
- visible={viewerVisible}
228
- index={viewerIndex}
229
- onDismiss={() => setViewerVisible(false)}
230
- onIndexChange={setViewerIndex}
231
- enableEditing={enableEditing}
232
- onImageEdit={onImageEdit}
233
- />
234
-
235
- <FilterBottomSheet
236
- ref={filterSheetRef}
237
- categories={allCategories}
238
- selectedIds={selectedIds}
239
- onFilterPress={(id, catId) => {
240
- const category = allCategories.find(c => c.id === catId);
241
- toggleFilter(id, category?.multiSelect);
242
- }}
243
- onClearFilters={clearFilters}
244
- title={t(config.translations.filterTitle)}
180
+ refreshControl={<RefreshControl refreshing={isLoading} onRefresh={() => void refetch()} tintColor={tokens.colors.primary} />}
245
181
  />
182
+ <CreationImageViewer creations={filters.filtered} visible={viewerVisible} index={viewerIndex} onDismiss={() => setViewerVisible(false)} onIndexChange={setViewerIndex} enableEditing={enableEditing} onImageEdit={onImageEdit} />
183
+ <FilterSheet visible={filters.statusFilterVisible} onClose={filters.closeStatusFilter} options={filters.statusFilter.filterOptions} selectedIds={[filters.statusFilter.selectedId]} onFilterPress={filters.statusFilter.selectFilter} onClearFilters={filters.statusFilter.clearFilter} title={t(config.translations.statusFilterTitle ?? "creations.filter.status")} clearLabel={t(config.translations.clearFilter ?? "common.clear")} />
184
+ <FilterSheet visible={filters.mediaFilterVisible} onClose={filters.closeMediaFilter} options={filters.mediaFilter.filterOptions} selectedIds={[filters.mediaFilter.selectedId]} onFilterPress={filters.mediaFilter.selectFilter} onClearFilters={filters.mediaFilter.clearFilter} title={t(config.translations.mediaFilterTitle ?? "creations.filter.media")} clearLabel={t(config.translations.clearFilter ?? "common.clear")} />
246
185
  </View>
247
186
  );
248
187
  }
249
188
 
250
- const useStyles = (tokens: DesignTokens, insets: { top: number; bottom: number }) =>
251
- StyleSheet.create({
252
- container: {
253
- flex: 1,
254
- },
255
- header: {
256
- paddingTop: insets.top + tokens.spacing.md,
257
- backgroundColor: tokens.colors.surface,
258
- borderBottomWidth: 1,
259
- borderBottomColor: tokens.colors.border,
260
- },
261
- listContent: {
262
- paddingHorizontal: tokens.spacing.md,
263
- paddingTop: tokens.spacing.md,
264
- paddingBottom: insets.bottom + 100,
265
- },
266
- emptyContent: {
267
- flexGrow: 1,
268
- },
269
- });
189
+ const styles = StyleSheet.create({
190
+ container: { flex: 1 },
191
+ header: { borderBottomWidth: 1 },
192
+ listContent: { paddingHorizontal: 16, paddingTop: 16 },
193
+ emptyContent: { flexGrow: 1 },
194
+ });
@@ -3,11 +3,39 @@
3
3
  * Provider-agnostic image-to-video generation feature
4
4
  */
5
5
 
6
- // Domain Types
6
+ // =============================================================================
7
+ // DOMAIN LAYER - Types
8
+ // =============================================================================
9
+
10
+ // Animation Types
11
+ export type { AnimationStyle, AnimationStyleId } from "./domain";
12
+
13
+ // Music Types
14
+ export type { MusicMood, MusicMoodId } from "./domain";
15
+
16
+ // Duration Types
17
+ export type { VideoDuration, DurationOption } from "./domain";
18
+
19
+ // Form Types
20
+ export type {
21
+ ImageToVideoFormState,
22
+ ImageToVideoFormActions,
23
+ ImageToVideoFormDefaults,
24
+ } from "./domain";
25
+
26
+ // Config Types
27
+ export type {
28
+ ImageToVideoCallbacks,
29
+ ImageToVideoFormConfig,
30
+ ImageToVideoTranslationsExtended,
31
+ } from "./domain";
32
+
33
+ // Core Feature Types
7
34
  export type {
8
35
  ImageToVideoOptions,
9
36
  ImageToVideoRequest,
10
37
  ImageToVideoResult,
38
+ ImageToVideoGenerationState,
11
39
  ImageToVideoFeatureState,
12
40
  ImageToVideoTranslations,
13
41
  ImageToVideoInputBuilder,
@@ -15,13 +43,72 @@ export type {
15
43
  ImageToVideoFeatureConfig,
16
44
  } from "./domain";
17
45
 
18
- // Infrastructure Services
46
+ // =============================================================================
47
+ // DOMAIN LAYER - Constants
48
+ // =============================================================================
49
+
50
+ export {
51
+ DEFAULT_ANIMATION_STYLES as IMAGE_TO_VIDEO_ANIMATION_STYLES,
52
+ DEFAULT_ANIMATION_STYLE_ID as IMAGE_TO_VIDEO_DEFAULT_ANIMATION,
53
+ DEFAULT_MUSIC_MOODS as IMAGE_TO_VIDEO_MUSIC_MOODS,
54
+ DEFAULT_MUSIC_MOOD_ID as IMAGE_TO_VIDEO_DEFAULT_MUSIC,
55
+ DEFAULT_DURATION_OPTIONS as IMAGE_TO_VIDEO_DURATION_OPTIONS,
56
+ DEFAULT_VIDEO_DURATION as IMAGE_TO_VIDEO_DEFAULT_DURATION,
57
+ DEFAULT_FORM_VALUES as IMAGE_TO_VIDEO_FORM_DEFAULTS,
58
+ DEFAULT_FORM_CONFIG as IMAGE_TO_VIDEO_CONFIG,
59
+ } from "./domain";
60
+
61
+ // =============================================================================
62
+ // INFRASTRUCTURE LAYER
63
+ // =============================================================================
64
+
19
65
  export { executeImageToVideo, hasImageToVideoSupport } from "./infrastructure";
20
66
  export type { ExecuteImageToVideoOptions } from "./infrastructure";
21
67
 
22
- // Presentation Hooks
68
+ // =============================================================================
69
+ // PRESENTATION LAYER - Hooks
70
+ // =============================================================================
71
+
72
+ export {
73
+ useImageToVideoFormState,
74
+ useImageToVideoGeneration,
75
+ useImageToVideoForm,
76
+ } from "./presentation";
77
+ export type {
78
+ UseImageToVideoFormStateOptions,
79
+ UseImageToVideoFormStateReturn,
80
+ UseImageToVideoGenerationOptions,
81
+ UseImageToVideoGenerationReturn,
82
+ UseImageToVideoFormOptions,
83
+ UseImageToVideoFormReturn,
84
+ } from "./presentation";
85
+
86
+ // Provider-based Feature Hook
23
87
  export { useImageToVideoFeature } from "./presentation";
24
88
  export type {
25
89
  UseImageToVideoFeatureProps,
26
90
  UseImageToVideoFeatureReturn,
27
91
  } from "./presentation";
92
+
93
+ // =============================================================================
94
+ // PRESENTATION LAYER - Components
95
+ // =============================================================================
96
+
97
+ export {
98
+ ImageToVideoAnimationStyleSelector,
99
+ ImageToVideoDurationSelector,
100
+ ImageToVideoMusicMoodSelector,
101
+ ImageToVideoSelectionGrid,
102
+ ImageToVideoHeroSection,
103
+ ImageToVideoGenerateButton,
104
+ } from "./presentation";
105
+
106
+ export type {
107
+ ImageToVideoAnimationStyleSelectorProps,
108
+ ImageToVideoDurationSelectorProps,
109
+ ImageToVideoMusicMoodSelectorProps,
110
+ ImageToVideoSelectionGridProps,
111
+ ImageToVideoSelectionGridTranslations,
112
+ ImageToVideoHeroSectionProps,
113
+ ImageToVideoGenerateButtonProps,
114
+ } from "./presentation";
@@ -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="Check" 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
+ });