@umituz/react-native-image 1.1.6 → 1.2.2

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.
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Presentation - Filter Slider Component
3
+ *
4
+ * Slider for adjusting filter parameters
5
+ */
6
+
7
+ import React from 'react';
8
+ import { View, StyleSheet, Text } from 'react-native';
9
+ import Slider from '@react-native-community/slider';
10
+
11
+ interface FilterSliderProps {
12
+ label: string;
13
+ value: number;
14
+ onValueChange: (value: number) => void;
15
+ minimumValue?: number;
16
+ maximumValue?: number;
17
+ step?: number;
18
+ valueFormat?: (value: number) => string;
19
+ color?: string;
20
+ }
21
+
22
+ export function FilterSlider({
23
+ label,
24
+ value,
25
+ onValueChange,
26
+ minimumValue = 0,
27
+ maximumValue = 100,
28
+ step = 1,
29
+ valueFormat,
30
+ color = '#007bff',
31
+ }: FilterSliderProps) {
32
+ const formatValue = (val: number): string => {
33
+ if (valueFormat) return valueFormat(val);
34
+ return `${Math.round(val)}`;
35
+ };
36
+
37
+ return (
38
+ <View style={styles.container}>
39
+ <View style={styles.labelContainer}>
40
+ <Text style={styles.label}>{label}</Text>
41
+ <Text style={[styles.value, { color }]}>
42
+ {formatValue(value)}
43
+ </Text>
44
+ </View>
45
+
46
+ <View style={[styles.sliderTrack, { backgroundColor: `${color}20` }]}>
47
+ <View
48
+ style={[
49
+ styles.sliderFill,
50
+ {
51
+ width: `${((value - minimumValue) / (maximumValue - minimumValue)) * 100}%`,
52
+ backgroundColor: color
53
+ }
54
+ ]}
55
+ />
56
+ <Slider
57
+ style={styles.slider}
58
+ minimumValue={minimumValue}
59
+ maximumValue={maximumValue}
60
+ value={value}
61
+ onValueChange={onValueChange}
62
+ step={step}
63
+ minimumTrackTintColor="transparent"
64
+ maximumTrackTintColor="transparent"
65
+ thumbTintColor={color}
66
+ />
67
+ </View>
68
+ </View>
69
+ );
70
+ }
71
+
72
+ const styles = StyleSheet.create({
73
+ container: {
74
+ paddingVertical: 8,
75
+ gap: 8,
76
+ },
77
+ labelContainer: {
78
+ flexDirection: 'row',
79
+ justifyContent: 'space-between',
80
+ alignItems: 'center',
81
+ },
82
+ label: {
83
+ fontSize: 14,
84
+ fontWeight: '500',
85
+ color: '#333333',
86
+ },
87
+ value: {
88
+ fontSize: 14,
89
+ fontWeight: '600',
90
+ },
91
+ sliderTrack: {
92
+ height: 6,
93
+ borderRadius: 3,
94
+ position: 'relative',
95
+ },
96
+ sliderFill: {
97
+ height: '100%',
98
+ borderRadius: 3,
99
+ position: 'absolute',
100
+ left: 0,
101
+ top: 0,
102
+ },
103
+ slider: {
104
+ position: 'absolute',
105
+ left: 0,
106
+ top: 0,
107
+ width: '100%',
108
+ height: '100%',
109
+ opacity: 0,
110
+ },
111
+ thumb: {
112
+ width: 20,
113
+ height: 20,
114
+ borderRadius: 10,
115
+ borderWidth: 2,
116
+ borderColor: '#ffffff',
117
+ shadowColor: '#000000',
118
+ shadowOffset: { width: 0, height: 2 },
119
+ shadowOpacity: 0.2,
120
+ shadowRadius: 2,
121
+ elevation: 3,
122
+ },
123
+ });
@@ -1,32 +1,66 @@
1
1
  /**
2
2
  * Presentation - Gallery Header Component
3
3
  *
4
- * NOTE: This component should be implemented by consumer app
5
- * using their design system and safe area handling
4
+ * High-performance, premium header for the Image Gallery.
5
+ * Uses design system tokens and handles safe areas.
6
6
  */
