@umituz/react-native-design-system 4.25.7 → 4.25.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.
Files changed (82) hide show
  1. package/package.json +1 -1
  2. package/src/atoms/AtomicAvatar.tsx +0 -5
  3. package/src/atoms/AtomicInput.tsx +0 -2
  4. package/src/atoms/AtomicProgress.tsx +0 -5
  5. package/src/atoms/AtomicTextArea.tsx +0 -4
  6. package/src/atoms/card/AtomicCard.tsx +111 -54
  7. package/src/atoms/input/types.ts +0 -2
  8. package/src/atoms/skeleton/AtomicSkeleton.tsx +2 -2
  9. package/src/image/presentation/components/ImageGallery.tsx +16 -15
  10. package/src/image/presentation/components/editor/StickerPickerSheet.tsx +4 -7
  11. package/src/index.ts +12 -55
  12. package/src/layouts/ScreenHeader/ScreenHeader.tsx +0 -2
  13. package/src/loading/presentation/providers/LoadingProvider.tsx +13 -4
  14. package/src/molecules/SearchBar/SearchBar.tsx +0 -2
  15. package/src/molecules/SearchBar/SearchSuggestions.tsx +1 -1
  16. package/src/molecules/SearchBar/types.ts +0 -1
  17. package/src/molecules/StepHeader/StepHeader.tsx +1 -1
  18. package/src/molecules/avatar/Avatar.tsx +76 -71
  19. package/src/molecules/avatar/AvatarGroup.tsx +1 -1
  20. package/src/molecules/calendar/presentation/components/CalendarDayCell.tsx +2 -3
  21. package/src/molecules/calendar/presentation/components/CalendarWeekdayHeader.tsx +1 -1
  22. package/src/molecules/countdown/components/Countdown.tsx +14 -11
  23. package/src/molecules/info-grid/InfoGrid.tsx +2 -2
  24. package/src/onboarding/presentation/components/BackgroundImageCollage.tsx +2 -2
  25. package/src/onboarding/presentation/components/OnboardingBackground.tsx +63 -49
  26. package/src/onboarding/presentation/components/OnboardingSlide.tsx +2 -2
  27. package/src/theme/infrastructure/providers/DesignSystemProvider.tsx +3 -1
  28. package/src/gallery/gallery-download.service.ts +0 -69
  29. package/src/gallery/gallery-save.service.ts +0 -80
  30. package/src/gallery/index.ts +0 -3
  31. package/src/gallery/types.ts +0 -11
  32. package/src/image/domain/entities/EditorTypes.ts +0 -23
  33. package/src/image/domain/entities/editor/EditorConfigTypes.ts +0 -35
  34. package/src/image/domain/entities/editor/EditorElementTypes.ts +0 -60
  35. package/src/image/domain/entities/editor/EditorFilterTypes.ts +0 -9
  36. package/src/image/domain/entities/editor/EditorLayerTypes.ts +0 -34
  37. package/src/image/domain/entities/editor/EditorStateTypes.ts +0 -35
  38. package/src/image/domain/entities/editor/EditorToolTypes.ts +0 -33
  39. package/src/image/infrastructure/services/ImageEditorService.ts +0 -134
  40. package/src/image/infrastructure/utils/ImageAnalysisUtils.ts +0 -120
  41. package/src/image/infrastructure/utils/ImageEditorHistoryUtils.ts +0 -63
  42. package/src/image/infrastructure/utils/LayerManager.ts +0 -65
  43. package/src/media/infrastructure/hooks/useGenericMediaGeneration.ts +0 -170
  44. package/src/molecules/ConfirmationModal.tsx +0 -42
  45. package/src/molecules/calendar/infrastructure/storage/CalendarStore.types.ts +0 -64
  46. package/src/molecules/calendar/infrastructure/storage/CalendarStore.utils.ts +0 -56
  47. package/src/molecules/calendar/infrastructure/storage/EventActions.ts +0 -140
  48. package/src/molecules/calendar/infrastructure/storage/NavigationActions.ts +0 -118
  49. package/src/molecules/calendar/presentation/hooks/useCalendar.ts +0 -185
  50. package/src/molecules/confirmation-modal/index.ts +0 -7
  51. package/src/molecules/listitem/index.ts +0 -6
  52. package/src/molecules/navigation/components/index.ts +0 -4
  53. package/src/molecules/navigation/utils/NavigationTheme.ts +0 -21
  54. package/src/presentation/utils/variants/compound.ts +0 -34
  55. package/src/services/api/ApiClient.ts +0 -180
  56. package/src/services/api/index.ts +0 -9
  57. package/src/services/api/types/ApiTypes.ts +0 -50
  58. package/src/services/api/utils/requestBuilder.ts +0 -92
  59. package/src/services/api/utils/responseHandler.ts +0 -130
  60. package/src/storage/cache/index.ts +0 -28
  61. package/src/theme/core/tokens/BorderRadius.ts +0 -16
  62. package/src/utilities/clipboard/ClipboardUtils.ts +0 -67
  63. package/src/utilities/clipboard/index.ts +0 -5
  64. package/src/utilities/index.ts +0 -6
  65. package/src/utilities/sharing/domain/entities/Share.ts +0 -104
  66. package/src/utilities/sharing/domain/entities/SharingUtils.ts +0 -111
  67. package/src/utilities/sharing/index.ts +0 -33
  68. package/src/utilities/sharing/infrastructure/services/SharingService.ts +0 -165
  69. package/src/utilities/sharing/presentation/hooks/useSharing.ts +0 -116
  70. package/src/utils/colorMapper.ts +0 -193
  71. package/src/utils/errors/adapters/CacheErrorAdapter.ts +0 -68
  72. package/src/utils/errors/adapters/ImageErrorAdapter.ts +0 -91
  73. package/src/utils/errors/adapters/StorageErrorAdapter.ts +0 -107
  74. package/src/utils/formatHelper.ts +0 -16
  75. package/src/utils/formatters/dateFormatter.ts +0 -64
  76. package/src/utils/formatters/numberFormatter.ts +0 -130
  77. package/src/utils/index.ts +0 -16
  78. package/src/utils/styleComposer.ts +0 -94
  79. package/src/utils/validationHelper.ts +0 -16
  80. package/src/utils/validators/dataValidators.ts +0 -111
  81. package/src/utils/validators/numericValidators.ts +0 -106
  82. package/src/utils/validators/stringValidators.ts +0 -85
