@umituz/react-native-image 1.1.0
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/LICENSE +22 -0
- package/README.md +250 -0
- package/package.json +53 -0
- package/src/domain/entities/Image.ts +273 -0
- package/src/index.ts +55 -0
- package/src/infrastructure/services/ImageService.ts +316 -0
- package/src/infrastructure/services/ImageViewerService.ts +89 -0
- package/src/presentation/hooks/useImage.ts +393 -0
- package/src/presentation/hooks/useImageGallery.ts +149 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Domain - useImage Hook
|
|
3
|
+
*
|
|
4
|
+
* React hook for image manipulation operations.
|
|
5
|
+
* Provides image resizing, cropping, rotating, and format conversion.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useCallback } from 'react';
|
|
9
|
+
import { ImageService } from '../../infrastructure/services/ImageService';
|
|
10
|
+
import type {
|
|
11
|
+
ImageManipulateAction,
|
|
12
|
+
ImageSaveOptions,
|
|
13
|
+
ImageManipulationResult,
|
|
14
|
+
SaveFormat,
|
|
15
|
+
} from '../../domain/entities/Image';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* useImage hook for image manipulation
|
|
19
|
+
*
|
|
20
|
+
* USAGE:
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const { resize, crop, rotate, compress, isProcessing } = useImage();
|
|
23
|
+
*
|
|
24
|
+
* // Resize image
|
|
25
|
+
* const resized = await resize(imageUri, 800, 600);
|
|
26
|
+
*
|
|
27
|
+
* // Crop image
|
|
28
|
+
* const cropped = await crop(imageUri, { originX: 0, originY: 0, width: 500, height: 500 });
|
|
29
|
+
*
|
|
30
|
+
* // Compress image
|
|
31
|
+
* const compressed = await compress(imageUri, 0.7);
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export const useImage = () => {
|
|
35
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
36
|
+
const [error, setError] = useState<string | null>(null);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resize image
|
|
40
|
+
*/
|
|
41
|
+
const resize = useCallback(
|
|
42
|
+
async (
|
|
43
|
+
uri: string,
|
|
44
|
+
width?: number,
|
|
45
|
+
height?: number,
|
|
46
|
+
options?: ImageSaveOptions
|
|
47
|
+
): Promise<ImageManipulationResult | null> => {
|
|
48
|
+
setIsProcessing(true);
|
|
49
|
+
setError(null);
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const result = await ImageService.resize(uri, width, height, options);
|
|
53
|
+
|
|
54
|
+
if (!result) {
|
|
55
|
+
setError('Failed to resize image');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return result;
|
|
59
|
+
} catch (err) {
|
|
60
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to resize image';
|
|
61
|
+
setError(errorMessage);
|
|
62
|
+
return null;
|
|
63
|
+
} finally {
|
|
64
|
+
setIsProcessing(false);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
[]
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Crop image
|
|
72
|
+
*/
|
|
73
|
+
const crop = useCallback(
|
|
74
|
+
async (
|
|
75
|
+
uri: string,
|
|
76
|
+
cropArea: { originX: number; originY: number; width: number; height: number },
|
|
77
|
+
options?: ImageSaveOptions
|
|
78
|
+
): Promise<ImageManipulationResult | null> => {
|
|
79
|
+
setIsProcessing(true);
|
|
80
|
+
setError(null);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const result = await ImageService.crop(uri, cropArea, options);
|
|
84
|
+
|
|
85
|
+
if (!result) {
|
|
86
|
+
setError('Failed to crop image');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return result;
|
|
90
|
+
} catch (err) {
|
|
91
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to crop image';
|
|
92
|
+
setError(errorMessage);
|
|
93
|
+
return null;
|
|
94
|
+
} finally {
|
|
95
|
+
setIsProcessing(false);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
[]
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Rotate image
|
|
103
|
+
*/
|
|
104
|
+
const rotate = useCallback(
|
|
105
|
+
async (
|
|
106
|
+
uri: string,
|
|
107
|
+
degrees: number,
|
|
108
|
+
options?: ImageSaveOptions
|
|
109
|
+
): Promise<ImageManipulationResult | null> => {
|
|
110
|
+
setIsProcessing(true);
|
|
111
|
+
setError(null);
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const result = await ImageService.rotate(uri, degrees, options);
|
|
115
|
+
|
|
116
|
+
if (!result) {
|
|
117
|
+
setError('Failed to rotate image');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return result;
|
|
121
|
+
} catch (err) {
|
|
122
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to rotate image';
|
|
123
|
+
setError(errorMessage);
|
|
124
|
+
return null;
|
|
125
|
+
} finally {
|
|
126
|
+
setIsProcessing(false);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
[]
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Flip image
|
|
134
|
+
*/
|
|
135
|
+
const flip = useCallback(
|
|
136
|
+
async (
|
|
137
|
+
uri: string,
|
|
138
|
+
flipOptions: { horizontal?: boolean; vertical?: boolean },
|
|
139
|
+
saveOptions?: ImageSaveOptions
|
|
140
|
+
): Promise<ImageManipulationResult | null> => {
|
|
141
|
+
setIsProcessing(true);
|
|
142
|
+
setError(null);
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const result = await ImageService.flip(uri, flipOptions, saveOptions);
|
|
146
|
+
|
|
147
|
+
if (!result) {
|
|
148
|
+
setError('Failed to flip image');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return result;
|
|
152
|
+
} catch (err) {
|
|
153
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to flip image';
|
|
154
|
+
setError(errorMessage);
|
|
155
|
+
return null;
|
|
156
|
+
} finally {
|
|
157
|
+
setIsProcessing(false);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
[]
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Perform multiple manipulations
|
|
165
|
+
*/
|
|
166
|
+
const manipulate = useCallback(
|
|
167
|
+
async (
|
|
168
|
+
uri: string,
|
|
169
|
+
action: ImageManipulateAction,
|
|
170
|
+
options?: ImageSaveOptions
|
|
171
|
+
): Promise<ImageManipulationResult | null> => {
|
|
172
|
+
setIsProcessing(true);
|
|
173
|
+
setError(null);
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const result = await ImageService.manipulate(uri, action, options);
|
|
177
|
+
|
|
178
|
+
if (!result) {
|
|
179
|
+
setError('Failed to manipulate image');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return result;
|
|
183
|
+
} catch (err) {
|
|
184
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to manipulate image';
|
|
185
|
+
setError(errorMessage);
|
|
186
|
+
return null;
|
|
187
|
+
} finally {
|
|
188
|
+
setIsProcessing(false);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
[]
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Compress image
|
|
196
|
+
*/
|
|
197
|
+
const compress = useCallback(
|
|
198
|
+
async (uri: string, quality: number = 0.8): Promise<ImageManipulationResult | null> => {
|
|
199
|
+
setIsProcessing(true);
|
|
200
|
+
setError(null);
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const result = await ImageService.compress(uri, quality);
|
|
204
|
+
|
|
205
|
+
if (!result) {
|
|
206
|
+
setError('Failed to compress image');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return result;
|
|
210
|
+
} catch (err) {
|
|
211
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to compress image';
|
|
212
|
+
setError(errorMessage);
|
|
213
|
+
return null;
|
|
214
|
+
} finally {
|
|
215
|
+
setIsProcessing(false);
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
[]
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Resize to fit within max dimensions
|
|
223
|
+
*/
|
|
224
|
+
const resizeToFit = useCallback(
|
|
225
|
+
async (
|
|
226
|
+
uri: string,
|
|
227
|
+
maxWidth: number,
|
|
228
|
+
maxHeight: number,
|
|
229
|
+
options?: ImageSaveOptions
|
|
230
|
+
): Promise<ImageManipulationResult | null> => {
|
|
231
|
+
setIsProcessing(true);
|
|
232
|
+
setError(null);
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const result = await ImageService.resizeToFit(uri, maxWidth, maxHeight, options);
|
|
236
|
+
|
|
237
|
+
if (!result) {
|
|
238
|
+
setError('Failed to resize image to fit');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return result;
|
|
242
|
+
} catch (err) {
|
|
243
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to resize image to fit';
|
|
244
|
+
setError(errorMessage);
|
|
245
|
+
return null;
|
|
246
|
+
} finally {
|
|
247
|
+
setIsProcessing(false);
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
[]
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Create thumbnail
|
|
255
|
+
*/
|
|
256
|
+
const createThumbnail = useCallback(
|
|
257
|
+
async (
|
|
258
|
+
uri: string,
|
|
259
|
+
size: number = 200,
|
|
260
|
+
options?: ImageSaveOptions
|
|
261
|
+
): Promise<ImageManipulationResult | null> => {
|
|
262
|
+
setIsProcessing(true);
|
|
263
|
+
setError(null);
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const result = await ImageService.createThumbnail(uri, size, options);
|
|
267
|
+
|
|
268
|
+
if (!result) {
|
|
269
|
+
setError('Failed to create thumbnail');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return result;
|
|
273
|
+
} catch (err) {
|
|
274
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to create thumbnail';
|
|
275
|
+
setError(errorMessage);
|
|
276
|
+
return null;
|
|
277
|
+
} finally {
|
|
278
|
+
setIsProcessing(false);
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
[]
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Crop to square
|
|
286
|
+
*/
|
|
287
|
+
const cropToSquare = useCallback(
|
|
288
|
+
async (
|
|
289
|
+
uri: string,
|
|
290
|
+
width: number,
|
|
291
|
+
height: number,
|
|
292
|
+
options?: ImageSaveOptions
|
|
293
|
+
): Promise<ImageManipulationResult | null> => {
|
|
294
|
+
setIsProcessing(true);
|
|
295
|
+
setError(null);
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
const result = await ImageService.cropToSquare(uri, width, height, options);
|
|
299
|
+
|
|
300
|
+
if (!result) {
|
|
301
|
+
setError('Failed to crop to square');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return result;
|
|
305
|
+
} catch (err) {
|
|
306
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to crop to square';
|
|
307
|
+
setError(errorMessage);
|
|
308
|
+
return null;
|
|
309
|
+
} finally {
|
|
310
|
+
setIsProcessing(false);
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
[]
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Convert image format
|
|
318
|
+
*/
|
|
319
|
+
const convertFormat = useCallback(
|
|
320
|
+
async (
|
|
321
|
+
uri: string,
|
|
322
|
+
format: SaveFormat,
|
|
323
|
+
quality?: number
|
|
324
|
+
): Promise<ImageManipulationResult | null> => {
|
|
325
|
+
setIsProcessing(true);
|
|
326
|
+
setError(null);
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const result = await ImageService.convertFormat(uri, format, quality);
|
|
330
|
+
|
|
331
|
+
if (!result) {
|
|
332
|
+
setError('Failed to convert image format');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return result;
|
|
336
|
+
} catch (err) {
|
|
337
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to convert image format';
|
|
338
|
+
setError(errorMessage);
|
|
339
|
+
return null;
|
|
340
|
+
} finally {
|
|
341
|
+
setIsProcessing(false);
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
[]
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Save image to device
|
|
349
|
+
*/
|
|
350
|
+
const saveImage = useCallback(
|
|
351
|
+
async (uri: string, filename?: string): Promise<string | null> => {
|
|
352
|
+
setIsProcessing(true);
|
|
353
|
+
setError(null);
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const savedUri = await ImageService.saveImage(uri, filename);
|
|
357
|
+
|
|
358
|
+
if (!savedUri) {
|
|
359
|
+
setError('Failed to save image');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return savedUri;
|
|
363
|
+
} catch (err) {
|
|
364
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to save image';
|
|
365
|
+
setError(errorMessage);
|
|
366
|
+
return null;
|
|
367
|
+
} finally {
|
|
368
|
+
setIsProcessing(false);
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
[]
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
// Functions
|
|
376
|
+
resize,
|
|
377
|
+
crop,
|
|
378
|
+
rotate,
|
|
379
|
+
flip,
|
|
380
|
+
manipulate,
|
|
381
|
+
compress,
|
|
382
|
+
resizeToFit,
|
|
383
|
+
createThumbnail,
|
|
384
|
+
cropToSquare,
|
|
385
|
+
convertFormat,
|
|
386
|
+
saveImage,
|
|
387
|
+
|
|
388
|
+
// State
|
|
389
|
+
isProcessing,
|
|
390
|
+
error,
|
|
391
|
+
};
|
|
392
|
+
};
|
|
393
|
+
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Domain - useImageGallery Hook
|
|
3
|
+
*
|
|
4
|
+
* React hook for image gallery and viewer using react-native-image-viewing.
|
|
5
|
+
* Provides full-screen image viewer with zoom, swipe, and gallery features.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
9
|
+
import { ImageViewerService } from '../../infrastructure/services/ImageViewerService';
|
|
10
|
+
import type {
|
|
11
|
+
ImageViewerItem,
|
|
12
|
+
ImageGalleryOptions,
|
|
13
|
+
} from '../../domain/entities/Image';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* useImageGallery hook return type
|
|
17
|
+
*/
|
|
18
|
+
export interface UseImageGalleryReturn {
|
|
19
|
+
// State
|
|
20
|
+
visible: boolean;
|
|
21
|
+
currentIndex: number;
|
|
22
|
+
images: ImageViewerItem[];
|
|
23
|
+
|
|
24
|
+
// Actions
|
|
25
|
+
open: (images: ImageViewerItem[] | string[], startIndex?: number, options?: ImageGalleryOptions) => void;
|
|
26
|
+
close: () => void;
|
|
27
|
+
setIndex: (index: number) => void;
|
|
28
|
+
|
|
29
|
+
// Gallery options
|
|
30
|
+
options: ImageGalleryOptions;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* useImageGallery hook for full-screen image viewer
|
|
35
|
+
*
|
|
36
|
+
* USAGE:
|
|
37
|
+
* ```typescript
|
|
38
|
+
* const { visible, currentIndex, images, open, close, options } = useImageGallery();
|
|
39
|
+
*
|
|
40
|
+
* // Open gallery with image URIs
|
|
41
|
+
* open(['uri1', 'uri2', 'uri3']);
|
|
42
|
+
*
|
|
43
|
+
* // Open gallery with metadata
|
|
44
|
+
* open([
|
|
45
|
+
* { uri: 'uri1', title: 'Photo 1' },
|
|
46
|
+
* { uri: 'uri2', title: 'Photo 2' },
|
|
47
|
+
* ], 0, { backgroundColor: '#000000' });
|
|
48
|
+
*
|
|
49
|
+
* // Render ImageViewing component
|
|
50
|
+
* <ImageViewing
|
|
51
|
+
* images={images}
|
|
52
|
+
* imageIndex={currentIndex}
|
|
53
|
+
* visible={visible}
|
|
54
|
+
* onRequestClose={close}
|
|
55
|
+
* onIndexChange={setIndex}
|
|
56
|
+
* {...options}
|
|
57
|
+
* />
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export const useImageGallery = (
|
|
61
|
+
defaultOptions?: ImageGalleryOptions
|
|
62
|
+
): UseImageGalleryReturn => {
|
|
63
|
+
const [visible, setVisible] = useState(false);
|
|
64
|
+
const [currentIndex, setCurrentIndex] = useState(0);
|
|
65
|
+
const [images, setImages] = useState<ImageViewerItem[]>([]);
|
|
66
|
+
const [galleryOptions, setGalleryOptions] = useState<ImageGalleryOptions>(
|
|
67
|
+
defaultOptions || ImageViewerService.getDefaultOptions()
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Open gallery with images
|
|
72
|
+
*/
|
|
73
|
+
const open = useCallback(
|
|
74
|
+
(
|
|
75
|
+
imageData: ImageViewerItem[] | string[],
|
|
76
|
+
startIndex: number = 0,
|
|
77
|
+
options?: ImageGalleryOptions
|
|
78
|
+
) => {
|
|
79
|
+
// Prepare images based on input type
|
|
80
|
+
const preparedImages =
|
|
81
|
+
typeof imageData[0] === 'string'
|
|
82
|
+
? ImageViewerService.prepareImages(imageData as string[])
|
|
83
|
+
: ImageViewerService.prepareImagesWithMetadata(imageData as ImageViewerItem[]);
|
|
84
|
+
|
|
85
|
+
setImages(preparedImages);
|
|
86
|
+
setCurrentIndex(options?.index ?? startIndex);
|
|
87
|
+
|
|
88
|
+
// Merge options with defaults
|
|
89
|
+
if (options) {
|
|
90
|
+
setGalleryOptions({
|
|
91
|
+
...galleryOptions,
|
|
92
|
+
...options,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setVisible(true);
|
|
97
|
+
},
|
|
98
|
+
[galleryOptions]
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Close gallery
|
|
103
|
+
*/
|
|
104
|
+
const close = useCallback(() => {
|
|
105
|
+
setVisible(false);
|
|
106
|
+
|
|
107
|
+
// Call onDismiss if provided
|
|
108
|
+
if (galleryOptions.onDismiss) {
|
|
109
|
+
galleryOptions.onDismiss();
|
|
110
|
+
}
|
|
111
|
+
}, [galleryOptions]);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Set current image index
|
|
115
|
+
*/
|
|
116
|
+
const setIndex = useCallback((index: number) => {
|
|
117
|
+
setCurrentIndex(index);
|
|
118
|
+
|
|
119
|
+
// Call onIndexChange if provided
|
|
120
|
+
if (galleryOptions.onIndexChange) {
|
|
121
|
+
galleryOptions.onIndexChange(index);
|
|
122
|
+
}
|
|
123
|
+
}, [galleryOptions]);
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Memoized options for ImageViewing component
|
|
127
|
+
*/
|
|
128
|
+
const options = useMemo(() => ({
|
|
129
|
+
backgroundColor: galleryOptions.backgroundColor || '#000000',
|
|
130
|
+
swipeToCloseEnabled: galleryOptions.swipeToCloseEnabled ?? true,
|
|
131
|
+
doubleTapToZoomEnabled: galleryOptions.doubleTapToZoomEnabled ?? true,
|
|
132
|
+
}), [galleryOptions]);
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
// State
|
|
136
|
+
visible,
|
|
137
|
+
currentIndex,
|
|
138
|
+
images,
|
|
139
|
+
|
|
140
|
+
// Actions
|
|
141
|
+
open,
|
|
142
|
+
close,
|
|
143
|
+
setIndex,
|
|
144
|
+
|
|
145
|
+
// Gallery options
|
|
146
|
+
options,
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
|