@umituz/react-native-image 1.1.0 → 1.1.1
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 +10 -4
- package/src/domain/entities/ImageConstants.ts +21 -0
- package/src/domain/entities/ImageTypes.ts +71 -0
- package/src/domain/utils/ImageUtils.ts +103 -0
- package/src/index.ts +16 -10
- package/src/infrastructure/services/ImageConversionService.ts +66 -0
- package/src/infrastructure/services/ImageStorageService.ts +22 -0
- package/src/infrastructure/services/ImageTransformService.ts +172 -0
- package/src/infrastructure/services/ImageViewerService.ts +1 -1
- package/src/presentation/components/ImageGallery.tsx +54 -0
- package/src/presentation/hooks/useImage.ts +13 -384
- package/src/presentation/hooks/useImageConversion.ts +26 -0
- package/src/presentation/hooks/useImageGallery.ts +1 -1
- package/src/presentation/hooks/useImageOperation.ts +33 -0
- package/src/presentation/hooks/useImageTransform.ts +37 -0
- package/src/domain/entities/Image.ts +0 -273
- package/src/infrastructure/services/ImageService.ts +0 -316
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-image",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Image manipulation and viewing for React Native apps - resize, crop, rotate, flip, compress, gallery viewer",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -29,17 +29,23 @@
|
|
|
29
29
|
"url": "https://github.com/umituz/react-native-image"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
|
+
"@umituz/react-native-filesystem": "latest",
|
|
33
|
+
"expo-image-manipulator": ">=11.0.0",
|
|
32
34
|
"react": ">=18.2.0",
|
|
33
35
|
"react-native": ">=0.74.0",
|
|
34
|
-
"
|
|
35
|
-
"react-native-image-viewing": ">=0.2.0",
|
|
36
|
-
"@umituz/react-native-filesystem": "latest"
|
|
36
|
+
"react-native-image-viewing": ">=0.2.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
+
"@react-native/typescript-config": "^0.83.0",
|
|
39
40
|
"@types/react": "^18.2.45",
|
|
40
41
|
"@types/react-native": "^0.73.0",
|
|
42
|
+
"@umituz/react-native-design-system-theme": "^1.12.0",
|
|
43
|
+
"@umituz/react-native-filesystem": "^2.1.1",
|
|
44
|
+
"expo-file-system": "^19.0.21",
|
|
45
|
+
"expo-image-manipulator": "^14.0.8",
|
|
41
46
|
"react": "^18.2.0",
|
|
42
47
|
"react-native": "^0.74.0",
|
|
48
|
+
"react-native-image-viewing": "^0.2.2",
|
|
43
49
|
"typescript": "^5.3.3"
|
|
44
50
|
},
|
|
45
51
|
"publishConfig": {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Constants
|
|
3
|
+
*/
|
|
4
|
+
import { SaveFormat } from "./ImageTypes";
|
|
5
|
+
|
|
6
|
+
export const IMAGE_CONSTANTS = {
|
|
7
|
+
MAX_WIDTH: 2048,
|
|
8
|
+
MAX_HEIGHT: 2048,
|
|
9
|
+
DEFAULT_QUALITY: 0.8,
|
|
10
|
+
THUMBNAIL_SIZE: 200,
|
|
11
|
+
COMPRESS_QUALITY: {
|
|
12
|
+
LOW: 0.5,
|
|
13
|
+
MEDIUM: 0.7,
|
|
14
|
+
HIGH: 0.9,
|
|
15
|
+
},
|
|
16
|
+
FORMAT: {
|
|
17
|
+
JPEG: 'jpeg' as SaveFormat,
|
|
18
|
+
PNG: 'png' as SaveFormat,
|
|
19
|
+
WEBP: 'webp' as SaveFormat,
|
|
20
|
+
},
|
|
21
|
+
} as const;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Type Definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export enum ImageFormat {
|
|
6
|
+
JPEG = 'jpeg',
|
|
7
|
+
PNG = 'png',
|
|
8
|
+
WEBP = 'webp',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type SaveFormat = 'jpeg' | 'png' | 'webp';
|
|
12
|
+
|
|
13
|
+
export enum ImageOrientation {
|
|
14
|
+
PORTRAIT = 'portrait',
|
|
15
|
+
LANDSCAPE = 'landscape',
|
|
16
|
+
SQUARE = 'square',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ImageManipulateAction {
|
|
20
|
+
resize?: { width?: number; height?: number };
|
|
21
|
+
crop?: { originX: number; originY: number; width: number; height: number };
|
|
22
|
+
rotate?: number;
|
|
23
|
+
flip?: { vertical?: boolean; horizontal?: boolean };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ImageSaveOptions {
|
|
27
|
+
format?: SaveFormat;
|
|
28
|
+
compress?: number;
|
|
29
|
+
base64?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ImageManipulationResult {
|
|
33
|
+
uri: string;
|
|
34
|
+
width: number;
|
|
35
|
+
height: number;
|
|
36
|
+
base64?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ImageMetadata {
|
|
40
|
+
uri: string;
|
|
41
|
+
width: number;
|
|
42
|
+
height: number;
|
|
43
|
+
format?: ImageFormat;
|
|
44
|
+
size?: number;
|
|
45
|
+
orientation?: ImageOrientation;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ImageViewerItem {
|
|
49
|
+
uri: string;
|
|
50
|
+
title?: string;
|
|
51
|
+
description?: string;
|
|
52
|
+
width?: number;
|
|
53
|
+
height?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ImageGalleryOptions {
|
|
57
|
+
index?: number;
|
|
58
|
+
backgroundColor?: string;
|
|
59
|
+
swipeToCloseEnabled?: boolean;
|
|
60
|
+
doubleTapToZoomEnabled?: boolean;
|
|
61
|
+
onDismiss?: () => void;
|
|
62
|
+
onIndexChange?: (index: number) => void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ImageOperationResult {
|
|
66
|
+
success: boolean;
|
|
67
|
+
uri?: string;
|
|
68
|
+
error?: string;
|
|
69
|
+
width?: number;
|
|
70
|
+
height?: number;
|
|
71
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Utilities
|
|
3
|
+
*/
|
|
4
|
+
import { ImageFormat, ImageOrientation, ImageManipulateAction } from "../entities/ImageTypes";
|
|
5
|
+
import { IMAGE_CONSTANTS } from "../entities/ImageConstants";
|
|
6
|
+
|
|
7
|
+
export class ImageUtils {
|
|
8
|
+
static getOrientation(width: number, height: number): ImageOrientation {
|
|
9
|
+
if (width > height) return ImageOrientation.LANDSCAPE;
|
|
10
|
+
if (height > width) return ImageOrientation.PORTRAIT;
|
|
11
|
+
return ImageOrientation.SQUARE;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static getAspectRatio(width: number, height: number): number {
|
|
15
|
+
return width / height;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static fitToSize(
|
|
19
|
+
width: number,
|
|
20
|
+
height: number,
|
|
21
|
+
maxWidth: number,
|
|
22
|
+
maxHeight: number
|
|
23
|
+
): { width: number; height: number } {
|
|
24
|
+
const aspectRatio = ImageUtils.getAspectRatio(width, height);
|
|
25
|
+
let newWidth = width;
|
|
26
|
+
let newHeight = height;
|
|
27
|
+
|
|
28
|
+
if (width > maxWidth) {
|
|
29
|
+
newWidth = maxWidth;
|
|
30
|
+
newHeight = newWidth / aspectRatio;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (newHeight > maxHeight) {
|
|
34
|
+
newHeight = maxHeight;
|
|
35
|
+
newWidth = newHeight * aspectRatio;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
width: Math.round(newWidth),
|
|
40
|
+
height: Math.round(newHeight),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static getThumbnailSize(
|
|
45
|
+
width: number,
|
|
46
|
+
height: number,
|
|
47
|
+
thumbnailSize: number = IMAGE_CONSTANTS.THUMBNAIL_SIZE
|
|
48
|
+
): { width: number; height: number } {
|
|
49
|
+
return ImageUtils.fitToSize(width, height, thumbnailSize, thumbnailSize);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static isValidImageUri(uri: string): boolean {
|
|
53
|
+
if (!uri) return false;
|
|
54
|
+
return (
|
|
55
|
+
uri.startsWith('file://') ||
|
|
56
|
+
uri.startsWith('content://') ||
|
|
57
|
+
uri.startsWith('http://') ||
|
|
58
|
+
uri.startsWith('https://') ||
|
|
59
|
+
uri.startsWith('data:image/')
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static getFormatFromUri(uri: string): ImageFormat | null {
|
|
64
|
+
const lowerUri = uri.toLowerCase();
|
|
65
|
+
if (lowerUri.includes('.jpg') || lowerUri.includes('.jpeg')) return ImageFormat.JPEG;
|
|
66
|
+
if (lowerUri.includes('.png')) return ImageFormat.PNG;
|
|
67
|
+
if (lowerUri.includes('.webp')) return ImageFormat.WEBP;
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static getExtensionFromFormat(format: ImageFormat): string {
|
|
72
|
+
switch (format) {
|
|
73
|
+
case ImageFormat.JPEG: return 'jpg';
|
|
74
|
+
case ImageFormat.PNG: return 'png';
|
|
75
|
+
case ImageFormat.WEBP: return 'webp';
|
|
76
|
+
default: return 'jpg';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static getSquareCrop(width: number, height: number): ImageManipulateAction['crop'] {
|
|
81
|
+
const size = Math.min(width, height);
|
|
82
|
+
const originX = (width - size) / 2;
|
|
83
|
+
const originY = (height - size) / 2;
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
originX: Math.round(originX),
|
|
87
|
+
originY: Math.round(originY),
|
|
88
|
+
width: Math.round(size),
|
|
89
|
+
height: Math.round(size),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static formatFileSize(bytes: number): string {
|
|
94
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
95
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
96
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
static needsCompression(bytes: number, maxSizeMB: number = 2): boolean {
|
|
100
|
+
const maxBytes = maxSizeMB * 1024 * 1024;
|
|
101
|
+
return bytes > maxBytes;
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Resize, crop, rotate, flip, compress, gallery viewer
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
|
-
* import { useImage,
|
|
8
|
+
* import { useImage, ImageGallery, ImageService, ImageUtils } from '@umituz/react-native-image';
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
// =============================================================================
|
|
@@ -21,35 +21,41 @@ export type {
|
|
|
21
21
|
ImageGalleryOptions,
|
|
22
22
|
ImageOperationResult,
|
|
23
23
|
SaveFormat,
|
|
24
|
-
} from './domain/entities/
|
|
24
|
+
} from './domain/entities/ImageTypes';
|
|
25
25
|
|
|
26
26
|
export {
|
|
27
27
|
ImageFormat,
|
|
28
28
|
ImageOrientation,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
} from './domain/entities/
|
|
29
|
+
} from './domain/entities/ImageTypes';
|
|
30
|
+
|
|
31
|
+
export { IMAGE_CONSTANTS } from './domain/entities/ImageConstants';
|
|
32
|
+
export { ImageUtils } from './domain/utils/ImageUtils';
|
|
32
33
|
|
|
33
34
|
// =============================================================================
|
|
34
35
|
// INFRASTRUCTURE LAYER - Services
|
|
35
36
|
// =============================================================================
|
|
36
37
|
|
|
37
|
-
export {
|
|
38
|
+
export { ImageTransformService } from './infrastructure/services/ImageTransformService';
|
|
39
|
+
export { ImageConversionService } from './infrastructure/services/ImageConversionService';
|
|
40
|
+
export { ImageStorageService } from './infrastructure/services/ImageStorageService';
|
|
38
41
|
export {
|
|
39
42
|
ImageViewerService,
|
|
40
43
|
type ImageViewerConfig,
|
|
41
44
|
} from './infrastructure/services/ImageViewerService';
|
|
42
45
|
|
|
43
46
|
// =============================================================================
|
|
44
|
-
// PRESENTATION LAYER - Hooks
|
|
47
|
+
// PRESENTATION LAYER - Components & Hooks
|
|
45
48
|
// =============================================================================
|
|
46
49
|
|
|
47
|
-
export {
|
|
48
|
-
|
|
49
|
-
} from './presentation/hooks/useImage';
|
|
50
|
+
export { ImageGallery, type ImageGalleryProps } from './presentation/components/ImageGallery';
|
|
51
|
+
|
|
52
|
+
export { useImage } from './presentation/hooks/useImage';
|
|
53
|
+
export { useImageTransform } from './presentation/hooks/useImageTransform';
|
|
54
|
+
export { useImageConversion } from './presentation/hooks/useImageConversion';
|
|
50
55
|
|
|
51
56
|
export {
|
|
52
57
|
useImageGallery,
|
|
53
58
|
type UseImageGalleryReturn,
|
|
54
59
|
} from './presentation/hooks/useImageGallery';
|
|
55
60
|
|
|
61
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Conversion Service
|
|
3
|
+
*
|
|
4
|
+
* Handles format conversion, compression, and thumbnail generation.
|
|
5
|
+
* (Thumbnail is treated as a specialized compression/resize)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as ImageManipulator from 'expo-image-manipulator';
|
|
9
|
+
import type {
|
|
10
|
+
ImageSaveOptions,
|
|
11
|
+
ImageManipulationResult,
|
|
12
|
+
SaveFormat,
|
|
13
|
+
} from '../../domain/entities/ImageTypes';
|
|
14
|
+
import { IMAGE_CONSTANTS } from '../../domain/entities/ImageConstants';
|
|
15
|
+
import { ImageTransformService } from './ImageTransformService';
|
|
16
|
+
|
|
17
|
+
export class ImageConversionService {
|
|
18
|
+
static async compress(
|
|
19
|
+
uri: string,
|
|
20
|
+
quality: number = IMAGE_CONSTANTS.DEFAULT_QUALITY
|
|
21
|
+
): Promise<ImageManipulationResult | null> {
|
|
22
|
+
try {
|
|
23
|
+
return await ImageManipulator.manipulateAsync(
|
|
24
|
+
uri,
|
|
25
|
+
[],
|
|
26
|
+
{ compress: quality }
|
|
27
|
+
);
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static async convertFormat(
|
|
34
|
+
uri: string,
|
|
35
|
+
format: SaveFormat,
|
|
36
|
+
quality?: number
|
|
37
|
+
): Promise<ImageManipulationResult | null> {
|
|
38
|
+
try {
|
|
39
|
+
return await ImageManipulator.manipulateAsync(
|
|
40
|
+
uri,
|
|
41
|
+
[],
|
|
42
|
+
{
|
|
43
|
+
compress: quality || IMAGE_CONSTANTS.DEFAULT_QUALITY,
|
|
44
|
+
format: ImageTransformService.mapFormat(format),
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static async createThumbnail(
|
|
53
|
+
uri: string,
|
|
54
|
+
size: number = IMAGE_CONSTANTS.THUMBNAIL_SIZE,
|
|
55
|
+
options?: ImageSaveOptions
|
|
56
|
+
): Promise<ImageManipulationResult | null> {
|
|
57
|
+
try {
|
|
58
|
+
return ImageTransformService.resizeToFit(uri, size, size, {
|
|
59
|
+
...options,
|
|
60
|
+
compress: options?.compress || IMAGE_CONSTANTS.COMPRESS_QUALITY.MEDIUM,
|
|
61
|
+
});
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Storage Service
|
|
3
|
+
*
|
|
4
|
+
* Handles saving images to the device filesystem.
|
|
5
|
+
* Wraps @umituz/react-native-filesystem.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { FileSystemService } from '@umituz/react-native-filesystem';
|
|
9
|
+
|
|
10
|
+
export class ImageStorageService {
|
|
11
|
+
static async saveImage(
|
|
12
|
+
uri: string,
|
|
13
|
+
filename?: string
|
|
14
|
+
): Promise<string | null> {
|
|
15
|
+
try {
|
|
16
|
+
const result = await FileSystemService.copyToDocuments(uri, filename);
|
|
17
|
+
return result.success && result.uri ? result.uri : null;
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Transform Service
|
|
3
|
+
*
|
|
4
|
+
* Handles basic geometric transformations: resize, crop, rotate, flip.
|
|
5
|
+
* Also handles combined manipulations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as ImageManipulator from 'expo-image-manipulator';
|
|
9
|
+
import type {
|
|
10
|
+
ImageManipulateAction,
|
|
11
|
+
ImageSaveOptions,
|
|
12
|
+
ImageManipulationResult,
|
|
13
|
+
SaveFormat,
|
|
14
|
+
} from '../../domain/entities/ImageTypes';
|
|
15
|
+
import { IMAGE_CONSTANTS } from '../../domain/entities/ImageConstants';
|
|
16
|
+
import { ImageUtils } from '../../domain/utils/ImageUtils';
|
|
17
|
+
|
|
18
|
+
export class ImageTransformService {
|
|
19
|
+
/**
|
|
20
|
+
* Helper to map SaveFormat to Manipulator format
|
|
21
|
+
*/
|
|
22
|
+
static mapFormat(format?: SaveFormat): ImageManipulator.SaveFormat {
|
|
23
|
+
if (format === 'png') return ImageManipulator.SaveFormat.PNG;
|
|
24
|
+
if (format === 'webp') return ImageManipulator.SaveFormat.WEBP;
|
|
25
|
+
return ImageManipulator.SaveFormat.JPEG;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static async resize(
|
|
29
|
+
uri: string,
|
|
30
|
+
width?: number,
|
|
31
|
+
height?: number,
|
|
32
|
+
options?: ImageSaveOptions
|
|
33
|
+
): Promise<ImageManipulationResult | null> {
|
|
34
|
+
try {
|
|
35
|
+
return await ImageManipulator.manipulateAsync(
|
|
36
|
+
uri,
|
|
37
|
+
[{ resize: { width, height } }],
|
|
38
|
+
{
|
|
39
|
+
compress: options?.compress || IMAGE_CONSTANTS.DEFAULT_QUALITY,
|
|
40
|
+
format: ImageTransformService.mapFormat(options?.format),
|
|
41
|
+
base64: options?.base64,
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static async crop(
|
|
50
|
+
uri: string,
|
|
51
|
+
cropArea: { originX: number; originY: number; width: number; height: number },
|
|
52
|
+
options?: ImageSaveOptions
|
|
53
|
+
): Promise<ImageManipulationResult | null> {
|
|
54
|
+
try {
|
|
55
|
+
return await ImageManipulator.manipulateAsync(
|
|
56
|
+
uri,
|
|
57
|
+
[{ crop: cropArea }],
|
|
58
|
+
{
|
|
59
|
+
compress: options?.compress || IMAGE_CONSTANTS.DEFAULT_QUALITY,
|
|
60
|
+
format: ImageTransformService.mapFormat(options?.format),
|
|
61
|
+
base64: options?.base64,
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static async rotate(
|
|
70
|
+
uri: string,
|
|
71
|
+
degrees: number,
|
|
72
|
+
options?: ImageSaveOptions
|
|
73
|
+
): Promise<ImageManipulationResult | null> {
|
|
74
|
+
try {
|
|
75
|
+
return await ImageManipulator.manipulateAsync(
|
|
76
|
+
uri,
|
|
77
|
+
[{ rotate: degrees }],
|
|
78
|
+
{
|
|
79
|
+
compress: options?.compress || IMAGE_CONSTANTS.DEFAULT_QUALITY,
|
|
80
|
+
format: ImageTransformService.mapFormat(options?.format),
|
|
81
|
+
base64: options?.base64,
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static async flip(
|
|
90
|
+
uri: string,
|
|
91
|
+
flip: { horizontal?: boolean; vertical?: boolean },
|
|
92
|
+
options?: ImageSaveOptions
|
|
93
|
+
): Promise<ImageManipulationResult | null> {
|
|
94
|
+
try {
|
|
95
|
+
const actions: ImageManipulator.Action[] = [];
|
|
96
|
+
if (flip.horizontal) actions.push({ flip: ImageManipulator.FlipType.Horizontal });
|
|
97
|
+
if (flip.vertical) actions.push({ flip: ImageManipulator.FlipType.Vertical });
|
|
98
|
+
|
|
99
|
+
return await ImageManipulator.manipulateAsync(
|
|
100
|
+
uri,
|
|
101
|
+
actions,
|
|
102
|
+
{
|
|
103
|
+
compress: options?.compress || IMAGE_CONSTANTS.DEFAULT_QUALITY,
|
|
104
|
+
format: ImageTransformService.mapFormat(options?.format),
|
|
105
|
+
base64: options?.base64,
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static async manipulate(
|
|
114
|
+
uri: string,
|
|
115
|
+
action: ImageManipulateAction,
|
|
116
|
+
options?: ImageSaveOptions
|
|
117
|
+
): Promise<ImageManipulationResult | null> {
|
|
118
|
+
try {
|
|
119
|
+
const actions: ImageManipulator.Action[] = [];
|
|
120
|
+
if (action.resize) actions.push({ resize: action.resize });
|
|
121
|
+
if (action.crop) {
|
|
122
|
+
// @ts-ignore - guarded by check above
|
|
123
|
+
actions.push({ crop: action.crop });
|
|
124
|
+
}
|
|
125
|
+
if (action.rotate) actions.push({ rotate: action.rotate });
|
|
126
|
+
if (action.flip) {
|
|
127
|
+
if (action.flip.horizontal) actions.push({ flip: ImageManipulator.FlipType.Horizontal });
|
|
128
|
+
if (action.flip.vertical) actions.push({ flip: ImageManipulator.FlipType.Vertical });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return await ImageManipulator.manipulateAsync(
|
|
132
|
+
uri,
|
|
133
|
+
actions,
|
|
134
|
+
{
|
|
135
|
+
compress: options?.compress || IMAGE_CONSTANTS.DEFAULT_QUALITY,
|
|
136
|
+
format: ImageTransformService.mapFormat(options?.format),
|
|
137
|
+
base64: options?.base64,
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
static async resizeToFit(
|
|
146
|
+
uri: string,
|
|
147
|
+
maxWidth: number,
|
|
148
|
+
maxHeight: number,
|
|
149
|
+
options?: ImageSaveOptions
|
|
150
|
+
): Promise<ImageManipulationResult | null> {
|
|
151
|
+
try {
|
|
152
|
+
const dimensions = ImageUtils.fitToSize(maxWidth, maxHeight, maxWidth, maxHeight);
|
|
153
|
+
return ImageTransformService.resize(uri, dimensions.width, dimensions.height, options);
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
static async cropToSquare(
|
|
160
|
+
uri: string,
|
|
161
|
+
width: number,
|
|
162
|
+
height: number,
|
|
163
|
+
options?: ImageSaveOptions
|
|
164
|
+
): Promise<ImageManipulationResult | null> {
|
|
165
|
+
try {
|
|
166
|
+
const cropArea = ImageUtils.getSquareCrop(width, height);
|
|
167
|
+
return ImageTransformService.crop(uri, cropArea, options);
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Gallery Component
|
|
3
|
+
*
|
|
4
|
+
* A wrapper around react-native-image-viewing that provides
|
|
5
|
+
* theme integration and standard configuration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import ImageViewing from 'react-native-image-viewing';
|
|
10
|
+
import { useAppDesignTokens } from '@umituz/react-native-design-system-theme';
|
|
11
|
+
import type { ImageViewerItem, ImageGalleryOptions } from '../../domain/entities/ImageTypes';
|
|
12
|
+
|
|
13
|
+
export interface ImageGalleryProps extends ImageGalleryOptions {
|
|
14
|
+
images: ImageViewerItem[];
|
|
15
|
+
visible: boolean;
|
|
16
|
+
onDismiss: () => void;
|
|
17
|
+
index?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const ImageGallery: React.FC<ImageGalleryProps> = ({
|
|
21
|
+
images,
|
|
22
|
+
visible,
|
|
23
|
+
onDismiss,
|
|
24
|
+
index = 0,
|
|
25
|
+
backgroundColor,
|
|
26
|
+
swipeToCloseEnabled = true,
|
|
27
|
+
doubleTapToZoomEnabled = true,
|
|
28
|
+
onIndexChange,
|
|
29
|
+
}) => {
|
|
30
|
+
const tokens = useAppDesignTokens();
|
|
31
|
+
|
|
32
|
+
// Use theme background if not provided
|
|
33
|
+
const bg = backgroundColor || tokens.colors.backgroundPrimary;
|
|
34
|
+
|
|
35
|
+
// Map images to structure expected by ImageViewing (uri object)
|
|
36
|
+
const viewerImages = React.useMemo(() =>
|
|
37
|
+
images.map(img => ({ uri: img.uri })),
|
|
38
|
+
[images]
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<ImageViewing
|
|
43
|
+
images={viewerImages}
|
|
44
|
+
imageIndex={index}
|
|
45
|
+
visible={visible}
|
|
46
|
+
onRequestClose={onDismiss}
|
|
47
|
+
onImageIndexChange={onIndexChange}
|
|
48
|
+
backgroundColor={bg}
|
|
49
|
+
swipeToCloseEnabled={swipeToCloseEnabled}
|
|
50
|
+
doubleTapToZoomEnabled={doubleTapToZoomEnabled}
|
|
51
|
+
// Can add custom Header/Footer here using theme tokens if needed
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
};
|