@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 +6 -17
- package/src/atoms/image/AtomicImage.tsx +0 -4
- package/src/image/presentation/components/ImageGallery.tsx +0 -1
- package/src/image/presentation/components/editor/TextEditorTabs.tsx +366 -58
- package/src/image/presentation/components/image/AtomicImage.tsx +0 -4
- package/src/media/infrastructure/services/MediaSaveService.ts +27 -83
- package/src/onboarding/presentation/components/BackgroundImageCollage.tsx +0 -1
- package/src/onboarding/presentation/components/OnboardingBackground.tsx +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-design-system",
|
|
3
|
-
"version": "2.9.
|
|
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.
|
|
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.
|
|
141
|
-
"expo-haptics": "~14.0.0",
|
|
135
|
+
"expo-font": "~14.0.9",
|
|
142
136
|
"expo-image": "~3.0.11",
|
|
143
|
-
"expo-
|
|
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.
|
|
139
|
+
"expo-secure-store": "~14.0.8",
|
|
150
140
|
"expo-sharing": "~14.0.8",
|
|
151
|
-
"expo-video": "~3.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
|
);
|
|
@@ -2,60 +2,111 @@
|
|
|
2
2
|
* Presentation - Text Editor Tabs
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import React from
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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<
|
|
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={
|
|
24
|
-
style={
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 = [
|
|
46
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
79
|
-
|
|
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<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
96
|
-
|
|
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
|
|
100
|
-
|
|
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
|
|
104
|
-
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
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
|
-
*
|
|
3
|
+
* Saves media to device storage using expo-file-system
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import * as
|
|
7
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
55
|
+
return MediaSaveService.saveToStorage(uri, "video", options);
|
|
65
56
|
}
|
|
66
57
|
|
|
67
58
|
/**
|
|
68
|
-
* Save media (image or video) to
|
|
59
|
+
* Save media (image or video) to storage
|
|
69
60
|
*/
|
|
70
|
-
static async
|
|
61
|
+
private static async saveToStorage(
|
|
71
62
|
uri: string,
|
|
72
|
-
|
|
73
|
-
options?: SaveOptions
|
|
63
|
+
mediaType: "image" | "video",
|
|
64
|
+
options?: SaveOptions,
|
|
74
65
|
): Promise<SaveResult> {
|
|
75
66
|
try {
|
|
76
|
-
const
|
|
67
|
+
const timestamp = Date.now();
|
|
68
|
+
const extension = mediaType === "image" ? "jpg" : "mp4";
|
|
69
|
+
const filename = `${mediaType}_${timestamp}.${extension}`;
|
|
77
70
|
|
|
78
|
-
|
|
71
|
+
const directory = FileSystem.documentDirectory;
|
|
72
|
+
if (!directory) {
|
|
79
73
|
return {
|
|
80
74
|
success: false,
|
|
81
|
-
error: "
|
|
75
|
+
error: "Document directory not available",
|
|
82
76
|
};
|
|
83
77
|
}
|
|
84
78
|
|
|
85
|
-
const
|
|
79
|
+
const destination = `${directory}${filename}`;
|
|
86
80
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
}
|
|
81
|
+
await FileSystem.copyAsync({
|
|
82
|
+
from: uri,
|
|
83
|
+
to: destination,
|
|
84
|
+
});
|
|
93
85
|
|
|
94
86
|
return {
|
|
95
87
|
success: true,
|
|
96
|
-
|
|
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
|
}
|