@umituz/react-native-image 1.1.1 → 1.1.3
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-image",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
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",
|
|
@@ -56,4 +56,4 @@
|
|
|
56
56
|
"README.md",
|
|
57
57
|
"LICENSE"
|
|
58
58
|
]
|
|
59
|
-
}
|
|
59
|
+
}
|
|
@@ -77,7 +77,7 @@ export class ImageUtils {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
static getSquareCrop(width: number, height: number):
|
|
80
|
+
static getSquareCrop(width: number, height: number): { originX: number; originY: number; width: number; height: number } {
|
|
81
81
|
const size = Math.min(width, height);
|
|
82
82
|
const originX = (width - size) / 2;
|
|
83
83
|
const originY = (height - size) / 2;
|
package/src/index.ts
CHANGED
|
@@ -52,6 +52,7 @@ export { ImageGallery, type ImageGalleryProps } from './presentation/components/
|
|
|
52
52
|
export { useImage } from './presentation/hooks/useImage';
|
|
53
53
|
export { useImageTransform } from './presentation/hooks/useImageTransform';
|
|
54
54
|
export { useImageConversion } from './presentation/hooks/useImageConversion';
|
|
55
|
+
export { useImageEditor } from './presentation/hooks/useImageEditor';
|
|
55
56
|
|
|
56
57
|
export {
|
|
57
58
|
useImageGallery,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GalleryHeader Component
|
|
3
|
+
* Header for ImageGallery with edit and close buttons
|
|
4
|
+
*
|
|
5
|
+
* This component should be implemented by the consumer app
|
|
6
|
+
* using their own design system and safe area handling.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import { View, TouchableOpacity, StyleSheet, Text } from 'react-native';
|
|
11
|
+
|
|
12
|
+
interface GalleryHeaderProps {
|
|
13
|
+
onEdit: () => void;
|
|
14
|
+
onClose: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function GalleryHeader({ onEdit, onClose }: GalleryHeaderProps) {
|
|
18
|
+
return (
|
|
19
|
+
<View style={styles.container}>
|
|
20
|
+
<TouchableOpacity style={styles.button} onPress={onEdit}>
|
|
21
|
+
<Text style={styles.buttonText}>Edit</Text>
|
|
22
|
+
</TouchableOpacity>
|
|
23
|
+
|
|
24
|
+
<TouchableOpacity style={styles.button} onPress={onClose}>
|
|
25
|
+
<Text style={styles.buttonText}>✕</Text>
|
|
26
|
+
</TouchableOpacity>
|
|
27
|
+
</View>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const styles = StyleSheet.create({
|
|
32
|
+
container: {
|
|
33
|
+
position: 'absolute',
|
|
34
|
+
top: 0,
|
|
35
|
+
left: 0,
|
|
36
|
+
right: 0,
|
|
37
|
+
paddingTop: 48,
|
|
38
|
+
paddingHorizontal: 16,
|
|
39
|
+
paddingBottom: 16,
|
|
40
|
+
flexDirection: 'row',
|
|
41
|
+
justifyContent: 'space-between',
|
|
42
|
+
alignItems: 'center',
|
|
43
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
44
|
+
},
|
|
45
|
+
button: {
|
|
46
|
+
flexDirection: 'row',
|
|
47
|
+
alignItems: 'center',
|
|
48
|
+
gap: 8,
|
|
49
|
+
padding: 12,
|
|
50
|
+
borderRadius: 8,
|
|
51
|
+
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
|
52
|
+
},
|
|
53
|
+
buttonText: {
|
|
54
|
+
fontSize: 14,
|
|
55
|
+
color: '#FFFFFF',
|
|
56
|
+
fontWeight: '600',
|
|
57
|
+
},
|
|
58
|
+
});
|
|
@@ -2,19 +2,23 @@
|
|
|
2
2
|
* Image Gallery Component
|
|
3
3
|
*
|
|
4
4
|
* A wrapper around react-native-image-viewing that provides
|
|
5
|
-
* theme integration and
|
|
5
|
+
* theme integration, standard configuration, and optional editing.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import React from 'react';
|
|
8
|
+
import React, { useCallback } from 'react';
|
|
9
9
|
import ImageViewing from 'react-native-image-viewing';
|
|
10
|
+
import * as ImageManipulator from 'expo-image-manipulator';
|
|
10
11
|
import { useAppDesignTokens } from '@umituz/react-native-design-system-theme';
|
|
11
12
|
import type { ImageViewerItem, ImageGalleryOptions } from '../../domain/entities/ImageTypes';
|
|
13
|
+
import { GalleryHeader } from './GalleryHeader';
|
|
12
14
|
|
|
13
15
|
export interface ImageGalleryProps extends ImageGalleryOptions {
|
|
14
16
|
images: ImageViewerItem[];
|
|
15
17
|
visible: boolean;
|
|
16
18
|
onDismiss: () => void;
|
|
17
19
|
index?: number;
|
|
20
|
+
onImageChange?: (uri: string, index: number) => void | Promise<void>;
|
|
21
|
+
enableEditing?: boolean;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
export const ImageGallery: React.FC<ImageGalleryProps> = ({
|
|
@@ -26,29 +30,74 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
|
|
|
26
30
|
swipeToCloseEnabled = true,
|
|
27
31
|
doubleTapToZoomEnabled = true,
|
|
28
32
|
onIndexChange,
|
|
33
|
+
onImageChange,
|
|
34
|
+
enableEditing = false,
|
|
29
35
|
}) => {
|
|
30
36
|
const tokens = useAppDesignTokens();
|
|
37
|
+
const [currentIndex, setCurrentIndex] = React.useState(index);
|
|
38
|
+
|
|
39
|
+
React.useEffect(() => {
|
|
40
|
+
setCurrentIndex(index);
|
|
41
|
+
}, [index]);
|
|
31
42
|
|
|
32
|
-
// Use theme background if not provided
|
|
33
43
|
const bg = backgroundColor || tokens.colors.backgroundPrimary;
|
|
34
44
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
images.map(img => ({ uri: img.uri })),
|
|
45
|
+
const viewerImages = React.useMemo(
|
|
46
|
+
() => images.map((img) => ({ uri: img.uri })),
|
|
38
47
|
[images]
|
|
39
48
|
);
|
|
40
49
|
|
|
50
|
+
const handleEdit = useCallback(async () => {
|
|
51
|
+
const currentImage = images[currentIndex];
|
|
52
|
+
if (!currentImage || !onImageChange) return;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const result = await ImageManipulator.manipulateAsync(
|
|
56
|
+
currentImage.uri,
|
|
57
|
+
[],
|
|
58
|
+
{
|
|
59
|
+
compress: 1,
|
|
60
|
+
format: ImageManipulator.SaveFormat.JPEG,
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (result.uri) {
|
|
65
|
+
await onImageChange(result.uri, currentIndex);
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
// Silent fail
|
|
69
|
+
}
|
|
70
|
+
}, [images, currentIndex, onImageChange]);
|
|
71
|
+
|
|
72
|
+
const handleIndexChange = useCallback(
|
|
73
|
+
(newIndex: number) => {
|
|
74
|
+
setCurrentIndex(newIndex);
|
|
75
|
+
onIndexChange?.(newIndex);
|
|
76
|
+
},
|
|
77
|
+
[onIndexChange]
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const headerComponent = useCallback(() => {
|
|
81
|
+
if (!enableEditing) return null;
|
|
82
|
+
return (
|
|
83
|
+
<GalleryHeader
|
|
84
|
+
onEdit={handleEdit}
|
|
85
|
+
onClose={onDismiss}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
}, [enableEditing, handleEdit, onDismiss]);
|
|
89
|
+
|
|
41
90
|
return (
|
|
42
91
|
<ImageViewing
|
|
43
92
|
images={viewerImages}
|
|
44
|
-
imageIndex={
|
|
93
|
+
imageIndex={currentIndex}
|
|
45
94
|
visible={visible}
|
|
46
95
|
onRequestClose={onDismiss}
|
|
47
|
-
onImageIndexChange={
|
|
96
|
+
onImageIndexChange={handleIndexChange}
|
|
48
97
|
backgroundColor={bg}
|
|
49
98
|
swipeToCloseEnabled={swipeToCloseEnabled}
|
|
50
99
|
doubleTapToZoomEnabled={doubleTapToZoomEnabled}
|
|
51
|
-
|
|
100
|
+
HeaderComponent={headerComponent}
|
|
52
101
|
/>
|
|
53
102
|
);
|
|
54
103
|
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useImageEditor Hook
|
|
3
|
+
* Provides image editing functionality with crop, rotate, flip
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback } from 'react';
|
|
7
|
+
import * as ImageManipulator from 'expo-image-manipulator';
|
|
8
|
+
import type { Action } from 'expo-image-manipulator';
|
|
9
|
+
|
|
10
|
+
interface UseImageEditorOptions {
|
|
11
|
+
onSave?: (uri: string) => void | Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useImageEditor({ onSave }: UseImageEditorOptions = {}) {
|
|
15
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
16
|
+
const [currentUri, setCurrentUri] = useState<string | null>(null);
|
|
17
|
+
|
|
18
|
+
const startEditing = useCallback((uri: string) => {
|
|
19
|
+
setCurrentUri(uri);
|
|
20
|
+
setIsEditing(true);
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
const cancelEditing = useCallback(() => {
|
|
24
|
+
setIsEditing(false);
|
|
25
|
+
setCurrentUri(null);
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
const saveEdit = useCallback(
|
|
29
|
+
async (actions: Action[]) => {
|
|
30
|
+
if (!currentUri) return;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const result = await ImageManipulator.manipulateAsync(
|
|
34
|
+
currentUri,
|
|
35
|
+
actions,
|
|
36
|
+
{ compress: 0.9, format: ImageManipulator.SaveFormat.JPEG }
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (onSave) {
|
|
40
|
+
await onSave(result.uri);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
setIsEditing(false);
|
|
44
|
+
setCurrentUri(null);
|
|
45
|
+
|
|
46
|
+
return result.uri;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
[currentUri, onSave]
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
isEditing,
|
|
56
|
+
currentUri,
|
|
57
|
+
startEditing,
|
|
58
|
+
cancelEditing,
|
|
59
|
+
saveEdit,
|
|
60
|
+
};
|
|
61
|
+
}
|