@umituz/react-native-design-system 2.9.58 → 2.9.60

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-design-system",
3
- "version": "2.9.58",
3
+ "version": "2.9.60",
4
4
  "description": "Universal design system for React Native apps - Consolidated package with atoms, molecules, organisms, theme, typography, responsive, safe area, exception, infinite scroll, UUID, image, timezone, offline, onboarding, and loading utilities",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -66,7 +66,6 @@
66
66
  "@expo/vector-icons": ">=15.0.0",
67
67
  "@react-native-async-storage/async-storage": ">=1.18.0",
68
68
  "@react-native-community/datetimepicker": ">=8.0.0",
69
- "@react-native-community/slider": ">=4.0.0",
70
69
  "@react-navigation/bottom-tabs": ">=7.0.0",
71
70
  "@react-navigation/native": ">=7.0.0",
72
71
  "@react-navigation/stack": ">=7.0.0",
@@ -82,9 +81,7 @@
82
81
  "expo-device": ">=5.0.0",
83
82
  "expo-font": ">=12.0.0",
84
83
  "expo-image": ">=3.0.0",
85
- "expo-image-manipulator": ">=12.0.0",
86
84
  "expo-image-picker": ">=14.0.0",
87
- "expo-media-library": ">=15.0.0",
88
85
  "expo-network": ">=8.0.0",
89
86
  "expo-secure-store": ">=14.0.0",
90
87
  "expo-sharing": ">=12.0.0",
@@ -92,7 +89,6 @@
92
89
  "react": ">=19.0.0",
93
90
  "react-native": ">=0.81.0",
94
91
  "react-native-gesture-handler": ">=2.20.0",
95
- "react-native-haptic-feedback": ">=2.0.0",
96
92
  "react-native-safe-area-context": ">=5.0.0",
97
93
  "react-native-svg": ">=15.0.0",
98
94
  "rn-emoji-keyboard": ">=1.7.0",
@@ -111,7 +107,6 @@
111
107
  "@expo/vector-icons": "^15.0.0",
112
108
  "@react-native-async-storage/async-storage": "^2.2.0",
113
109
  "@react-native-community/datetimepicker": "^8.5.1",
114
- "@react-native-community/slider": "^4.5.5",
115
110
  "@react-navigation/bottom-tabs": "^7.9.0",
116
111
  "@react-navigation/native": "^7.1.26",
117
112
  "@react-navigation/stack": "^7.6.13",
@@ -134,27 +129,21 @@
134
129
  "eslint-plugin-react-native": "^5.0.0",
135
130
  "expo-application": "~5.9.1",
136
131
  "expo-clipboard": "~8.0.7",
137
- "expo-crypto": "~14.0.0",
132
+ "expo-crypto": "~14.0.8",
138
133
  "expo-device": "~7.0.2",
139
134
  "expo-file-system": "^19.0.21",
140
- "expo-font": "~14.0.0",
141
- "expo-haptics": "~14.0.0",
135
+ "expo-font": "~14.0.9",
142
136
  "expo-image": "~3.0.11",
143
- "expo-image-manipulator": "~13.0.0",
144
- "expo-image-picker": "~16.0.0",
145
- "expo-localization": "~16.0.1",
146
- "expo-media-library": "~17.0.0",
147
- "expo-modules-core": "^3.0.29",
137
+ "expo-localization": "~17.0.7",
148
138
  "expo-network": "~8.0.0",
149
- "expo-secure-store": "~14.0.0",
139
+ "expo-secure-store": "~14.0.8",
150
140
  "expo-sharing": "~14.0.8",
151
- "expo-video": "~3.0.0",
141
+ "expo-video": "~3.0.15",
152
142
  "i18next": "^25.0.0",
153
143
  "react": "19.1.0",
154
144
  "react-i18next": "^16.0.0",
155
145
  "react-native": "0.81.5",
156
146
  "react-native-gesture-handler": "^2.20.0",
157
- "react-native-haptic-feedback": "^2.3.3",
158
147
  "react-native-safe-area-context": "^5.6.0",
