@umituz/react-native-image 1.3.9 → 1.3.10
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 +8 -2
- package/src/index.ts +3 -0
- package/src/infrastructure/services/ImageConversionService.ts +7 -32
- package/src/infrastructure/services/ImageEditorService.ts +41 -179
- package/src/infrastructure/services/ImageTransformService.ts +28 -168
- package/src/infrastructure/utils/FilterProcessor.ts +21 -236
- package/src/infrastructure/utils/ImageEditorHistoryUtils.ts +63 -0
- package/src/infrastructure/utils/ImageFilterUtils.ts +152 -0
- package/src/infrastructure/utils/ImageTransformUtils.ts +25 -0
- package/src/presentation/components/editor/FilterPickerSheet.tsx +75 -0
- package/src/presentation/components/editor/StickerPickerSheet.tsx +62 -0
- package/src/presentation/components/editor/TextEditorSheet.tsx +98 -0
- package/src/presentation/components/editor/TextEditorTabs.tsx +111 -0
- package/src/presentation/hooks/useImage.ts +5 -8
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentation - Sticker Picker Sheet
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React, { forwardRef } from 'react';
|
|
6
|
+
import { View, TouchableOpacity, ScrollView, Image, Dimensions } from 'react-native';
|
|
7
|
+
import {
|
|
8
|
+
BottomSheetModal,
|
|
9
|
+
AtomicText,
|
|
10
|
+
useAppDesignTokens,
|
|
11
|
+
BottomSheetModalRef
|
|
12
|
+
} from '@umituz/react-native-design-system';
|
|
13
|
+
|
|
14
|
+
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
|
15
|
+
|
|
16
|
+
export interface StickerPickerSheetProps {
|
|
17
|
+
stickers: string[];
|
|
18
|
+
onSelectSticker: (uri: string) => void;
|
|
19
|
+
onDismiss: () => void;
|
|
20
|
+
title?: string;
|
|
21
|
+
snapPoints?: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const StickerPickerSheet = forwardRef<BottomSheetModalRef, StickerPickerSheetProps>(
|
|
25
|
+
({ stickers, onSelectSticker, onDismiss, title = 'Select Sticker', snapPoints = ['60%'] }, ref) => {
|
|
26
|
+
const tokens = useAppDesignTokens();
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<BottomSheetModal ref={ref} snapPoints={snapPoints} onDismiss={onDismiss}>
|
|
30
|
+
<View style={{ padding: tokens.spacing.lg, flex: 1 }}>
|
|
31
|
+
<AtomicText style={{ ...tokens.typography.headingSmall, marginBottom: tokens.spacing.md }}>
|
|
32
|
+
{title}
|
|
33
|
+
</AtomicText>
|
|
34
|
+
|
|
35
|
+
<ScrollView showsVerticalScrollIndicator={false}>
|
|
36
|
+
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: tokens.spacing.md }}>
|
|
37
|
+
{stickers.map((uri, index) => (
|
|
38
|
+
<TouchableOpacity
|
|
39
|
+
key={index}
|
|
40
|
+
onPress={() => onSelectSticker(uri)}
|
|
41
|
+
style={{
|
|
42
|
+
width: (SCREEN_WIDTH - 64) / 3,
|
|
43
|
+
aspectRatio: 1,
|
|
44
|
+
backgroundColor: tokens.colors.surfaceVariant,
|
|
45
|
+
borderRadius: tokens.borderRadius.md,
|
|
46
|
+
padding: tokens.spacing.sm,
|
|
47
|
+
alignItems: 'center',
|
|
48
|
+
justifyContent: 'center',
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<Image source={{ uri }} style={{ width: '100%', height: '100%' }} resizeMode="contain" />
|
|
52
|
+
</TouchableOpacity>
|
|
53
|
+
))}
|
|
54
|
+
</View>
|
|
55
|
+
</ScrollView>
|
|
56
|
+
</View>
|
|
57
|
+
</BottomSheetModal>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
StickerPickerSheet.displayName = 'StickerPickerSheet';
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentation - Text Editor Sheet
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React, { forwardRef, useState } from 'react';
|
|
6
|
+
import { View, TouchableOpacity, ScrollView } from 'react-native';
|
|
7
|
+
import {
|
|
8
|
+
BottomSheetModal,
|
|
9
|
+
AtomicText,
|
|
10
|
+
AtomicIcon,
|
|
11
|
+
useAppDesignTokens,
|
|
12
|
+
BottomSheetModalRef
|
|
13
|
+
} from '@umituz/react-native-design-system';
|
|
14
|
+
import { TextContentTab, TextStyleTab, TextTransformTab } from './TextEditorTabs';
|
|
15
|
+
|
|
16
|
+
export interface TextEditorSheetProps {
|
|
17
|
+
text: string;
|
|
18
|
+
onTextChange: (text: string) => void;
|
|
19
|
+
color: string;
|
|
20
|
+
onColorChange: (color: string) => void;
|
|
21
|
+
fontSize: number;
|
|
22
|
+
onFontSizeChange: (size: number) => void;
|
|
23
|
+
fontFamily: string;
|
|
24
|
+
onFontFamilyChange: (font: string) => void;
|
|
25
|
+
scale: number;
|
|
26
|
+
onScaleChange: (scale: number) => void;
|
|
27
|
+
rotation: number;
|
|
28
|
+
onRotationChange: (rotation: number) => void;
|
|
29
|
+
opacity: number;
|
|
30
|
+
onOpacityChange: (opacity: number) => void;
|
|
31
|
+
onDelete?: () => void;
|
|
32
|
+
onDismiss: () => void;
|
|
33
|
+
snapPoints?: string[];
|
|
34
|
+
t: (key: string) => string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const TextEditorSheet = forwardRef<BottomSheetModalRef, TextEditorSheetProps>((props, ref) => {
|
|
38
|
+
const tokens = useAppDesignTokens();
|
|
39
|
+
const [activeTab, setActiveTab] = useState<'content' | 'style' | 'transform'>('content');
|
|
40
|
+
const { onDismiss, snapPoints = ['75%'], t } = props;
|
|
41
|
+
|
|
42
|
+
const tabs = [
|
|
43
|
+
{ id: 'content', label: 'Text', icon: 'text' },
|
|
44
|
+
{ id: 'style', label: 'Style', icon: 'color-palette' },
|
|
45
|
+
{ id: 'transform', label: 'Edit', icon: 'options' },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<BottomSheetModal ref={ref} snapPoints={snapPoints} onDismiss={onDismiss}>
|
|
50
|
+
<View style={{ padding: tokens.spacing.lg, flex: 1 }}>
|
|
51
|
+
<View style={{ flexDirection: 'row', gap: tokens.spacing.md, marginBottom: tokens.spacing.lg }}>
|
|
52
|
+
{tabs.map(tab => (
|
|
53
|
+
<TouchableOpacity
|
|
54
|
+
key={tab.id}
|
|
55
|
+
onPress={() => setActiveTab(tab.id as any)}
|
|
56
|
+
style={{
|
|
57
|
+
flex: 1, alignItems: 'center', paddingVertical: tokens.spacing.sm,
|
|
58
|
+
borderBottomWidth: 3, borderBottomColor: activeTab === tab.id ? tokens.colors.primary : 'transparent'
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
<AtomicIcon name={tab.icon as any} size={20} color={activeTab === tab.id ? 'primary' : 'secondary'} />
|
|
62
|
+
<AtomicText style={{
|
|
63
|
+
...tokens.typography.labelSmall,
|
|
64
|
+
color: activeTab === tab.id ? tokens.colors.primary : tokens.colors.textSecondary,
|
|
65
|
+
marginTop: 4
|
|
66
|
+
}}>
|
|
67
|
+
{tab.label}
|
|
68
|
+
</AtomicText>
|
|
69
|
+
</TouchableOpacity>
|
|
70
|
+
))}
|
|
71
|
+
</View>
|
|
72
|
+
|
|
73
|
+
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingBottom: 60 }}>
|
|
74
|
+
{activeTab === 'content' && <TextContentTab text={props.text} onTextChange={props.onTextChange} t={t} />}
|
|
75
|
+
{activeTab === 'style' && (
|
|
76
|
+
<TextStyleTab
|
|
77
|
+
fontSize={props.fontSize} setFontSize={props.onFontSizeChange}
|
|
78
|
+
color={props.color} setColor={props.onColorChange}
|
|
79
|
+
fontFamily={props.fontFamily} setFontFamily={props.onFontFamilyChange}
|
|
80
|
+
t={t}
|
|
81
|
+
/>
|
|
82
|
+
)}
|
|
83
|
+
{activeTab === 'transform' && (
|
|
84
|
+
<TextTransformTab
|
|
85
|
+
scale={props.scale} setScale={props.onScaleChange}
|
|
86
|
+
rotation={props.rotation} setRotation={props.onRotationChange}
|
|
87
|
+
opacity={props.opacity} setOpacity={props.onOpacityChange}
|
|
88
|
+
onDelete={props.onDelete}
|
|
89
|
+
t={t}
|
|
90
|
+
/>
|
|
91
|
+
)}
|
|
92
|
+
</ScrollView>
|
|
93
|
+
</View>
|
|
94
|
+
</BottomSheetModal>
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
TextEditorSheet.displayName = 'TextEditorSheet';
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentation - Text Editor Tabs
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { View, TextInput, ScrollView, TouchableOpacity } from 'react-native';
|
|
7
|
+
import Slider from '@react-native-community/slider';
|
|
8
|
+
import { AtomicText, AtomicIcon, useAppDesignTokens } from '@umituz/react-native-design-system';
|
|
9
|
+
|
|
10
|
+
interface TabProps {
|
|
11
|
+
t: (key: string) => string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const TextContentTab: React.FC<TabProps & { text: string; onTextChange: (t: string) => void }> = ({ text, onTextChange, t }) => {
|
|
15
|
+
const tokens = useAppDesignTokens();
|
|
16
|
+
return (
|
|
17
|
+
<View style={{ gap: tokens.spacing.lg }}>
|
|
18
|
+
<TextInput
|
|
19
|
+
value={text}
|
|
20
|
+
onChangeText={onTextChange}
|
|
21
|
+
placeholder={t('editor.text_placeholder')}
|
|
22
|
+
style={{
|
|
23
|
+
...tokens.typography.bodyLarge,
|
|
24
|
+
borderWidth: 1,
|
|
25
|
+
borderColor: tokens.colors.border,
|
|
26
|
+
borderRadius: tokens.borderRadius.md,
|
|
27
|
+
padding: tokens.spacing.md,
|
|
28
|
+
minHeight: 120,
|
|
29
|
+
textAlignVertical: 'top',
|
|
30
|
+
}}
|
|
31
|
+
multiline
|
|
32
|
+
/>
|
|
33
|
+
</View>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const TextStyleTab: React.FC<TabProps & {
|
|
38
|
+
fontSize: number; setFontSize: (s: number) => void;
|
|
39
|
+
color: string; setColor: (c: string) => void;
|
|
40
|
+
fontFamily: string; setFontFamily: (f: string) => void;
|
|
41
|
+
}> = ({ fontSize, setFontSize, color, setColor, fontFamily, setFontFamily, t }) => {
|
|
42
|
+
const tokens = useAppDesignTokens();
|
|
43
|
+
const colors = ['#FFFFFF', '#000000', '#FF0000', '#FFFF00', '#0000FF', '#00FF00', '#FF00FF', '#FFA500'];
|
|
44
|
+
const fonts = ['System', 'serif', 'sans-serif', 'monospace'];
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<View style={{ gap: tokens.spacing.xl }}>
|
|
48
|
+
<View>
|
|
49
|
+
<AtomicText style={{ ...tokens.typography.labelMedium, marginBottom: tokens.spacing.sm }}>Font</AtomicText>
|
|
50
|
+
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: tokens.spacing.sm }}>
|
|
51
|
+
{fonts.map(f => (
|
|
52
|
+
<TouchableOpacity key={f} onPress={() => setFontFamily(f)} style={{
|
|
53
|
+
paddingHorizontal: tokens.spacing.md, paddingVertical: tokens.spacing.xs, borderRadius: tokens.borderRadius.full,
|
|
54
|
+
borderWidth: 1, borderColor: fontFamily === f ? tokens.colors.primary : tokens.colors.border,
|
|
55
|
+
backgroundColor: fontFamily === f ? tokens.colors.primaryContainer : tokens.colors.surface
|
|
56
|
+
}}>
|
|
57
|
+
<AtomicText style={{ fontFamily: f }}>{f}</AtomicText>
|
|
58
|
+
</TouchableOpacity>
|
|
59
|
+
))}
|
|
60
|
+
</ScrollView>
|
|
61
|
+
</View>
|
|
62
|
+
|
|
63
|
+
<View>
|
|
64
|
+
<AtomicText style={{ ...tokens.typography.labelMedium, marginBottom: tokens.spacing.sm }}>Color</AtomicText>
|
|
65
|
+
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: tokens.spacing.sm }}>
|
|
66
|
+
{colors.map(c => (
|
|
67
|
+
<TouchableOpacity key={c} onPress={() => setColor(c)} style={{
|
|
68
|
+
width: 40, height: 40, borderRadius: 20, backgroundColor: c,
|
|
69
|
+
borderWidth: color === c ? 3 : 1, borderColor: tokens.colors.primary
|
|
70
|
+
}} />
|
|
71
|
+
))}
|
|
72
|
+
</ScrollView>
|
|
73
|
+
</View>
|
|
74
|
+
|
|
75
|
+
<View>
|
|
76
|
+
<AtomicText style={{ ...tokens.typography.labelMedium, marginBottom: tokens.spacing.xs }}>Size: {fontSize}px</AtomicText>
|
|
77
|
+
<Slider value={fontSize} onValueChange={setFontSize} minimumValue={12} maximumValue={100} step={1} minimumTrackTintColor={tokens.colors.primary} />
|
|
78
|
+
</View>
|
|
79
|
+
</View>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const TextTransformTab: React.FC<TabProps & {
|
|
84
|
+
scale: number; setScale: (s: number) => void;
|
|
85
|
+
rotation: number; setRotation: (r: number) => void;
|
|
86
|
+
opacity: number; setOpacity: (o: number) => void;
|
|
87
|
+
onDelete?: () => void;
|
|
88
|
+
}> = ({ scale, setScale, rotation, setRotation, opacity, setOpacity, onDelete, t }) => {
|
|
89
|
+
const tokens = useAppDesignTokens();
|
|
90
|
+
return (
|
|
91
|
+
<View style={{ gap: tokens.spacing.xl }}>
|
|
92
|
+
<View>
|
|
93
|
+
<AtomicText style={{ ...tokens.typography.labelMedium, marginBottom: tokens.spacing.xs }}>Scale: {scale.toFixed(2)}x</AtomicText>
|
|
94
|
+
<Slider value={scale} onValueChange={setScale} minimumValue={0.2} maximumValue={3} step={0.1} minimumTrackTintColor={tokens.colors.primary} />
|
|
95
|
+
</View>
|
|
96
|
+
<View>
|
|
97
|
+
<AtomicText style={{ ...tokens.typography.labelMedium, marginBottom: tokens.spacing.xs }}>Rotation: {Math.round(rotation)}°</AtomicText>
|
|
98
|
+
<Slider value={rotation} onValueChange={setRotation} minimumValue={0} maximumValue={360} step={1} minimumTrackTintColor={tokens.colors.primary} />
|
|
99
|
+
</View>
|
|
100
|
+
{onDelete && (
|
|
101
|
+
<TouchableOpacity onPress={onDelete} style={{
|
|
102
|
+
flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: tokens.spacing.sm,
|
|
103
|
+
padding: tokens.spacing.md, borderRadius: tokens.borderRadius.md, borderWidth: 1, borderColor: tokens.colors.error
|
|
104
|
+
}}>
|
|
105
|
+
<AtomicIcon name="trash" size={20} color="error" />
|
|
106
|
+
<AtomicText style={{ ...tokens.typography.labelMedium, color: tokens.colors.error }}>Delete Layer</AtomicText>
|
|
107
|
+
</TouchableOpacity>
|
|
108
|
+
)}
|
|
109
|
+
</View>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
@@ -7,36 +7,33 @@
|
|
|
7
7
|
import { useImageTransform } from './useImageTransform';
|
|
8
8
|
import { useImageConversion } from './useImageConversion';
|
|
9
9
|
import { useImageBatch } from './useImageBatch';
|
|
10
|
-
import {
|
|
10
|
+
import { useImageEnhance } from './useImageEnhance';
|
|
11
11
|
import { useImageMetadata } from './useImageMetadata';
|
|
12
12
|
|
|
13
13
|
export const useImage = () => {
|
|
14
14
|
const transform = useImageTransform();
|
|
15
15
|
const conversion = useImageConversion();
|
|
16
16
|
const batch = useImageBatch();
|
|
17
|
-
const
|
|
17
|
+
const enhance = useImageEnhance();
|
|
18
18
|
const metadata = useImageMetadata();
|
|
19
19
|
|
|
20
20
|
return {
|
|
21
|
-
// Basic operations
|
|
22
21
|
...transform,
|
|
23
22
|
...conversion,
|
|
24
|
-
// Advanced operations
|
|
25
23
|
...batch,
|
|
26
|
-
...
|
|
24
|
+
...enhance,
|
|
27
25
|
...metadata,
|
|
28
|
-
// Combined state
|
|
29
26
|
isProcessing:
|
|
30
27
|
transform.isTransforming ||
|
|
31
28
|
conversion.isConverting ||
|
|
32
29
|
batch.isBatchProcessing ||
|
|
33
|
-
|
|
30
|
+
enhance.isEnhancing ||
|
|
34
31
|
metadata.isExtracting,
|
|
35
32
|
error:
|
|
36
33
|
transform.transformError ||
|
|
37
34
|
conversion.conversionError ||
|
|
38
35
|
batch.batchError ||
|
|
39
|
-
|
|
36
|
+
enhance.enhancementError ||
|
|
40
37
|
metadata.metadataError,
|
|
41
38
|
};
|
|
42
39
|
};
|