@@ -13,41 +13,86 @@ import type { AvatarSize, AvatarShape } from './Avatar.types';
13
13
  import { SIZE_CONFIGS, AVATAR_CONSTANTS } from './Avatar.constants';
14
14
  import { AvatarUtils } from './Avatar.utils';
15
15
 
16
- /**
17
- * Avatar component props
18
- */
19
16
  export interface AvatarProps {
20
- /** Image URI */
21
17
  uri?: string;
22
- /** User name for initials */
23
18
  name?: string;
24
- /** Icon name (fallback when no image/name) */
25
19
  icon?: string;
26
- /** Size preset */
27
20
  size?: AvatarSize;
28
- /** Shape */
29
21
  shape?: AvatarShape;
30
- /** Custom background color */
31
22
  backgroundColor?: string;
32
- /** Show status indicator */
33
23
  showStatus?: boolean;
34
- /** Status (online/offline/away/busy) */
35
24
  status?: 'online' | 'offline' | 'away' | 'busy';
36
- /** Custom container style */
37
25
  style?: StyleProp<ViewStyle>;
38
- /** Custom image style */
39
26
  imageStyle?: StyleProp<ImageStyle>;
40
- /** OnPress handler */
41
27
  onPress?: () => void;
42
28
  }
43
29
 