159
148
  "react-native-svg": "15.12.1",
160
149
  "rn-emoji-keyboard": "^1.7.0",
@@ -9,11 +9,8 @@ export const AtomicImage: React.FC<AtomicImageProps> = ({
9
9
  style,
10
10
  rounded,
11
11
  contentFit = 'cover',
12
- transition = 300,
13
12
  ...props
14
13
  }) => {
15
-
16
-
17
14
  return (
18
15
  <ExpoImage
19
16
  style={[
@@ -21,7 +18,6 @@ export const AtomicImage: React.FC<AtomicImageProps> = ({
21
18
  rounded && { borderRadius: 9999 }
22
19
  ]}
23
20
  contentFit={contentFit}
24
- transition={transition}
25
21
  {...props}
26
22
  />
27
23
  );
@@ -62,7 +62,6 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
62
62
  source={{ uri: item.uri }}
63
63
  style={styles.fullImage}
64
64
  contentFit="contain"
65
- transition={0}
66
65
  cachePolicy="memory-disk"
67
66
  />
68
67
  </View>
@@ -2,60 +2,111 @@
2
2
  * Presentation - Text Editor Tabs
3
3
  */
4
4
 
5
- import React from 'react';
6
- import { View, TextInput, ScrollView, TouchableOpacity } from 'react-native';
7
- import Slider from '@react-native-community/slider';
8
- import { AtomicText } from '../../../../atoms/AtomicText';
9
- import { AtomicIcon } from '../../../../atoms/AtomicIcon';
10
- import { useAppDesignTokens } from '../../../../theme/hooks/useAppDesignTokens';
5
+ import React from "react";
6
+ import {
7
+ View,
8
+ TextInput,
9
+ ScrollView,
10
+ TouchableOpacity,
11
+ StyleSheet,
12
+ } from "react-native";
13
+ import { AtomicText } from "../../../../atoms/AtomicText";
14
+ import { AtomicIcon } from "../../../../atoms/AtomicIcon";
15
+ import { useAppDesignTokens } from "../../../../theme/hooks/useAppDesignTokens";
11
16
 
12
17
  interface TabProps {
13
18
  t: (key: string) => string;
14
19
  }
15
20
 
16
- export const TextContentTab: React.FC<TabProps & { text: string; onTextChange: (t: string) => void }> = ({ text, onTextChange, t: translate }) => {
21
+ export const TextContentTab: React.FC<
22
+ TabProps & { text: string; onTextChange: (t: string) => void }
23
+ > = ({ text, onTextChange, t }) => {
17
24
  const tokens = useAppDesignTokens();
18
25
  return (
19
26
  <View style={{ gap: tokens.spacing.lg }}>
20
27
  <TextInput
21
28
  value={text}
22
29
  onChangeText={onTextChange}
23
- placeholder={translate('editor.text_placeholder')}
24
- style={{
25
- ...tokens.typography.bodyLarge,
26
- borderWidth: 1,
27
- borderColor: tokens.colors.border,
28
- borderRadius: tokens.radius.md,
29
- padding: tokens.spacing.md,
30
- minHeight: 120,
31
- textAlignVertical: 'top',
32
- }}
30
+ placeholder={t("editor.text_placeholder")}
31
+ style={[
32
+ styles.textInput,
33
+ {
34
+ ...tokens.typography.bodyLarge,
35
+ borderColor: tokens.colors.border,
36
+ borderRadius: tokens.borders.radius.md,
37
+ padding: tokens.spacing.md,
38
+ minHeight: 120,
39
+ color: tokens.colors.textPrimary,
40
+ },
41
+ ]}
33
42
  multiline
34
43
  />
35
44
  </View>
36
45
  );
37
46
  };
38
47
 
39
- export const TextStyleTab: React.FC<TabProps & {
40
- fontSize: number; setFontSize: (s: number) => void;
41
- color: string; setColor: (c: string) => void;
42
- fontFamily: string; setFontFamily: (f: string) => void;
43
- }> = ({ fontSize, setFontSize, color, setColor, fontFamily, setFontFamily }) => {
48
+ export const TextStyleTab: React.FC<
49
+ TabProps & {
50
+ fontSize: number;
51
+ setFontSize: (s: number) => void;
52
+ color: string;
53
+ setColor: (c: string) => void;
54
+ fontFamily: string;
55
+ setFontFamily: (f: string) => void;
56
+ }
57
+ > = ({ fontSize, setFontSize, color, setColor, fontFamily, setFontFamily }) => {
44
58
  const tokens = useAppDesignTokens();
45
- const colors = ['#FFFFFF', '#000000', '#FF0000', '#FFFF00', '#0000FF', '#00FF00', '#FF00FF', '#FFA500'];
46
- const fonts = ['System', 'serif', 'sans-serif', 'monospace'];
59
+ const colors = [
60
+ "#FFFFFF",
61
+ "#000000",
62
+ "#FF0000",
63
+ "#FFFF00",
64
+ "#0000FF",
65
+ "#00FF00",
66
+ "#FF00FF",
67
+ "#FFA500",
68
+ ];
69
+ const fonts = ["System", "serif", "sans-serif", "monospace"];
70
+ const fontSizes = [12, 14, 16, 18, 20, 24, 28, 32];
47
71
 
48
72
  return (
49
73
  <View style={{ gap: tokens.spacing.xl }}>
50
74
  <View>
51
- <AtomicText style={{ ...tokens.typography.labelMedium, marginBottom: tokens.spacing.sm }}>Font</AtomicText>
52
- <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: tokens.spacing.sm }}>
53
- {fonts.map(f => (
54
- <TouchableOpacity key={f} onPress={() => setFontFamily(f)} style={{
55
- paddingHorizontal: tokens.spacing.md, paddingVertical: tokens.spacing.xs, borderRadius: tokens.radius.full,
56
- borderWidth: 1, borderColor: fontFamily === f ? tokens.colors.primary : tokens.colors.border,
57
- backgroundColor: fontFamily === f ? tokens.colors.primaryContainer : tokens.colors.surface
58
- }}>
75
+ <AtomicText
76
+ style={{
77
+ ...tokens.typography.labelMedium,
78
+ marginBottom: tokens.spacing.sm,
79
+ }}
80
+ >
81
+ Font
82
+ </AtomicText>
83
+ <ScrollView
84
+ horizontal
85
+ showsHorizontalScrollIndicator={false}
86
+ contentContainerStyle={{ gap: tokens.spacing.sm }}
87
+ >
88
+ {fonts.map((f) => (
89
+ <TouchableOpacity
90
+ key={f}
91
+ onPress={() => setFontFamily(f)}
92
+ style={[
93
+ styles.fontButton,
94
+ {
95
+ paddingHorizontal: tokens.spacing.md,
96
+ paddingVertical: tokens.spacing.xs,
97
+ borderRadius: tokens.borders.radius.full,
98
+ borderWidth: 1,
99
+ borderColor:
100
+ fontFamily === f
101
+ ? tokens.colors.primary
102
+ : tokens.colors.border,
103
+ backgroundColor:
104
+ fontFamily === f
105
+ ? tokens.colors.primary
106
+ : tokens.colors.surface,
107
+ },
108
+ ]}
109
+ >
59
110
  <AtomicText style={{ fontFamily: f }}>{f}</AtomicText>
60
111
  </TouchableOpacity>
61
112
  ))}
@@ -63,55 +114,312 @@ export const TextStyleTab: React.FC<TabProps & {
63
114
  </View>
64
115
 
65
116
  <View>
66
- <AtomicText style={{ ...tokens.typography.labelMedium, marginBottom: tokens.spacing.sm }}>Color</AtomicText>
67
- <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: tokens.spacing.sm }}>
68
- {colors.map(c => (
69
- <TouchableOpacity key={c} onPress={() => setColor(c)} style={{
70
- width: 40, height: 40, borderRadius: 20, backgroundColor: c,
71
- borderWidth: color === c ? 3 : 1, borderColor: tokens.colors.primary
72
- }} />
117
+ <AtomicText
118
+ style={{
119
+ ...tokens.typography.labelMedium,
120
+ marginBottom: tokens.spacing.sm,
121
+ }}
122
+ >
123
+ Color
124
+ </AtomicText>
125
+ <ScrollView
126
+ horizontal
127
+ showsHorizontalScrollIndicator={false}
128
+ contentContainerStyle={{ gap: tokens.spacing.sm }}
129
+ >
130
+ {colors.map((c) => (
131
+ <TouchableOpacity
132
+ key={c}
133
+ onPress={() => setColor(c)}
134
+ style={[
135
+ styles.colorButton,
136
+ {
137
+ width: 40,
138
+ height: 40,
139
+ borderRadius: 20,
140
+ backgroundColor: c,
141
+ borderWidth: color === c ? 3 : 1,
142
+ borderColor: tokens.colors.primary,
143
+ },
144
+ ]}
145
+ />
73
146
  ))}
74
147
  </ScrollView>
75
148
  </View>
76
149
 
77
150
  <View>
78
- <AtomicText style={{ ...tokens.typography.labelMedium, marginBottom: tokens.spacing.xs }}>Size: {fontSize}px</AtomicText>
79
- <Slider value={fontSize} onValueChange={setFontSize} minimumValue={12} maximumValue={100} step={1} minimumTrackTintColor={tokens.colors.primary} />
151
+ <AtomicText
152
+ style={{
153
+ ...tokens.typography.labelMedium,
154
+ marginBottom: tokens.spacing.xs,
155
+ }}
156
+ >
157
+ Size: {fontSize}px
158
+ </AtomicText>
159
+ <ScrollView
160
+ horizontal
161
+ showsHorizontalScrollIndicator={false}
162
+ contentContainerStyle={{ gap: tokens.spacing.sm }}
163
+ >
164
+ {fontSizes.map((s) => (
165
+ <TouchableOpacity
166
+ key={s}
167
+ onPress={() => setFontSize(s)}
168
+ style={[
169
+ styles.sizeButton,
170
+ {
171
+ paddingHorizontal: tokens.spacing.md,
172
+ paddingVertical: tokens.spacing.sm,
173
+ borderRadius: tokens.borders.radius.md,
174
+ borderWidth: 1,
175
+ borderColor:
176
+ fontSize === s
177
+ ? tokens.colors.primary
178
+ : tokens.colors.border,
179
+ backgroundColor:
180
+ fontSize === s
181
+ ? tokens.colors.primary
182
+ : tokens.colors.surface,
183
+ },
184
+ ]}
185
+ >
186
+ <AtomicText
187
+ style={{
188
+ color: fontSize === s ? "white" : tokens.colors.textPrimary,
189
+ fontWeight: "600",
190
+ }}
191
+ >
192
+ {s}
193
+ </AtomicText>
194
+ </TouchableOpacity>
195
+ ))}
196
+ </ScrollView>
80
197
  </View>
81
198
  </View>
82
199
  );
83
200
  };
84
201
 
85
- export const TextTransformTab: React.FC<TabProps & {
86
- scale: number; setScale: (s: number) => void;
87
- rotation: number; setRotation: (r: number) => void;
88
- opacity: number; setOpacity: (o: number) => void;
89
- onDelete?: () => void;
90
- }> = ({ scale, setScale, rotation, setRotation, opacity, setOpacity, onDelete }) => {
202
+ export const TextTransformTab: React.FC<
203
+ TabProps & {
204
+ scale: number;
205
+ setScale: (s: number) => void;
206
+ rotation: number;
207
+ setRotation: (r: number) => void;
208
+ opacity: number;
209
+ setOpacity: (o: number) => void;
210
+ onDelete?: () => void;
211
+ }
212
+ > = ({
213
+ scale,
214
+ setScale,
215
+ rotation,
216
+ setRotation,
217
+ opacity,
218
+ setOpacity,
219
+ onDelete,
220
+ }) => {
91
221
  const tokens = useAppDesignTokens();
222
+ const scales = [0.5, 0.75, 1, 1.25, 1.5, 2];
223
+ const rotations = [0, 45, 90, 135, 180, 225, 270, 315];
224
+ const opacities = [0.2, 0.4, 0.6, 0.8, 1];
225
+
92
226
  return (
93
227
  <View style={{ gap: tokens.spacing.xl }}>
94
228
  <View>
95
- <AtomicText style={{ ...tokens.typography.labelMedium, marginBottom: tokens.spacing.xs }}>Scale: {scale.toFixed(2)}x</AtomicText>
96
- <Slider value={scale} onValueChange={setScale} minimumValue={0.2} maximumValue={3} step={0.1} minimumTrackTintColor={tokens.colors.primary} />
229
+ <AtomicText
230
+ style={{
231
+ ...tokens.typography.labelMedium,
232
+ marginBottom: tokens.spacing.xs,
233
+ }}
234
+ >
235
+ Scale: {scale.toFixed(2)}x
236
+ </AtomicText>
237
+ <ScrollView
238
+ horizontal
239
+ showsHorizontalScrollIndicator={false}
240
+ contentContainerStyle={{ gap: tokens.spacing.sm }}
241
+ >
242
+ {scales.map((s) => (
243
+ <TouchableOpacity
244
+ key={s}
245
+ onPress={() => setScale(s)}
246
+ style={[
247
+ styles.transformButton,
248
+ {
249
+ paddingHorizontal: tokens.spacing.md,
250
+ paddingVertical: tokens.spacing.sm,
251
+ borderRadius: tokens.borders.radius.md,
252
+ borderWidth: 1,
253
+ borderColor:
254
+ scale === s ? tokens.colors.primary : tokens.colors.border,
255
+ backgroundColor:
256
+ scale === s ? tokens.colors.primary : tokens.colors.surface,
257
+ },
258
+ ]}
259
+ >
260
+ <AtomicText
261
+ style={{
262
+ color: scale === s ? "white" : tokens.colors.textPrimary,
263
+ }}
264
+ >
265
+ {s.toFixed(1)}x
266
+ </AtomicText>
267
+ </TouchableOpacity>
268
+ ))}
269
+ </ScrollView>
97
270
  </View>
271
+
98
272
  <View>
99
- <AtomicText style={{ ...tokens.typography.labelMedium, marginBottom: tokens.spacing.xs }}>Rotation: {Math.round(rotation)}°</AtomicText>
100
- <Slider value={rotation} onValueChange={setRotation} minimumValue={0} maximumValue={360} step={1} minimumTrackTintColor={tokens.colors.primary} />
273
+ <AtomicText
274
+ style={{
275
+ ...tokens.typography.labelMedium,
276
+ marginBottom: tokens.spacing.xs,
277
+ }}
278
+ >
279
+ Rotation: {Math.round(rotation)}°
280
+ </AtomicText>
281
+ <ScrollView
282
+ horizontal
283
+ showsHorizontalScrollIndicator={false}
284
+ contentContainerStyle={{ gap: tokens.spacing.sm }}
285
+ >
286
+ {rotations.map((r) => (
287
+ <TouchableOpacity
288
+ key={r}
289
+ onPress={() => setRotation(r)}
290
+ style={[
291
+ styles.transformButton,
292
+ {
293
+ paddingHorizontal: tokens.spacing.md,
294
+ paddingVertical: tokens.spacing.sm,
295
+ borderRadius: tokens.borders.radius.md,
296
+ borderWidth: 1,
297
+ borderColor:
298
+ rotation === r
299
+ ? tokens.colors.primary
300
+ : tokens.colors.border,
301
+ backgroundColor:
302
+ rotation === r
303
+ ? tokens.colors.primary
304
+ : tokens.colors.surface,
305
+ },
306
+ ]}
307
+ >
308
+ <AtomicText
309
+ style={{
310
+ color: rotation === r ? "white" : tokens.colors.textPrimary,
311
+ }}
312
+ >
313
+ {r}°
314
+ </AtomicText>
315
+ </TouchableOpacity>
316
+ ))}
317
+ </ScrollView>
101
318
  </View>
319
+
102
320
  <View>
103
- <AtomicText style={{ ...tokens.typography.labelMedium, marginBottom: tokens.spacing.xs }}>Opacity: {(opacity * 100).toFixed(0)}%</AtomicText>
104
- <Slider value={opacity} onValueChange={setOpacity} minimumValue={0} maximumValue={1} step={0.05} minimumTrackTintColor={tokens.colors.primary} />
321
+ <AtomicText
322
+ style={{
323
+ ...tokens.typography.labelMedium,
324
+ marginBottom: tokens.spacing.xs,
325
+ }}
326
+ >
327
+ Opacity: {(opacity * 100).toFixed(0)}%
328
+ </AtomicText>
329
+ <ScrollView
330
+ horizontal
331
+ showsHorizontalScrollIndicator={false}
332
+ contentContainerStyle={{ gap: tokens.spacing.sm }}
333
+ >
334
+ {opacities.map((o) => (
335
+ <TouchableOpacity
336
+ key={o}
337
+ onPress={() => setOpacity(o)}
338
+ style={[
339
+ styles.transformButton,
340
+ {
341
+ paddingHorizontal: tokens.spacing.md,
342
+ paddingVertical: tokens.spacing.sm,
343
+ borderRadius: tokens.borders.radius.md,
344
+ borderWidth: 1,
345
+ borderColor:
346
+ opacity === o
347
+ ? tokens.colors.primary
348
+ : tokens.colors.border,
349
+ backgroundColor:
350
+ opacity === o
351
+ ? tokens.colors.primary
352
+ : tokens.colors.surface,
353
+ },
354
+ ]}
355
+ >
356
+ <AtomicText
357
+ style={{
358
+ color: opacity === o ? "white" : tokens.colors.textPrimary,
359
+ }}
360
+ >
361
+ {Math.round(o * 100)}%
362
+ </AtomicText>
363
+ </TouchableOpacity>
364
+ ))}
365
+ </ScrollView>
105
366
  </View>
367
+
106
368
  {onDelete && (
107
- <TouchableOpacity onPress={onDelete} style={{
108
- flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: tokens.spacing.sm,
109
- padding: tokens.spacing.md, borderRadius: tokens.radius.md, borderWidth: 1, borderColor: tokens.colors.error
110
- }}>
369
+ <TouchableOpacity
370
+ onPress={onDelete}
371
+ style={[
372
+ styles.deleteButton,
373
+ {
374
+ flexDirection: "row",
375
+ alignItems: "center",
376
+ justifyContent: "center",
377
+ gap: tokens.spacing.sm,
378
+ padding: tokens.spacing.md,
379
+ borderRadius: tokens.borders.radius.md,
380
+ borderWidth: 1,
381
+ borderColor: tokens.colors.error,
382
+ },
383
+ ]}
384
+ >
111
385
  <AtomicIcon name="trash" size={20} color="error" />
112
- <AtomicText style={{ ...tokens.typography.labelMedium, color: tokens.colors.error }}>Delete Layer</AtomicText>
386
+ <AtomicText
387
+ style={{
388
+ ...tokens.typography.labelMedium,
389
+ color: tokens.colors.error,
390
+ }}
391
+ >
392
+ Delete Layer
393
+ </AtomicText>
113
394
  </TouchableOpacity>
114
395
  )}
115
396
  </View>
116
397
  );
117
398
  };
399
+
400
+ const styles = StyleSheet.create({
401
+ textInput: {
402
+ borderWidth: 1,
403
+ textAlignVertical: "top",
404
+ },
405
+ fontButton: {
406
+ paddingVertical: 8,
407
+ minWidth: 80,
408
+ alignItems: "center",
409
+ },
410
+ colorButton: {
411
+ width: 40,
412
+ height: 40,
413
+ },
414
+ sizeButton: {
415
+ minWidth: 50,
416
+ alignItems: "center",
417
+ },
418
+ transformButton: {
419
+ minWidth: 60,
420
+ alignItems: "center",
421
+ },
422
+ deleteButton: {
423
+ alignSelf: "flex-start",
424
+ },
425
+ });
@@ -9,11 +9,8 @@ export const AtomicImage: React.FC<AtomicImageProps> = ({
9
9
  style,
10
10
  rounded,
11
11
  contentFit = 'cover',
12
- transition = 300,
13
12
  ...props
14
13
  }) => {
15
-
16
-
17
14
  return (
18
15
  <ExpoImage
19
16
  style={[
@@ -21,7 +18,6 @@ export const AtomicImage: React.FC<AtomicImageProps> = ({
21
18
  rounded && { borderRadius: 9999 }
22
19
  ]}
23
20
  contentFit={contentFit}
24
- transition={transition}
25
21
  {...props}
26
22
  />
27
23
  );
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * Media Save Service
3
- * Save images and videos to device gallery
3
+ * Saves media to device storage using expo-file-system
4
4
  */
5
5
 
6
- import * as MediaLibrary from "expo-media-library";
7
- import { MediaType, MediaLibraryPermission } from "../../domain/entities/Media";
6
+ import * as FileSystem from "expo-file-system";
7
+ import { Platform } from "react-native";
8
+ import { MediaLibraryPermission } from "../../domain/entities/Media";
8
9
 
9
10
  export interface SaveResult {
10
11
  success: boolean;
11
- assetId?: string;
12
+ path?: string;
12
13
  error?: string;
13
14
  }
14
15
 
@@ -24,24 +25,14 @@ export class MediaSaveService {
24
25
  * Request media library write permission
25
26
  */
26
27
  static async requestPermission(): Promise<MediaLibraryPermission> {
27
- try {
28
- const { status } = await MediaLibrary.requestPermissionsAsync();
29
- return MediaSaveService.mapPermissionStatus(status);
30
- } catch {
31
- return MediaLibraryPermission.DENIED;
32
- }
28
+ return MediaLibraryPermission.GRANTED;
33
29
  }
34
30
 
35
31
  /**
36
32
  * Get current permission status
37
33
  */
38
34
  static async getPermissionStatus(): Promise<MediaLibraryPermission> {
39
- try {
40
- const { status } = await MediaLibrary.getPermissionsAsync();
41
- return MediaSaveService.mapPermissionStatus(status);
42
- } catch {
43
- return MediaLibraryPermission.DENIED;
44
- }
35
+ return MediaLibraryPermission.GRANTED;
45
36
  }
46
37
 
47
38
  /**
@@ -49,9 +40,9 @@ export class MediaSaveService {
49
40
  */
50
41
  static async saveImage(
51
42
  uri: string,
52
- options?: SaveOptions
43
+ options?: SaveOptions,
53
44
  ): Promise<SaveResult> {
54
- return MediaSaveService.saveToGallery(uri, MediaType.IMAGE, options);
45
+ return MediaSaveService.saveToStorage(uri, "image", options);
55
46
  }
56
47
 
57
48
  /**
@@ -59,41 +50,42 @@ export class MediaSaveService {
59
50
  */
60
51
  static async saveVideo(
61
52
  uri: string,
62
- options?: SaveOptions
53
+ options?: SaveOptions,
63
54
  ): Promise<SaveResult> {
64
- return MediaSaveService.saveToGallery(uri, MediaType.VIDEO, options);
55
+ return MediaSaveService.saveToStorage(uri, "video", options);
65
56
  }
66
57
 
67
58
  /**
68
- * Save media (image or video) to gallery
59
+ * Save media (image or video) to storage
69
60
  */
70
- static async saveToGallery(
61
+ private static async saveToStorage(
71
62
  uri: string,
72
- _mediaType: MediaType = MediaType.ALL,
73
- options?: SaveOptions
63
+ mediaType: "image" | "video",
64
+ options?: SaveOptions,
74
65
  ): Promise<SaveResult> {
75
66
  try {
76
- const permission = await MediaSaveService.requestPermission();
67
+ const timestamp = Date.now();
68
+ const extension = mediaType === "image" ? "jpg" : "mp4";
69
+ const filename = `${mediaType}_${timestamp}.${extension}`;
77
70
 
78
- if (permission === MediaLibraryPermission.DENIED) {
71
+ const directory = FileSystem.documentDirectory;
72
+ if (!directory) {
79
73
  return {
80
74
  success: false,
81
- error: "Permission denied to save media",
75
+ error: "Document directory not available",
82
76
  };
83
77
  }
84
78
 
85
- const asset = await MediaLibrary.createAssetAsync(uri);
79
+ const destination = `${directory}${filename}`;
86
80
 
87
- if (options?.album) {
88
- const album = await MediaSaveService.getOrCreateAlbum(options.album);
89
- if (album) {
90
- await MediaLibrary.addAssetsToAlbumAsync([asset], album, false);
91
- }
92
- }
81
+ await FileSystem.copyAsync({
82
+ from: uri,
83
+ to: destination,
84
+ });
93
85
 
94
86
  return {
95
87
  success: true,
96
- assetId: asset.id,
88
+ path: destination,
97
89
  };
98
90
  } catch (error) {
99
91
  const message = error instanceof Error ? error.message : "Unknown error";
@@ -103,52 +95,4 @@ export class MediaSaveService {
103
95
  };
104
96
  }
105
97
  }
106
-
107
- /**
108
- * Get or create album
109
- */
110
- private static async getOrCreateAlbum(
111
- albumName: string
112
- ): Promise<MediaLibrary.Album | null> {
113
- try {
114
- const albums = await MediaLibrary.getAlbumsAsync();
115
- const existingAlbum = albums.find((album) => album.title === albumName);
116
-
117
- if (existingAlbum) {
118
- return existingAlbum;
119
- }
120
-
121
- const asset = await MediaLibrary.getAssetsAsync({ first: 1 });
122
- if (asset.assets.length > 0) {
123
- const newAlbum = await MediaLibrary.createAlbumAsync(
124
- albumName,
125
- asset.assets[0],
126
- false
127
- );
128
- return newAlbum;
129
- }
130
-
131
- return null;
132
- } catch {
133
- return null;
134
- }
135
- }
136
-
137
- /**
138
- * Map permission status
139
- */
140
- private static mapPermissionStatus(
141
- status: MediaLibrary.PermissionStatus
142
- ): MediaLibraryPermission {
143
- switch (status) {
144
- case MediaLibrary.PermissionStatus.GRANTED:
145
- return MediaLibraryPermission.GRANTED;
146
- case MediaLibrary.PermissionStatus.DENIED:
147
- return MediaLibraryPermission.DENIED;
148
- case MediaLibrary.PermissionStatus.UNDETERMINED:
149
- return MediaLibraryPermission.DENIED;
150
- default:
151
- return MediaLibraryPermission.DENIED;
152
- }
153
- }
154
98
  }
@@ -82,7 +82,6 @@ export const BackgroundImageCollage: React.FC<BackgroundImageCollageProps> = ({
82
82
  source={item.source}
83
83
  style={item.style}
84
84
  contentFit="cover"
85
- transition={300}
86
85
  />
87
86
  ))}
88
87
  </View>
@@ -53,7 +53,6 @@ export const OnboardingBackground: React.FC<OnboardingBackgroundProps> = ({
53
53
  source={currentSlide.backgroundImage}
54
54
  style={StyleSheet.absoluteFill}
55
55
  contentFit="cover"
56
- transition={500}
57
56
  cachePolicy="memory-disk"
58
57
  priority="high"
59
58
  />