7
7
 
8
8
  import React from 'react';
9
- import { View, TouchableOpacity, StyleSheet, Text } from 'react-native';
9
+ import { View, TouchableOpacity, StyleSheet } from 'react-native';
10
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
11
+ import { useAppDesignTokens } from '@umituz/react-native-design-system-theme';
12
+ import { AtomicText } from '@umituz/react-native-design-system-atoms';
10
13
 
11
14
  interface GalleryHeaderProps {
12
15
  onEdit?: () => void;
13
16
  onClose: () => void;
17
+ title?: string;
14
18
  }
15
19
 
16
- export function GalleryHeader({ onEdit, onClose }: GalleryHeaderProps) {
20
+ export function GalleryHeader({ onEdit, onClose, title }: GalleryHeaderProps) {
21
+ const insets = useSafeAreaInsets();
22
+ const tokens = useAppDesignTokens();
23
+
17
24
  return (
18
- <View style={styles.container}>
19
- {onEdit ? (
20
- <TouchableOpacity style={styles.button} onPress={onEdit}>
21
- <Text style={styles.buttonText}>Edit</Text>
22
- </TouchableOpacity>
23
- ) : (
24
- <View style={styles.spacer} />
25
- )}
25
+ <View style={[
26
+ styles.container,
27
+ {
28
+ paddingTop: Math.max(insets.top, 24),
29
+ backgroundColor: 'rgba(0, 0, 0, 0.4)'
30
+ }
31
+ ]}>
32
+ <View style={styles.leftSection}>
33
+ {onEdit ? (
34
+ <TouchableOpacity
35
+ style={[styles.actionButton, { backgroundColor: 'rgba(255, 255, 255, 0.15)' }]}
36
+ onPress={onEdit}
37
+ activeOpacity={0.7}
38
+ >
39
+ <AtomicText style={styles.buttonText}>Edit</AtomicText>
40
+ </TouchableOpacity>
41
+ ) : (
42
+ <View style={styles.spacer} />
43
+ )}
44
+ </View>
45
+
46
+ <View style={styles.centerSection}>
47
+ {title ? (
48
+ <AtomicText type="bodyMedium" style={styles.titleText}>
49
+ {title}
50
+ </AtomicText>
51
+ ) : null}
52
+ </View>
26
53
 
27
- <TouchableOpacity style={styles.button} onPress={onClose}>
28
- <Text style={styles.buttonText}>✕</Text>
29
- </TouchableOpacity>
54
+ <View style={styles.rightSection}>
55
+ <TouchableOpacity
56
+ style={[styles.closeButton, { backgroundColor: 'rgba(0, 0, 0, 0.4)' }]}
57
+ onPress={onClose}
58
+ activeOpacity={0.7}
59
+ hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
60
+ >
61
+ <AtomicText style={styles.closeIcon}>✕</AtomicText>
62
+ </TouchableOpacity>
63
+ </View>
30
64
  </View>
31
65
  );
32
66
  }
@@ -37,28 +71,56 @@ const styles = StyleSheet.create({
37
71
  top: 0,
38
72
  left: 0,
39
73
  right: 0,
40
- paddingTop: 48,
41
- paddingHorizontal: 16,
74
+ paddingHorizontal: 20,
42
75
  paddingBottom: 16,
43
76
  flexDirection: 'row',
44
77
  justifyContent: 'space-between',
45
78
  alignItems: 'center',
46
- backgroundColor: 'rgba(0, 0, 0, 0.5)',
79
+ zIndex: 1000,
80
+ },
81
+ leftSection: {
82
+ flex: 1,
83
+ alignItems: 'flex-start',
84
+ },
85
+ centerSection: {
86
+ flex: 2,
87
+ alignItems: 'center',
88
+ },
89
+ rightSection: {
90
+ flex: 1,
91
+ alignItems: 'flex-end',
47
92
  },
48
93
  spacer: {
49
- width: 48,
94
+ width: 44,
95
+ height: 44,
50
96
  },
51
- button: {
52
- flexDirection: 'row',
97
+ actionButton: {
98
+ paddingVertical: 8,
99
+ paddingHorizontal: 16,
100
+ borderRadius: 20,
101
+ justifyContent: 'center',
102
+ alignItems: 'center',
103
+ },
104
+ closeButton: {
105
+ width: 50,
106
+ height: 50,
107
+ borderRadius: 25,
108
+ justifyContent: 'center',
53
109
  alignItems: 'center',
54
- gap: 8,
55
- padding: 12,
56
- borderRadius: 8,
57
- backgroundColor: 'rgba(255, 255, 255, 0.1)',
58
110
  },
59
111
  buttonText: {
60
112
  fontSize: 14,
113
+ color: '#FFFFFF',
114
+ fontWeight: 'bold',
115
+ },
116
+ closeIcon: {
117
+ fontSize: 28,
118
+ color: '#FFFFFF',
119
+ fontWeight: '300',
120
+ },
121
+ titleText: {
61
122
  color: '#FFFFFF',
62
123
  fontWeight: '600',
124
+ textAlign: 'center',
63
125
  },
64
126
  });
@@ -1,15 +1,19 @@
1
1
  /**
2
2
  * Presentation - Image Gallery Component
3
3
  *
4
- * Wrapper around react-native-image-viewing with theme integration
4
+ * High-performance, premium image gallery using expo-image.
5
+ * Replaces slow standard image components for instant loading.
5
6
  */
6
7
 
7
- import React, { useCallback } from 'react';
8
- import ImageViewing from 'react-native-image-viewing';
9
- // import { useAppDesignTokens } from '@umituz/react-native-design-system-theme';
8
+ import React, { useCallback, useMemo, useState, useEffect } from 'react';
9
+ import { Modal, View, StyleSheet, FlatList, Dimensions, TouchableOpacity } from 'react-native';
10
+ import { Image } from 'expo-image';
11
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
10
12
  import type { ImageViewerItem, ImageGalleryOptions } from '../../domain/entities/ImageTypes';
11
13
  import { GalleryHeader } from './GalleryHeader';
12
14
 
15
+ const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
16
+
13
17
  export interface ImageGalleryProps extends ImageGalleryOptions {
14
18
  images: ImageViewerItem[];
15
19
  visible: boolean;
@@ -17,6 +21,7 @@ export interface ImageGalleryProps extends ImageGalleryOptions {
17
21
  index?: number;
18
22
  onImageChange?: (uri: string, index: number) => void | Promise<void>;
19
23
  enableEditing?: boolean;
24
+ title?: string;
20
25
  }
21
26
 
22
27
  export const ImageGallery: React.FC<ImageGalleryProps> = ({
@@ -24,66 +29,110 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
24
29
  visible,
25
30
  onDismiss,
26
31
  index = 0,
27
- backgroundColor,
28
- swipeToCloseEnabled = true,
29
- doubleTapToZoomEnabled = true,
32
+ backgroundColor = '#000000',
30
33
  onIndexChange,
31
34
  onImageChange,
32
35
  enableEditing = false,
36
+ title,
33
37
  }) => {
34
- // const tokens = useAppDesignTokens();
35
- const [currentIndex, setCurrentIndex] = React.useState(index);
36
-
37
- React.useEffect(() => {
38
- setCurrentIndex(index);
39
- }, [index]);
40
-
41
- const bg = backgroundColor || '#000000';
38
+ const insets = useSafeAreaInsets();
39
+ const [currentIndex, setCurrentIndex] = useState(index);
42
40
 
43
- const viewerImages = React.useMemo(
44
- () => images.map((img) => ({ uri: img.uri })),
45
- [images]
46
- );
41
+ useEffect(() => {
42
+ if (visible) setCurrentIndex(index);
43
+ }, [visible, index]);
47
44
 
48
45
  const handleEdit = useCallback(async () => {
49
46
  const currentImage = images[currentIndex];
50
47
  if (!currentImage || !onImageChange) return;
51
-
52
- try {
53
- await onImageChange(currentImage.uri, currentIndex);
54
- } catch (error) {
55
- // Consumer should handle editing logic
56
- }
48
+ await onImageChange(currentImage.uri, currentIndex);
57
49
  }, [images, currentIndex, onImageChange]);
58
50
 
59
- const handleIndexChange = useCallback(
60
- (newIndex: number) => {
61
- setCurrentIndex(newIndex);
62
- onIndexChange?.(newIndex);
63
- },
64
- [onIndexChange]
65
- );
51
+ const handleScroll = useCallback((event: any) => {
52
+ const nextIndex = Math.round(event.nativeEvent.contentOffset.x / SCREEN_WIDTH);
53
+ if (nextIndex !== currentIndex) {
54
+ setCurrentIndex(nextIndex);
55
+ onIndexChange?.(nextIndex);
56
+ }
57
+ }, [currentIndex, onIndexChange]);
66
58
 
67
- const headerComponent = useCallback(() => {
68
- return (
69
- <GalleryHeader
70
- onEdit={enableEditing ? handleEdit : undefined}
71
- onClose={onDismiss}
59
+ const renderItem = useCallback(({ item }: { item: ImageViewerItem }) => (
60
+ <View style={styles.imageWrapper}>
61
+ <Image
62
+ source={{ uri: item.uri }}
63
+ style={styles.fullImage}
64
+ contentFit="contain"
65
+ transition={200}
66
+ cachePolicy="memory-disk"
72
67
  />
73
- );
74
- }, [enableEditing, handleEdit, onDismiss]);
68
+ </View>
69
+ ), []);
70
+
71
+ if (!visible && !currentIndex) return null;
75
72
 
76
73
  return (
77
- <ImageViewing
78
- images={viewerImages}
79
- imageIndex={currentIndex}
74
+ <Modal
80
75
  visible={visible}
76
+ transparent
77
+ animationType="fade"
81
78
  onRequestClose={onDismiss}
82
- onImageIndexChange={handleIndexChange}
83
- backgroundColor={bg}
84
- swipeToCloseEnabled={swipeToCloseEnabled}
85
- doubleTapToZoomEnabled={doubleTapToZoomEnabled}
86
- HeaderComponent={headerComponent}
87
- />
79
+ statusBarTranslucent
80
+ >
81
+ <View style={[styles.container, { backgroundColor }]}>
82
+ <GalleryHeader
83
+ onClose={onDismiss}
84
+ onEdit={enableEditing ? handleEdit : undefined}
85
+ title={title || `${currentIndex + 1} / ${images.length}`}
86
+ />
87
+
88
+ <FlatList
89
+ data={images}
90
+ renderItem={renderItem}
91
+ horizontal
92
+ pagingEnabled
93
+ showsHorizontalScrollIndicator={false}
94
+ initialScrollIndex={index}
95
+ getItemLayout={(_, i) => ({
96
+ length: SCREEN_WIDTH,
97
+ offset: SCREEN_WIDTH * i,
98
+ index: i,
99
+ })}
100
+ onScroll={handleScroll}
101
+ scrollEventThrottle={16}
102
+ keyExtractor={(item, i) => `${item.uri}-${i}`}
103
+ style={styles.list}
104
+ />
105
+
106
+ <View style={[styles.footer, { paddingBottom: Math.max(insets.bottom, 20) }]}>
107
+ {/* Potential for thumbnail strip or captions in future */}
108
+ </View>
109
+ </View>
110
+ </Modal>
88
111
  );
89
112
  };
113
+
114
+ const styles = StyleSheet.create({
115
+ container: {
116
+ flex: 1,
117
+ },
118
+ list: {
119
+ flex: 1,
120
+ },
121
+ imageWrapper: {
122
+ width: SCREEN_WIDTH,
123
+ height: SCREEN_HEIGHT,
124
+ justifyContent: 'center',
125
+ alignItems: 'center',
126
+ },
127
+ fullImage: {
128
+ width: '100%',
129
+ height: '100%',
130
+ },
131
+ footer: {
132
+ position: 'absolute',
133
+ bottom: 0,
134
+ left: 0,
135
+ right: 0,
136
+ alignItems: 'center',
137
+ }
138
+ });
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Presentation - Editor Tool Hook
3
+ *
4
+ * Manages individual editor tools and their states
5
+ */
6
+
7
+ import { useState, useCallback } from 'react';
8
+ import { DrawingEngine, type DrawingPath, type DrawingConfig } from '../../infrastructure/utils/DrawingEngine';
9
+ import { CropTool, type CropConfig } from '../../infrastructure/utils/CropTool';
10
+ import { TextEditor, type TextFormatting } from '../../infrastructure/utils/TextEditor';
11
+ import { ShapeRenderer, type ShapeStyle } from '../../infrastructure/utils/ShapeRenderer';
12
+
13
+ interface ToolState {
14
+ isDrawing: boolean;
15
+ currentPath: DrawingPath;
16
+ startPoint?: { x: number; y: number };
17
+ currentTool: string;
18
+ }
19
+
20
+ interface UseEditorToolsOptions {
21
+ onStateChange?: (state: any) => void;
22
+ canvas?: HTMLCanvasElement | null;
23
+ }
24
+
25
+ export function useEditorTools({ onStateChange, canvas }: UseEditorToolsOptions = {}) {
26
+ const [toolState, setToolState] = useState<ToolState>({
27
+ isDrawing: false,
28
+ currentPath: [],
29
+ currentTool: 'move',
30
+ });
31
+
32
+ const [drawingConfig, setDrawingConfig] = useState<DrawingConfig>({
33
+ color: '#000000',
34
+ size: 5,
35
+ opacity: 1,
36
+ style: 'normal',
37
+ smoothing: true,
38
+ });
39
+
40
+ const [cropConfig, setCropConfig] = useState<CropConfig>({
41
+ aspectRatio: undefined,
42
+ lockAspectRatio: false,
43
+ showGrid: true,
44
+ });
45
+
46
+ const [textFormatting, setTextFormatting] = useState<TextFormatting>({
47
+ fontSize: 16,
48
+ fontFamily: 'Arial',
49
+ fontWeight: 'normal',
50
+ fontStyle: 'normal',
51
+ color: '#000000',
52
+ textAlign: 'left',
53
+ opacity: 1,
54
+ });
55
+
56
+ const setTool = useCallback((tool: string) => {
57
+ const newState = { ...toolState, currentTool: tool };
58
+ setToolState(newState);
59
+ onStateChange?.(newState);
60
+ }, [toolState, onStateChange]);
61
+
62
+ const startDrawing = useCallback((point: { x: number; y: number }) => {
63
+ if (!canvas) return;
64
+
65
+ const ctx = canvas.getContext('2d');
66
+ if (!ctx) return;
67
+
68
+ const newState = {
69
+ ...toolState,
70
+ isDrawing: true,
71
+ startPoint: point,
72
+ currentPath: [point],
73
+ };
74
+ setToolState(newState);
75
+ }, [toolState, canvas]);
76
+
77
+ const draw = useCallback((point: { x: number; y: number }) => {
78
+ if (!toolState.isDrawing || !canvas) return;
79
+
80
+ const ctx = canvas.getContext('2d');
81
+ if (!ctx) return;
82
+
83
+ const newPath = [...toolState.currentPath, point];
84
+
85
+ setToolState(prev => ({
86
+ ...prev,
87
+ currentPath: newPath,
88
+ }));
89
+
90
+ // Real-time drawing feedback
91
+ const tempPath = [...toolState.currentPath, point];
92
+ DrawingEngine.drawStroke(ctx, tempPath, drawingConfig);
93
+ }, [toolState.isDrawing, toolState.currentPath, drawingConfig, canvas]);
94
+
95
+ const stopDrawing = useCallback(() => {
96
+ if (!toolState.isDrawing || !canvas) return;
97
+
98
+ const ctx = canvas.getContext('2d');
99
+ if (!ctx) return;
100
+
101
+ // Finalize the stroke
102
+ DrawingEngine.drawStroke(ctx, toolState.currentPath, drawingConfig);
103
+
104
+ const newState = {
105
+ ...toolState,
106
+ isDrawing: false,
107
+ currentPath: [],
108
+ startPoint: undefined,
109
+ };
110
+ setToolState(newState);
111
+ onStateChange?.(newState);
112
+ }, [toolState, drawingConfig, canvas, onStateChange]);
113
+
114
+ const drawShape = useCallback((
115
+ type: 'rectangle' | 'circle' | 'line' | 'arrow',
116
+ startPoint: { x: number; y: number },
117
+ endPoint: { x: number; y: number },
118
+ style: ShapeStyle
119
+ ) => {
120
+ if (!canvas) return;
121
+
122
+ const ctx = canvas.getContext('2d');
123
+ if (!ctx) return;
124
+
125
+ DrawingEngine.drawShape(ctx, type, startPoint, endPoint, style as any);
126
+ onStateChange?.({ toolState, shape: { type, startPoint, endPoint, style } });
127
+ }, [canvas, onStateChange, toolState]);
128
+
129
+ const addText = useCallback((
130
+ text: string,
131
+ position: { x: number; y: number },
132
+ formatting?: Partial<TextFormatting>
133
+ ) => {
134
+ if (!canvas) return;
135
+
136
+ const ctx = canvas.getContext('2d');
137
+ if (!ctx) return;
138
+
139
+ const finalFormatting = { ...textFormatting, ...formatting };
140
+ TextEditor.renderText(ctx, text, position, finalFormatting);
141
+
142
+ setTextFormatting(finalFormatting);
143
+ onStateChange?.({ toolState, text: { text, position, formatting: finalFormatting } });
144
+ }, [textFormatting, canvas, onStateChange, toolState]);
145
+
146
+ const setCropArea = useCallback((cropArea: any) => {
147
+ if (!canvas) return;
148
+
149
+ const ctx = canvas.getContext('2d');
150
+ if (!ctx) return;
151
+
152
+ CropTool.drawCropOverlay(ctx, cropArea, cropConfig);
153
+ onStateChange?.({ toolState, cropArea });
154
+ }, [cropConfig, canvas, onStateChange, toolState]);
155
+
156
+ const clearCanvas = useCallback(() => {
157
+ if (!canvas) return;
158
+
159
+ const ctx = canvas.getContext('2d');
160
+ if (!ctx) return;
161
+
162
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
163
+ onStateChange?.({ toolState, cleared: true });
164
+ }, [canvas, onStateChange, toolState]);
165
+
166
+ return {
167
+ // State
168
+ toolState,
169
+ drawingConfig,
170
+ cropConfig,
171
+ textFormatting,
172
+
173
+ // Actions
174
+ setTool,
175
+ startDrawing,
176
+ draw,
177
+ stopDrawing,
178
+ drawShape,
179
+ addText,
180
+ setCropArea,
181
+ clearCanvas,
182
+
183
+ // Config setters
184
+ setDrawingConfig,
185
+ setCropConfig,
186
+ setTextFormatting,
187
+ };
188
+ }