44
- /**
45
- * Avatar Component
46
- * Displays user avatars with automatic fallback hierarchy:
47
- * 1. Image (if uri provided)
48
- * 2. Initials (if name provided)
49
- * 3. Icon (fallback)
50
- */
30
+ interface AvatarContentProps {
31
+ hasImage: boolean;
32
+ hasName: boolean;
33
+ uri?: string;
34
+ initials: string;
35
+ icon: string;
36
+ config: typeof SIZE_CONFIGS[AvatarSize];
37
+ borderRadius: number;
38
+ imageStyle?: StyleProp<ImageStyle>;
39
+ }
40
+
41
+ const AvatarContent: React.FC<AvatarContentProps> = ({
42
+ hasImage,
43
+ hasName,
44
+ uri,
45
+ initials,
46
+ icon,
47
+ config,
48
+ borderRadius,
49
+ imageStyle,
50
+ }) => {
51
+ const tokens = useAppDesignTokens();
52
+
53
+ if (hasImage) {
54
+ return (
55
+ <Image
56
+ source={{ uri }}
57
+ style={[
58
+ styles.image,
59
+ {
60
+ width: config.size,
61
+ height: config.size,
62
+ borderRadius,
63
+ },
64
+ imageStyle,
65
+ ]}
66
+ />
67
+ );
68
+ }
69
+
70
+ if (hasName) {
71
+ return (
72
+ <AtomicText
73
+ type="bodyMedium"
74
+ style={[
75
+ styles.initials,
76
+ {
77
+ fontSize: config.fontSize,
78
+ color: tokens.colors.textInverse,
79
+ },
80
+ ]}
81
+ >
82
+ {initials}
83
+ </AtomicText>
84
+ );
85
+ }
86
+
87
+ return (
88
+ <AtomicIcon
89
+ name={icon}
90
+ customSize={config.iconSize}
91
+ customColor={tokens.colors.textInverse}
92
+ />
93
+ );
94
+ };
95
+
51
96
  export const Avatar: React.FC<AvatarProps> = ({
52
97
  uri,
53
98
  name,
@@ -64,7 +109,6 @@ export const Avatar: React.FC<AvatarProps> = ({
64
109
  const tokens = useAppDesignTokens();
65
110
  const config = useMemo(() => SIZE_CONFIGS[size], [size]);
66
111
 
67
- // Determine avatar type and content
68
112
  const hasImage = !!uri;
69
113
  const hasName = !!name;
70
114
  const initials = useMemo(
@@ -80,57 +124,11 @@ export const Avatar: React.FC<AvatarProps> = ({
80
124
  [shape, config.size]
81
125
  );
82
126
 
83
- // Status indicator position
84
127
  const statusPosition = useMemo(() => ({
85
128
  bottom: 0,
86
129
  right: 0,
87
130
  }), []);
88
131
 
89
- const renderContent = () => {
90
- if (hasImage) {
91
- return (
92
- <Image
93
- source={{ uri }}
94
- style={[
95
- styles.image,
96
- {
97
- width: config.size,
98
- height: config.size,
99
- borderRadius,
100
- },
101
- imageStyle,
102
- ]}
103
- />
104
- );
105
- }
106
-
107
- if (hasName) {
108
- return (
109
- <AtomicText
110
- type="bodyMedium"
111
- style={[
112
- styles.initials,
113
- {
114
- fontSize: config.fontSize,
115
- color: tokens.colors.textInverse,
116
- },
117
- ]}
118
- >
119
- {initials}
120
- </AtomicText>
121
- );
122
- }
123
-
124
- // Fallback to icon
125
- return (
126
- <AtomicIcon
127
- name={icon}
128
- customSize={config.iconSize}
129
- customColor={tokens.colors.textInverse}
130
- />
131
- );
132
- };
133
-
134
132
  const AvatarWrapper = onPress ? TouchableOpacity : View;
135
133
 
136
134
  return (
@@ -151,9 +149,17 @@ export const Avatar: React.FC<AvatarProps> = ({
151
149
  accessibilityLabel={name || 'User avatar'}
152
150
  accessible={true}
153
151
  >
154
- {renderContent()}
152
+ <AvatarContent
153
+ hasImage={hasImage}
154
+ hasName={hasName}
155
+ uri={uri}
156
+ initials={initials}
157
+ icon={icon}
158
+ config={config}
159
+ borderRadius={borderRadius}
160
+ imageStyle={imageStyle}
161
+ />
155
162
 
156
- {/* Status Indicator */}
157
163
  {showStatus && (
158
164
  <View
159
165
  style={[
@@ -194,4 +200,3 @@ const styles = StyleSheet.create({
194
200
  position: 'absolute',
195
201
  },
196
202
  });
197
-
@@ -60,7 +60,7 @@ export const AvatarGroup: React.FC<AvatarGroupProps> = ({
60
60
  <View style={[styles.container, style]}>
61
61
  {visibleItems.map((item, index) => (
62
62
  <View
63
- key={index}
63
+ key={item.uri || item.name || item.icon}
64
64
  style={[
65
65
  styles.avatarWrapper,
66
66
  index > 0 && { marginLeft: spacing },
@@ -40,7 +40,6 @@ export const CalendarDayCell: React.FC<CalendarDayCellProps> = ({
40
40
 
41
41
  return (
42
42
  <TouchableOpacity
43
- key={index}
44
43
  style={[
45
44
  calendarStyles.dayCell,
46
45
  {
@@ -75,9 +74,9 @@ export const CalendarDayCell: React.FC<CalendarDayCellProps> = ({
75
74
  <View style={[calendarStyles.eventDot, { backgroundColor: tokens.colors.success }]} />
76
75
  )}
77
76
 
78
- {visibleEvents.map((event, eventIndex) => (
77
+ {visibleEvents.map((event) => (
79
78
  <View
80
- key={eventIndex}
79
+ key={event.id}
81
80
  style={[
82
81
  calendarStyles.eventDot,
83
82
  {
@@ -17,7 +17,7 @@ export const CalendarWeekdayHeader: React.FC<CalendarWeekdayHeaderProps> = ({
17
17
  return (
18
18
  <View style={calendarStyles.weekdayHeader}>
19
19
  {weekdayNames.map((day, index) => (
20
- <View key={index} style={calendarStyles.weekdayCell}>
20
+ <View key={day} style={calendarStyles.weekdayCell}>
21
21
  <AtomicText type="bodySmall" color="secondary" style={calendarStyles.weekdayText}>
22
22
  {day}
23
23
  </AtomicText>
@@ -1,4 +1,4 @@
1
- import React, { useMemo, useState, useEffect } from 'react';
1
+ import React, { useMemo, useState } from 'react';
2
2
  import { View, StyleSheet } from 'react-native';
3
3
  import { useAppDesignTokens } from '../../../theme';
4
4
  import { useCountdown } from '../hooks/useCountdown';
@@ -7,6 +7,9 @@ import { TimeUnit } from './TimeUnit';
7
7
  import type { CountdownTarget, CountdownDisplayConfig } from '../types/CountdownTypes';
8
8
  import type { IconName } from '../../../atoms';
9
9
 
10
+ const EMPTY_TARGETS: CountdownTarget[] = [];
11
+ const DEFAULT_DISPLAY_CONFIG: CountdownDisplayConfig = {};
12
+
10
13
  export interface CountdownProps {
11
14
  target: CountdownTarget;
12
15
  alternateTargets?: CountdownTarget[];
@@ -19,8 +22,8 @@ export interface CountdownProps {
19
22
 
20
23
  export const Countdown: React.FC<CountdownProps> = ({
21
24
  target,
22
- alternateTargets = [],
23
- displayConfig = {},
25
+ alternateTargets = EMPTY_TARGETS,
26
+ displayConfig = DEFAULT_DISPLAY_CONFIG,
24
27
  interval = 1000,
25
28
  onExpire,
26
29
  onTargetChange,
@@ -49,17 +52,12 @@ export const Countdown: React.FC<CountdownProps> = ({
49
52
  onExpire,
50
53
  });
51
54
 
52
- useEffect(() => {
53
- if (currentTarget) {
54
- updateTarget(currentTarget);
55
- }
56
- }, [currentTarget, updateTarget]);
57
-
58
55
  const handleToggle = () => {
59
56
  const nextIndex = (currentTargetIndex + 1) % allTargets.length;
60
57
  const nextTarget = allTargets[nextIndex];
61
58
  if (nextTarget) {
62
59
  setCurrentTargetIndex(nextIndex);
60
+ updateTarget(nextTarget);
63
61
  onTargetChange?.(nextTarget);
64
62
  }
65
63
  };
@@ -78,6 +76,7 @@ export const Countdown: React.FC<CountdownProps> = ({
78
76
 
79
77
  const timeUnits = useMemo(() => {
80
78
  interface CountdownUnit {
79
+ key: string;
81
80
  value: number;
82
81
  label: string;
83
82
  }
@@ -87,24 +86,28 @@ export const Countdown: React.FC<CountdownProps> = ({
87
86
 
88
87
  if (shouldShowDays) {
89
88
  units.push({
89
+ key: 'days',
90
90
  value: timeRemaining.days,
91
91
  label: labelFormatter('days', timeRemaining.days)
92
92
  });
93
93
  }
94
94
  if (showHours) {
95
95
  units.push({
96
+ key: 'hours',
96
97
  value: timeRemaining.hours,
97
98
  label: labelFormatter('hours', timeRemaining.hours)
98
99
  });
99
100
  }
100
101
  if (showMinutes) {
101
102
  units.push({
103
+ key: 'minutes',
102
104
  value: timeRemaining.minutes,
103
105
  label: labelFormatter('minutes', timeRemaining.minutes)
104
106
  });
105
107
  }
106
108
  if (showSeconds) {
107
109
  units.push({
110
+ key: 'seconds',
108
111
  value: timeRemaining.seconds,
109
112
  label: labelFormatter('seconds', timeRemaining.seconds)
110
113
  });
@@ -125,9 +128,9 @@ export const Countdown: React.FC<CountdownProps> = ({
125
128
  )}
126
129
 
127
130
  <View style={[styles.grid, { gap: tokens.spacing.sm }]}>
128
- {timeUnits.map((unit, index) => (
131
+ {timeUnits.map((unit) => (
129
132
  <TimeUnit
130
- key={index}
133
+ key={unit.key}
131
134
  value={unit.value}
132
135
  label={unit.label}
133
136
  size={size}
@@ -84,8 +84,8 @@ export const InfoGrid: React.FC<InfoGridProps> = ({
84
84
  )}
85
85
 
86
86
  <View style={styles.grid}>
87
- {items.map((item, index) => (
88
- <View key={index} style={[styles.item, itemStyle]}>
87
+ {items.map((item) => (
88
+ <View key={item.text} style={[styles.item, itemStyle]}>
89
89
  {item.icon && (
90
90
  <View style={styles.iconContainer}>
91
91
  <AtomicIcon name={item.icon} size="xs" color="primary" />
@@ -76,9 +76,9 @@ export const BackgroundImageCollage: React.FC<BackgroundImageCollageProps> = ({
76
76
 
77
77
  return (
78
78
  <View style={[StyleSheet.absoluteFill, { opacity }]} pointerEvents="none">
79
- {imageLayouts.map((item, index) => (
79
+ {imageLayouts.map((item) => (
80
80
  <Image
81
- key={index}
81
+ key={String(item.source)}
82
82
  source={item.source}
83
83
  style={item.style}
84
84
  contentFit="cover"
@@ -10,6 +10,64 @@ import { BackgroundVideo } from "./BackgroundVideo";
10
10
  import { BackgroundImageCollage } from "./BackgroundImageCollage";
11
11
  import { AtomicImage } from "../../../atoms/image/AtomicImage";
12
12
 
13
+ interface BackgroundContentProps {
14
+ slide: OnboardingSlide;
15
+ useCustomBackground: boolean;
16
+ overlayOpacity: number;
17
+ }
18
+
19
+ const BackgroundContent: React.FC<BackgroundContentProps> = ({
20
+ slide,
21
+ useCustomBackground,
22
+ overlayOpacity,
23
+ }) => {
24
+ if (slide.backgroundVideo) {
25
+ return (
26
+ <BackgroundVideo
27
+ source={slide.backgroundVideo}
28
+ overlayOpacity={overlayOpacity}
29
+ />
30
+ );
31
+ }
32
+
33
+ if (slide.backgroundImages && slide.backgroundImages.length > 0) {
34
+ return (
35
+ <BackgroundImageCollage
36
+ images={slide.backgroundImages}
37
+ layout={slide.backgroundImagesLayout || "grid"}
38
+ columns={slide.backgroundImagesColumns}
39
+ gap={slide.backgroundImagesGap}
40
+ borderRadius={slide.backgroundImagesBorderRadius}
41
+ />
42
+ );
43
+ }
44
+
45
+ if (slide.backgroundImage) {
46
+ return (
47
+ <AtomicImage
48
+ source={slide.backgroundImage}
49
+ style={StyleSheet.absoluteFill}
50
+ contentFit="cover"
51
+ cachePolicy="memory-disk"
52
+ priority="high"
53
+ />
54
+ );
55
+ }
56
+
57
+ if (useCustomBackground && slide.backgroundColor) {
58
+ return (
59
+ <View
60
+ style={[
61
+ StyleSheet.absoluteFill,
62
+ { backgroundColor: slide.backgroundColor }
63
+ ]}
64
+ />
65
+ );
66
+ }
67
+
68
+ return null;
69
+ };
70
+
13
71
  interface OnboardingBackgroundProps {
14
72
  currentSlide: OnboardingSlide | undefined;
15
73
  useCustomBackground: boolean;
@@ -25,57 +83,13 @@ export const OnboardingBackground: React.FC<OnboardingBackgroundProps> = ({
25
83
  }) => {
26
84
  if (!currentSlide) return null;
27
85
 
28
- const renderContent = () => {
29
- if (currentSlide.backgroundVideo) {
30
- return (
31
- <BackgroundVideo
32
- source={currentSlide.backgroundVideo}
33
- overlayOpacity={overlayOpacity}
34
- />
35
- );
36
- }
37
-
38
- if (currentSlide.backgroundImages && currentSlide.backgroundImages.length > 0) {
39
- return (
40
- <BackgroundImageCollage
41
- images={currentSlide.backgroundImages}
42
- layout={currentSlide.backgroundImagesLayout || "grid"}
43
- columns={currentSlide.backgroundImagesColumns}
44
- gap={currentSlide.backgroundImagesGap}
45
- borderRadius={currentSlide.backgroundImagesBorderRadius}
46
- />
47
- );
48
- }
49
-
50
- if (currentSlide.backgroundImage) {
51
- return (
52
- <AtomicImage
53
- source={currentSlide.backgroundImage}
54
- style={StyleSheet.absoluteFill}
55
- contentFit="cover"
56
- cachePolicy="memory-disk"
57
- priority="high"
58
- />
59
- );
60
- }
61
-
62
- if (useCustomBackground && currentSlide.backgroundColor) {
63
- return (
64
- <View
65
- style={[
66
- StyleSheet.absoluteFill,
67
- { backgroundColor: currentSlide.backgroundColor }
68
- ]}
69
- />
70
- );
71
- }
72
-
73
- return null;
74
- };
75
-
76
86
  return (
77
87
  <View style={StyleSheet.absoluteFill} pointerEvents="none">
78
- {renderContent()}
88
+ <BackgroundContent
89
+ slide={currentSlide}
90
+ useCustomBackground={useCustomBackground}
91
+ overlayOpacity={overlayOpacity}
92
+ />
79
93
  <View
80
94
  style={[
81
95
  StyleSheet.absoluteFill,
@@ -64,9 +64,9 @@ export const OnboardingSlide = ({
64
64
 
65
65
  {slide.features && slide.features.length > 0 && (
66
66
  <View style={styles.features}>
67
- {slide.features.map((feature, index) => (
67
+ {slide.features.map((feature) => (
68
68
  <View
69
- key={index}
69
+ key={feature}
70
70
  style={[
71
71
  styles.featureItem,
72
72
  { backgroundColor: colors.featureItemBg },
@@ -14,6 +14,8 @@ import type { IconRenderer, IconNames } from '../../../atoms/icon/iconStore';
14
14
  const SplashScreen = lazy(() => import('../../../molecules/splash').then(m => ({ default: m.SplashScreen })));
15
15
 
16
16
 
17
+ const EMPTY_FONTS: Record<string, any> = {};
18
+
17
19
  interface DesignSystemProviderProps {
18
20
  children: ReactNode;
19
21
  customColors?: CustomThemeColors;
@@ -44,7 +46,7 @@ export const DesignSystemProvider: React.FC<DesignSystemProviderProps> = ({
44
46
  iconNames,
45
47
  }) => {
46
48
  const [isInitialized, setIsInitialized] = useState(false);
47
- const [fontsLoaded, fontError] = fonts ? useFonts(fonts) : [true, null];
49
+ const [fontsLoaded, fontError] = useFonts(fonts ?? EMPTY_FONTS);
48
50
 
49
51
  const initialize = useTheme((state) => state.initialize);
50
52
  const setCustomColors = useTheme((state) => state.setCustomColors);
@@ -1,69 +0,0 @@
1
- /**
2
- * Gallery Download Service
3
- * Single Responsibility: Download remote media to local storage
4
- */
5
-
6
- import { FileSystemService } from "../filesystem";
7
- import { validateImageUri, getFileExtension } from "../image";
8
- import { timezoneService } from "../timezone";
9
- import type { DownloadMediaResult } from "./types";
10
-
11
- const generateFilename = (uri: string, prefix: string): string => {
12
- const extension = getFileExtension(uri) || "jpg";
13
- const timestamp = timezoneService.formatDateToString(new Date());
14
- const randomId = Math.random().toString(36).substring(2, 10);
15
- return `${prefix}_${timestamp}_${randomId}.${extension}`;
16
- };
17
-
18
- class GalleryDownloadService {
19
- async downloadMedia(
20
- mediaUri: string,
21
- prefix: string = "media",
22
- ): Promise<DownloadMediaResult> {
23
- try {
24
- const validationResult = validateImageUri(mediaUri, "Media");
25
- if (!validationResult.isValid) {
26
- return {
27
- success: false,
28
- error: validationResult.error || "Invalid media file",
29
- };
30
- }
31
-
32
- const filename = generateFilename(mediaUri, prefix);
33
- const documentDir = FileSystemService.getDocumentDirectory();
34
- const fileUri = `${documentDir}${filename}`;
35
-
36
- const downloadResult = await FileSystemService.downloadFile(
37
- mediaUri,
38
- fileUri,
39
- );
40
-
41
- if (!downloadResult.success || !downloadResult.uri) {
42
- return {
43
- success: false,
44
- error: downloadResult.error || "Download failed",
45
- };
46
- }
47
-
48
- return {
49
- success: true,
50
- localUri: downloadResult.uri,
51
- };
52
- } catch (error) {
53
- return {
54
- success: false,
55
- error: error instanceof Error ? error.message : "Download failed",
56
- };
57
- }
58
- }
59
-
60
- isRemoteUrl(uri: string): boolean {
61
- return uri.startsWith("http://") || uri.startsWith("https://");
62
- }
63
-
64
- async cleanupFile(fileUri: string): Promise<void> {
65
- await FileSystemService.deleteFile(fileUri);
66
- }
67
- }
68
-
69
- export const galleryDownloadService = new GalleryDownloadService();
@@ -1,80 +0,0 @@
1
- /**
2
- * Gallery Save Service
3
- * Single Responsibility: Save media to device gallery
4
- */
5
-
6
- import * as MediaLibrary from "expo-media-library";
7
- import { validateImageUri } from "../image";
8
- import { galleryDownloadService } from "./gallery-download.service";
9
- import type { SaveMediaResult } from "./types";
10
-
11
- const requestMediaPermissions = async (): Promise<boolean> => {
12
- try {
13
- const { status } = await MediaLibrary.requestPermissionsAsync();
14
- return status === "granted";
15
- } catch {
16
- return false;
17
- }
18
- };
19
-
20
- class GallerySaveService {
21
- async saveToGallery(
22
- mediaUri: string,
23
- prefix?: string,
24
- ): Promise<SaveMediaResult> {
25
- try {
26
- const validationResult = validateImageUri(mediaUri, "Media");
27
- if (!validationResult.isValid) {
28
- return {
29
- success: false,
30
- error: validationResult.error || "Invalid media file",
31
- };
32
- }
33
-
34
- const hasPermission = await requestMediaPermissions();
35
- if (!hasPermission) {
36
- return {
37
- success: false,
38
- error: "Media library permission denied",
39
- };
40
- }
41
-
42
- let localUri = mediaUri;
43
- const isRemote = galleryDownloadService.isRemoteUrl(mediaUri);
44
-
45
- if (isRemote) {
46
- const downloadResult = await galleryDownloadService.downloadMedia(
47
- mediaUri,
48
- prefix,
49
- );
50
-
51
- if (!downloadResult.success || !downloadResult.localUri) {
52
- return {
53
- success: false,
54
- error: downloadResult.error || "Download failed",
55
- };
56
- }
57
-
58
- localUri = downloadResult.localUri;
59
- }
60
-
61
- const asset = await MediaLibrary.createAssetAsync(localUri);
62
-
63
- if (isRemote && localUri) {
64
- await galleryDownloadService.cleanupFile(localUri);
65
- }
66
-
67
- return {
68
- success: true,
69
- fileUri: asset.uri,
70
- };
71
- } catch (error) {
72
- return {
73
- success: false,
74
- error: error instanceof Error ? error.message : "Save failed",
75
- };
76
- }
77
- }
78
- }
79
-
80
- export const gallerySaveService = new GallerySaveService();
@@ -1,3 +0,0 @@
1
- export { gallerySaveService } from "./gallery-save.service";
2
- export { galleryDownloadService } from "./gallery-download.service";
3
- export type { SaveMediaResult, DownloadMediaResult } from "./types";
@@ -1,11 +0,0 @@
1
- export interface SaveMediaResult {
2
- success: boolean;
3
- fileUri?: string;
4
- error?: string;
5
- }
6
-
7
- export interface DownloadMediaResult {
8
- success: boolean;
9
- localUri?: string;
10
- error?: string;
11
- }