@umituz/react-native-photo-editor 2.0.23 → 2.0.25
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 +1 -1
- package/src/PhotoEditor.tsx +43 -137
- package/src/application/hooks/useEditor.ts +4 -6
- package/src/application/hooks/useEditorUI.ts +8 -5
- package/src/application/stores/EditorStore.ts +17 -6
- package/src/domain/entities/Layer.entity.ts +86 -0
- package/src/domain/entities/{Layer.ts → Layer.legacy.ts} +3 -3
- package/src/domain/entities/StickerLayer.entity.ts +37 -0
- package/src/domain/entities/TextLayer.entity.ts +58 -0
- package/src/domain/entities/index.ts +9 -0
- package/src/domain/services/History.service.ts +69 -0
- package/src/domain/services/LayerFactory.service.ts +81 -0
- package/src/domain/services/LayerRepository.service.ts +85 -0
- package/src/domain/services/LayerService.ts +1 -1
- package/src/domain/types.ts +39 -0
- package/src/domain/value-objects/FilterSettings.vo.ts +89 -0
- package/src/domain/value-objects/LayerDefaults.vo.ts +56 -0
- package/src/domain/value-objects/Transform.vo.ts +61 -0
- package/src/domain/value-objects/index.ts +13 -0
- package/src/index.ts +4 -4
- package/src/infrastructure/gesture/createTransformGesture.ts +127 -0
- package/src/infrastructure/gesture/useTransformGesture.ts +7 -13
- package/src/presentation/components/DraggableLayer.tsx +13 -13
- package/src/presentation/components/EditorCanvas.tsx +5 -5
- package/src/presentation/components/EditorContent.tsx +72 -0
- package/src/presentation/components/EditorHeader.tsx +48 -0
- package/src/presentation/components/EditorSheets.tsx +85 -0
- package/src/presentation/components/FontControls.tsx +2 -2
- package/src/presentation/components/sheets/AdjustmentsSheet.tsx +4 -4
- package/src/presentation/components/sheets/FilterSheet.tsx +1 -1
- package/src/presentation/components/sheets/LayerManager.tsx +3 -4
- package/src/presentation/components/sheets/TextEditorSheet.tsx +1 -1
- package/src/types.ts +8 -18
- package/src/utils/constants.ts +84 -0
- package/src/utils/formatters.ts +29 -0
- package/src/utils/helpers.ts +51 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/validators.ts +38 -0
- package/ARCHITECTURE.md +0 -104
- package/MIGRATION.md +0 -100
- package/src/components/AIMagicSheet.tsx +0 -107
- package/src/components/AdjustmentsSheet.tsx +0 -108
- package/src/components/ColorPicker.tsx +0 -77
- package/src/components/DraggableSticker.tsx +0 -161
- package/src/components/DraggableText.tsx +0 -181
- package/src/components/EditorCanvas.tsx +0 -106
- package/src/components/EditorToolbar.tsx +0 -155
- package/src/components/FilterPicker.tsx +0 -73
- package/src/components/FontControls.tsx +0 -132
- package/src/components/LayerManager.tsx +0 -164
- package/src/components/Slider.tsx +0 -112
- package/src/components/StickerPicker.tsx +0 -47
- package/src/components/TextEditorSheet.tsx +0 -160
- package/src/core/HistoryManager.ts +0 -53
- package/src/hooks/usePhotoEditor.ts +0 -172
- package/src/hooks/usePhotoEditorUI.ts +0 -162
- package/src/infrastructure/history/HistoryManager.ts +0 -38
package/ARCHITECTURE.md
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
# Architecture - Photo Editor DDD Design
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
This photo editor is built using **Domain-Driven Design (DDD)** principles, ensuring maintainability, testability, and scalability. Every file is kept under 150 lines for clarity.
|
|
6
|
-
|
|
7
|
-
## Layer Structure
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
src/
|
|
11
|
-
├── domain/ # Business logic (pure TypeScript)
|
|
12
|
-
│ ├── entities/
|
|
13
|
-
│ │ ├── Layer.ts # Layer entities (TextLayer, StickerLayer)
|
|
14
|
-
│ │ ├── Transform.ts # Transform value object
|
|
15
|
-
│ │ └── Filters.ts # Filters value object
|
|
16
|
-
│ └── services/
|
|
17
|
-
│ ├── LayerService.ts # Layer business logic
|
|
18
|
-
│ └── HistoryService.ts # Undo/redo service
|
|
19
|
-
│
|
|
20
|
-
├── infrastructure/ # External concerns
|
|
21
|
-
│ ├── gesture/
|
|
22
|
-
│ │ ├── useTransformGesture.ts # Reusable gesture hook
|
|
23
|
-
│ │ └── types.ts # Gesture types
|
|
24
|
-
│ └── history/
|
|
25
|
-
│ └── HistoryManager.ts # Legacy wrapper
|
|
26
|
-
│
|
|
27
|
-
├── application/ # Application logic
|
|
28
|
-
│ ├── stores/
|
|
29
|
-
│ │ └── EditorStore.ts # Zustand state store
|
|
30
|
-
│ └── hooks/
|
|
31
|
-
│ ├── useEditor.ts # Main editor hook
|
|
32
|
-
│ └── useEditorUI.ts # UI-specific hook
|
|
33
|
-
│
|
|
34
|
-
└── presentation/ # UI components
|
|
35
|
-
└── components/
|
|
36
|
-
├── DraggableLayer.tsx # Unified draggable (replaces Text + Sticker)
|
|
37
|
-
├── EditorCanvas.tsx
|
|
38
|
-
├── EditorToolbar.tsx
|
|
39
|
-
├── FontControls.tsx
|
|
40
|
-
├── ui/
|
|
41
|
-
│ ├── ColorPicker.tsx
|
|
42
|
-
│ └── Slider.tsx
|
|
43
|
-
└── sheets/
|
|
44
|
-
├── TextEditorSheet.tsx
|
|
45
|
-
├── FilterSheet.tsx
|
|
46
|
-
├── AdjustmentsSheet.tsx
|
|
47
|
-
├── LayerManager.tsx
|
|
48
|
-
├── StickerPicker.tsx
|
|
49
|
-
└── AIMagicSheet.tsx
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
## Key Improvements
|
|
53
|
-
|
|
54
|
-
### 1. Eliminated Code Duplication (~180 lines)
|
|
55
|
-
- **Before**: `DraggableText.tsx` (182 lines) and `DraggableSticker.tsx` (162 lines) - duplicate gesture logic
|
|
56
|
-
- **After**: `DraggableLayer.tsx` (110 lines) + `useTransformGesture.ts` (130 lines) - reusable gesture hook
|
|
57
|
-
|
|
58
|
-
### 2. Domain Entities
|
|
59
|
-
- Rich domain models with business logic
|
|
60
|
-
- Type-safe layer operations
|
|
61
|
-
- Value objects for Transform and Filters
|
|
62
|
-
|
|
63
|
-
### 3. Separated Concerns
|
|
64
|
-
- **Domain**: Pure business logic, no framework dependencies
|
|
65
|
-
- **Infrastructure**: React Native, gesture handlers
|
|
66
|
-
- **Application**: State management, orchestration
|
|
67
|
-
- **Presentation**: UI components only
|
|
68
|
-
|
|
69
|
-
### 4. State Management
|
|
70
|
-
- Zustand store for global editor state
|
|
71
|
-
- History service for undo/redo
|
|
72
|
-
- Clean separation between domain and UI state
|
|
73
|
-
|
|
74
|
-
## Usage
|
|
75
|
-
|
|
76
|
-
```tsx
|
|
77
|
-
import { PhotoEditor } from "@umituz/react-native-photo-editor";
|
|
78
|
-
|
|
79
|
-
<PhotoEditor
|
|
80
|
-
imageUri={imageUri}
|
|
81
|
-
onSave={(uri, layers, filters) => console.log({ uri, layers, filters })}
|
|
82
|
-
onClose={() => navigation.goBack()}
|
|
83
|
-
t={(key) => i18n.t(key)}
|
|
84
|
-
/>
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
## Testing
|
|
88
|
-
|
|
89
|
-
Each layer can be tested independently:
|
|
90
|
-
- Domain: Pure functions, easy to unit test
|
|
91
|
-
- Infrastructure: Gesture hooks with test utils
|
|
92
|
-
- Application: Store with test environment
|
|
93
|
-
- Presentation: React component testing
|
|
94
|
-
|
|
95
|
-
## Migration from Old Architecture
|
|
96
|
-
|
|
97
|
-
The old files have been preserved for backward compatibility:
|
|
98
|
-
- Old hooks: `src/hooks/`
|
|
99
|
-
- Old components: `src/components/`
|
|
100
|
-
|
|
101
|
-
New code should use:
|
|
102
|
-
- `useEditor()` instead of `usePhotoEditor()`
|
|
103
|
-
- `useEditorUI()` instead of `usePhotoEditorUI()`
|
|
104
|
-
- Components from `src/presentation/components/`
|
package/MIGRATION.md
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
# Migration Guide - Old to New Architecture
|
|
2
|
-
|
|
3
|
-
## What Changed?
|
|
4
|
-
|
|
5
|
-
### Before
|
|
6
|
-
```
|
|
7
|
-
src/
|
|
8
|
-
├── hooks/
|
|
9
|
-
│ ├── usePhotoEditor.ts # 173 lines - mixed concerns
|
|
10
|
-
│ └── usePhotoEditorUI.ts # 163 lines - UI + business logic
|
|
11
|
-
├── components/
|
|
12
|
-
│ ├── DraggableText.tsx # 182 lines - gesture + UI
|
|
13
|
-
│ ├── DraggableSticker.tsx # 162 lines - DUPLICATE of above
|
|
14
|
-
│ └── ...
|
|
15
|
-
├── core/
|
|
16
|
-
│ └── HistoryManager.ts # Generic, reusable
|
|
17
|
-
└── types.ts # Simple type definitions
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
### After
|
|
21
|
-
```
|
|
22
|
-
src/
|
|
23
|
-
├── domain/ # Pure business logic
|
|
24
|
-
│ ├── entities/ # Rich domain models
|
|
25
|
-
│ └── services/ # Business operations
|
|
26
|
-
├── infrastructure/ # External systems
|
|
27
|
-
│ ├── gesture/ # Reusable gestures
|
|
28
|
-
│ └── history/
|
|
29
|
-
├── application/ # Orchestration
|
|
30
|
-
│ ├── stores/ # State management
|
|
31
|
-
│ └── hooks/ # Clean hooks
|
|
32
|
-
└── presentation/ # UI only
|
|
33
|
-
└── components/
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## API Changes
|
|
37
|
-
|
|
38
|
-
### Hooks
|
|
39
|
-
|
|
40
|
-
**Old:**
|
|
41
|
-
```tsx
|
|
42
|
-
import { usePhotoEditor } from "./hooks/usePhotoEditor";
|
|
43
|
-
import { usePhotoEditorUI } from "./hooks/usePhotoEditorUI";
|
|
44
|
-
|
|
45
|
-
const editor = usePhotoEditor([]);
|
|
46
|
-
const ui = usePhotoEditorUI();
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
**New:**
|
|
50
|
-
```tsx
|
|
51
|
-
import { useEditor } from "./application/hooks/useEditor";
|
|
52
|
-
import { useEditorUI } from "./application/hooks/useEditorUI";
|
|
53
|
-
|
|
54
|
-
const editor = useEditor();
|
|
55
|
-
const ui = useEditorUI();
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### Components
|
|
59
|
-
|
|
60
|
-
**Old:**
|
|
61
|
-
```tsx
|
|
62
|
-
import DraggableText from "./components/DraggableText";
|
|
63
|
-
import DraggableSticker from "./components/DraggableSticker";
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
**New:**
|
|
67
|
-
```tsx
|
|
68
|
-
import { DraggableLayer } from "./presentation/components/DraggableLayer";
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### Types
|
|
72
|
-
|
|
73
|
-
**Old:**
|
|
74
|
-
```tsx
|
|
75
|
-
import type { Layer, TextLayer, ImageFilters } from "./types";
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
**New:**
|
|
79
|
-
```tsx
|
|
80
|
-
import type { Layer, TextLayer, FilterValues } from "./domain/entities/Layer";
|
|
81
|
-
import { FiltersVO } from "./domain/entities/Filters";
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
## Benefits
|
|
85
|
-
|
|
86
|
-
1. **180 lines less code** - Removed duplication between DraggableText and DraggableSticker
|
|
87
|
-
2. **Clear separation** - Business logic separate from UI
|
|
88
|
-
3. **Testable** - Each layer can be tested independently
|
|
89
|
-
4. **Maintainable** - Files under 150 lines each
|
|
90
|
-
5. **Scalable** - Easy to add new features
|
|
91
|
-
|
|
92
|
-
## Backward Compatibility
|
|
93
|
-
|
|
94
|
-
The old API is still exported. Your existing code will continue to work.
|
|
95
|
-
|
|
96
|
-
To migrate gradually:
|
|
97
|
-
1. Start using `useEditor()` in new features
|
|
98
|
-
2. Replace `DraggableText` + `DraggableSticker` with `DraggableLayer`
|
|
99
|
-
3. Update imports to new paths
|
|
100
|
-
4. Remove old imports when ready
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import React, { useState, useMemo } from "react";
|
|
2
|
-
import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
|
|
3
|
-
import { AtomicText, AtomicIcon, AtomicButton } from "@umituz/react-native-design-system/atoms";
|
|
4
|
-
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
5
|
-
import { DEFAULT_AI_STYLES } from "../constants";
|
|
6
|
-
|
|
7
|
-
interface AIMagicSheetProps {
|
|
8
|
-
/**
|
|
9
|
-
* Called with the selected style ID. Should return a generated caption string.
|
|
10
|
-
* If undefined, the AI button is disabled.
|
|
11
|
-
*/
|
|
12
|
-
onGenerateCaption?: (style: string) => Promise<string> | void;
|
|
13
|
-
isLoading?: boolean;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const AIMagicSheet: React.FC<AIMagicSheetProps> = ({
|
|
17
|
-
onGenerateCaption,
|
|
18
|
-
isLoading = false,
|
|
19
|
-
}) => {
|
|
20
|
-
const tokens = useAppDesignTokens();
|
|
21
|
-
const [selected, setSelected] = useState<string | null>(null);
|
|
22
|
-
const [loading, setLoading] = useState(false);
|
|
23
|
-
|
|
24
|
-
const styles = useMemo(() => StyleSheet.create({
|
|
25
|
-
container: { padding: tokens.spacing.md, gap: tokens.spacing.md },
|
|
26
|
-
header: { flexDirection: "row", alignItems: "center", gap: tokens.spacing.sm },
|
|
27
|
-
grid: { gap: tokens.spacing.sm },
|
|
28
|
-
card: {
|
|
29
|
-
flexDirection: "row",
|
|
30
|
-
alignItems: "center",
|
|
31
|
-
padding: tokens.spacing.md,
|
|
32
|
-
backgroundColor: tokens.colors.surfaceVariant,
|
|
33
|
-
borderRadius: tokens.borders.radius.md,
|
|
34
|
-
borderWidth: 2,
|
|
35
|
-
borderColor: "transparent",
|
|
36
|
-
},
|
|
37
|
-
cardActive: {
|
|
38
|
-
borderColor: tokens.colors.primary,
|
|
39
|
-
backgroundColor: tokens.colors.primary + "10",
|
|
40
|
-
},
|
|
41
|
-
info: { flex: 1, marginLeft: tokens.spacing.sm },
|
|
42
|
-
}), [tokens]);
|
|
43
|
-
|
|
44
|
-
const handleGenerate = async () => {
|
|
45
|
-
if (!selected || !onGenerateCaption) return;
|
|
46
|
-
setLoading(true);
|
|
47
|
-
try {
|
|
48
|
-
await onGenerateCaption(selected);
|
|
49
|
-
} finally {
|
|
50
|
-
setLoading(false);
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const isGenerating = isLoading || loading;
|
|
55
|
-
|
|
56
|
-
return (
|
|
57
|
-
<View style={styles.container}>
|
|
58
|
-
<View style={styles.header}>
|
|
59
|
-
<AtomicIcon name="sparkles" size="md" color="primary" />
|
|
60
|
-
<AtomicText type="headlineSmall">AI Caption Magic</AtomicText>
|
|
61
|
-
</View>
|
|
62
|
-
<ScrollView showsVerticalScrollIndicator={false}>
|
|
63
|
-
<View style={styles.grid}>
|
|
64
|
-
{DEFAULT_AI_STYLES.map((style) => {
|
|
65
|
-
const isActive = selected === style.id;
|
|
66
|
-
const [emoji, ...words] = style.label.split(" ");
|
|
67
|
-
return (
|
|
68
|
-
<TouchableOpacity
|
|
69
|
-
key={style.id}
|
|
70
|
-
style={[styles.card, isActive && styles.cardActive]}
|
|
71
|
-
onPress={() => setSelected(style.id)}
|
|
72
|
-
accessibilityLabel={style.label}
|
|
73
|
-
accessibilityRole="button"
|
|
74
|
-
accessibilityState={{ selected: isActive }}
|
|
75
|
-
>
|
|
76
|
-
<AtomicText style={{ fontSize: 24 }}>{emoji}</AtomicText>
|
|
77
|
-
<View style={styles.info}>
|
|
78
|
-
<AtomicText
|
|
79
|
-
fontWeight="bold"
|
|
80
|
-
color={isActive ? "primary" : "textPrimary"}
|
|
81
|
-
>
|
|
82
|
-
{words.join(" ")}
|
|
83
|
-
</AtomicText>
|
|
84
|
-
<AtomicText type="labelSmall" color="textSecondary">
|
|
85
|
-
{style.desc}
|
|
86
|
-
</AtomicText>
|
|
87
|
-
</View>
|
|
88
|
-
{isActive && (
|
|
89
|
-
<AtomicIcon name="checkmark-circle" size="md" color="primary" />
|
|
90
|
-
)}
|
|
91
|
-
</TouchableOpacity>
|
|
92
|
-
);
|
|
93
|
-
})}
|
|
94
|
-
</View>
|
|
95
|
-
</ScrollView>
|
|
96
|
-
<AtomicButton
|
|
97
|
-
variant="primary"
|
|
98
|
-
disabled={!selected || !onGenerateCaption || isGenerating}
|
|
99
|
-
onPress={handleGenerate}
|
|
100
|
-
loading={isGenerating}
|
|
101
|
-
icon="sparkles"
|
|
102
|
-
>
|
|
103
|
-
Generate Caption
|
|
104
|
-
</AtomicButton>
|
|
105
|
-
</View>
|
|
106
|
-
);
|
|
107
|
-
};
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { View, TouchableOpacity } from "react-native";
|
|
3
|
-
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
|
|
4
|
-
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
5
|
-
import { Slider } from "./Slider";
|
|
6
|
-
import { ImageFilters, DEFAULT_IMAGE_FILTERS } from "../types";
|
|
7
|
-
|
|
8
|
-
interface AdjustmentsSheetProps {
|
|
9
|
-
filters: ImageFilters;
|
|
10
|
-
onFiltersChange: (filters: ImageFilters) => void;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const AdjustmentsSheet: React.FC<AdjustmentsSheetProps> = ({
|
|
14
|
-
filters,
|
|
15
|
-
onFiltersChange,
|
|
16
|
-
}) => {
|
|
17
|
-
const tokens = useAppDesignTokens();
|
|
18
|
-
|
|
19
|
-
const update = (key: keyof ImageFilters, val: number) => {
|
|
20
|
-
onFiltersChange({ ...filters, [key]: val });
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const handleReset = () => onFiltersChange(DEFAULT_IMAGE_FILTERS);
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<View style={{ padding: tokens.spacing.md, gap: tokens.spacing.lg }}>
|
|
27
|
-
<View
|
|
28
|
-
style={{
|
|
29
|
-
flexDirection: "row",
|
|
30
|
-
alignItems: "center",
|
|
31
|
-
justifyContent: "space-between",
|
|
32
|
-
}}
|
|
33
|
-
>
|
|
34
|
-
<View style={{ flexDirection: "row", alignItems: "center", gap: tokens.spacing.sm }}>
|
|
35
|
-
<AtomicIcon name="brush" size="md" color="primary" />
|
|
36
|
-
<AtomicText type="headlineSmall">Adjustments</AtomicText>
|
|
37
|
-
</View>
|
|
38
|
-
<TouchableOpacity
|
|
39
|
-
onPress={handleReset}
|
|
40
|
-
accessibilityLabel="Reset adjustments"
|
|
41
|
-
accessibilityRole="button"
|
|
42
|
-
style={{
|
|
43
|
-
paddingHorizontal: tokens.spacing.md,
|
|
44
|
-
paddingVertical: tokens.spacing.xs,
|
|
45
|
-
backgroundColor: tokens.colors.surfaceVariant,
|
|
46
|
-
borderRadius: tokens.borders.radius.sm,
|
|
47
|
-
}}
|
|
48
|
-
>
|
|
49
|
-
<AtomicText type="labelSmall" color="textSecondary">
|
|
50
|
-
Reset
|
|
51
|
-
</AtomicText>
|
|
52
|
-
</TouchableOpacity>
|
|
53
|
-
</View>
|
|
54
|
-
|
|
55
|
-
<Slider
|
|
56
|
-
label="Brightness"
|
|
57
|
-
value={filters.brightness}
|
|
58
|
-
min={0.5}
|
|
59
|
-
max={2}
|
|
60
|
-
step={0.05}
|
|
61
|
-
onValueChange={(v) => update("brightness", v)}
|
|
62
|
-
formatValue={(v) => `${Math.round((v - 1) * 100) >= 0 ? "+" : ""}${Math.round((v - 1) * 100)}%`}
|
|
63
|
-
/>
|
|
64
|
-
|
|
65
|
-
<Slider
|
|
66
|
-
label="Contrast"
|
|
67
|
-
value={filters.contrast}
|
|
68
|
-
min={0.5}
|
|
69
|
-
max={2}
|
|
70
|
-
step={0.05}
|
|
71
|
-
onValueChange={(v) => update("contrast", v)}
|
|
72
|
-
formatValue={(v) => `${Math.round((v - 1) * 100) >= 0 ? "+" : ""}${Math.round((v - 1) * 100)}%`}
|
|
73
|
-
/>
|
|
74
|
-
|
|
75
|
-
<Slider
|
|
76
|
-
label="Saturation"
|
|
77
|
-
value={filters.saturation}
|
|
78
|
-
min={0}
|
|
79
|
-
max={2}
|
|
80
|
-
step={0.05}
|
|
81
|
-
onValueChange={(v) => update("saturation", v)}
|
|
82
|
-
formatValue={(v) => `${Math.round((v - 1) * 100) >= 0 ? "+" : ""}${Math.round((v - 1) * 100)}%`}
|
|
83
|
-
/>
|
|
84
|
-
|
|
85
|
-
<Slider
|
|
86
|
-
label="Hue Rotate"
|
|
87
|
-
value={filters.hueRotate ?? 0}
|
|
88
|
-
min={0}
|
|
89
|
-
max={360}
|
|
90
|
-
step={1}
|
|
91
|
-
onValueChange={(v) => update("hueRotate", v)}
|
|
92
|
-
formatValue={(v) => `${Math.round(v)}°`}
|
|
93
|
-
/>
|
|
94
|
-
|
|
95
|
-
<Slider
|
|
96
|
-
label="Sepia"
|
|
97
|
-
value={filters.sepia}
|
|
98
|
-
min={0}
|
|
99
|
-
max={1}
|
|
100
|
-
step={0.05}
|
|
101
|
-
onValueChange={(v) => update("sepia", v)}
|
|
102
|
-
formatValue={(v) => `${Math.round(v * 100)}%`}
|
|
103
|
-
/>
|
|
104
|
-
</View>
|
|
105
|
-
);
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
export default React.memo(AdjustmentsSheet);
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { View, TouchableOpacity } from "react-native";
|
|
3
|
-
import { AtomicText } from "@umituz/react-native-design-system/atoms";
|
|
4
|
-
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
5
|
-
import { DEFAULT_TEXT_COLORS } from "../constants";
|
|
6
|
-
|
|
7
|
-
interface ColorPickerProps {
|
|
8
|
-
selectedColor: string;
|
|
9
|
-
onSelectColor: (color: string) => void;
|
|
10
|
-
label?: string;
|
|
11
|
-
colors?: readonly string[];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const ColorPicker: React.FC<ColorPickerProps> = ({
|
|
15
|
-
selectedColor,
|
|
16
|
-
onSelectColor,
|
|
17
|
-
label,
|
|
18
|
-
colors = DEFAULT_TEXT_COLORS,
|
|
19
|
-
}) => {
|
|
20
|
-
const tokens = useAppDesignTokens();
|
|
21
|
-
|
|
22
|
-
return (
|
|
23
|
-
<View style={{ gap: tokens.spacing.xs }}>
|
|
24
|
-
{label && (
|
|
25
|
-
<AtomicText type="labelMedium" color="textSecondary">
|
|
26
|
-
{label}
|
|
27
|
-
</AtomicText>
|
|
28
|
-
)}
|
|
29
|
-
<View
|
|
30
|
-
style={{
|
|
31
|
-
flexDirection: "row",
|
|
32
|
-
flexWrap: "wrap",
|
|
33
|
-
gap: tokens.spacing.xs,
|
|
34
|
-
}}
|
|
35
|
-
>
|
|
36
|
-
{colors.map((color) => {
|
|
37
|
-
const isSelected = selectedColor === color;
|
|
38
|
-
return (
|
|
39
|
-
<TouchableOpacity
|
|
40
|
-
key={color}
|
|
41
|
-
onPress={() => onSelectColor(color)}
|
|
42
|
-
accessibilityLabel={`Color ${color}`}
|
|
43
|
-
accessibilityRole="button"
|
|
44
|
-
accessibilityState={{ selected: isSelected }}
|
|
45
|
-
style={{
|
|
46
|
-
width: 34,
|
|
47
|
-
height: 34,
|
|
48
|
-
borderRadius: 17,
|
|
49
|
-
backgroundColor: color,
|
|
50
|
-
borderWidth: isSelected ? 3 : 1.5,
|
|
51
|
-
borderColor: isSelected
|
|
52
|
-
? tokens.colors.primary
|
|
53
|
-
: tokens.colors.border,
|
|
54
|
-
alignItems: "center",
|
|
55
|
-
justifyContent: "center",
|
|
56
|
-
}}
|
|
57
|
-
>
|
|
58
|
-
{isSelected && (
|
|
59
|
-
<View
|
|
60
|
-
style={{
|
|
61
|
-
width: 10,
|
|
62
|
-
height: 10,
|
|
63
|
-
borderRadius: 5,
|
|
64
|
-
backgroundColor:
|
|
65
|
-
color === "#FFFFFF" ? "#000000" : "#FFFFFF",
|
|
66
|
-
}}
|
|
67
|
-
/>
|
|
68
|
-
)}
|
|
69
|
-
</TouchableOpacity>
|
|
70
|
-
);
|
|
71
|
-
})}
|
|
72
|
-
</View>
|
|
73
|
-
</View>
|
|
74
|
-
);
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export default React.memo(ColorPicker);
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import React, { useState, useRef, useCallback, useEffect } from "react";
|
|
2
|
-
import { View, StyleSheet } from "react-native";
|
|
3
|
-
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
4
|
-
import { Image } from "expo-image";
|
|
5
|
-
import { AtomicText } from "@umituz/react-native-design-system/atoms";
|
|
6
|
-
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
7
|
-
import type { LayerTransform } from "./DraggableText";
|
|
8
|
-
|
|
9
|
-
interface DraggableStickerProps {
|
|
10
|
-
uri: string;
|
|
11
|
-
initialX: number;
|
|
12
|
-
initialY: number;
|
|
13
|
-
rotation?: number;
|
|
14
|
-
scale?: number;
|
|
15
|
-
opacity?: number;
|
|
16
|
-
onTransformEnd: (transform: LayerTransform) => void;
|
|
17
|
-
onPress: () => void;
|
|
18
|
-
isSelected?: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const isEmojiString = (str: string) =>
|
|
22
|
-
str.length <= 4 && !/^https?:\/\//i.test(str) && !str.startsWith("/");
|
|
23
|
-
|
|
24
|
-
export const DraggableSticker: React.FC<DraggableStickerProps> = ({
|
|
25
|
-
uri,
|
|
26
|
-
initialX,
|
|
27
|
-
initialY,
|
|
28
|
-
rotation: rotationProp = 0,
|
|
29
|
-
scale: scaleProp = 1,
|
|
30
|
-
opacity = 1,
|
|
31
|
-
onTransformEnd,
|
|
32
|
-
onPress,
|
|
33
|
-
isSelected,
|
|
34
|
-
}) => {
|
|
35
|
-
const tokens = useAppDesignTokens();
|
|
36
|
-
const [position, setPosition] = useState({ x: initialX, y: initialY });
|
|
37
|
-
const [scale, setScale] = useState(scaleProp);
|
|
38
|
-
const [rotation, setRotation] = useState(rotationProp);
|
|
39
|
-
|
|
40
|
-
// Sync when props change (e.g., undo/redo)
|
|
41
|
-
useEffect(() => { setPosition({ x: initialX, y: initialY }); }, [initialX, initialY]);
|
|
42
|
-
useEffect(() => { setScale(scaleProp); }, [scaleProp]);
|
|
43
|
-
useEffect(() => { setRotation(rotationProp); }, [rotationProp]);
|
|
44
|
-
|
|
45
|
-
const positionRef = useRef(position);
|
|
46
|
-
positionRef.current = position;
|
|
47
|
-
const scaleRef = useRef(scale);
|
|
48
|
-
scaleRef.current = scale;
|
|
49
|
-
const rotationRef = useRef(rotation);
|
|
50
|
-
rotationRef.current = rotation;
|
|
51
|
-
const onTransformEndRef = useRef(onTransformEnd);
|
|
52
|
-
onTransformEndRef.current = onTransformEnd;
|
|
53
|
-
const onPressRef = useRef(onPress);
|
|
54
|
-
onPressRef.current = onPress;
|
|
55
|
-
|
|
56
|
-
const offsetRef = useRef({ x: initialX, y: initialY });
|
|
57
|
-
const scaleStartRef = useRef(scaleProp);
|
|
58
|
-
const rotationStartRef = useRef(rotationProp);
|
|
59
|
-
|
|
60
|
-
const emitTransform = useCallback(() => {
|
|
61
|
-
onTransformEndRef.current({
|
|
62
|
-
x: positionRef.current.x,
|
|
63
|
-
y: positionRef.current.y,
|
|
64
|
-
scale: scaleRef.current,
|
|
65
|
-
rotation: rotationRef.current,
|
|
66
|
-
});
|
|
67
|
-
}, []);
|
|
68
|
-
|
|
69
|
-
const panGesture = Gesture.Pan()
|
|
70
|
-
.runOnJS(true)
|
|
71
|
-
.averageTouches(true)
|
|
72
|
-
.onStart(() => {
|
|
73
|
-
offsetRef.current = { x: positionRef.current.x, y: positionRef.current.y };
|
|
74
|
-
})
|
|
75
|
-
.onUpdate((e) => {
|
|
76
|
-
setPosition({
|
|
77
|
-
x: offsetRef.current.x + e.translationX,
|
|
78
|
-
y: offsetRef.current.y + e.translationY,
|
|
79
|
-
});
|
|
80
|
-
})
|
|
81
|
-
.onEnd(emitTransform);
|
|
82
|
-
|
|
83
|
-
const pinchGesture = Gesture.Pinch()
|
|
84
|
-
.runOnJS(true)
|
|
85
|
-
.onStart(() => {
|
|
86
|
-
scaleStartRef.current = scaleRef.current;
|
|
87
|
-
})
|
|
88
|
-
.onUpdate((e) => {
|
|
89
|
-
setScale(Math.max(0.2, Math.min(6, scaleStartRef.current * e.scale)));
|
|
90
|
-
})
|
|
91
|
-
.onEnd(emitTransform);
|
|
92
|
-
|
|
93
|
-
const rotationGesture = Gesture.Rotation()
|
|
94
|
-
.runOnJS(true)
|
|
95
|
-
.onStart(() => {
|
|
96
|
-
rotationStartRef.current = rotationRef.current;
|
|
97
|
-
})
|
|
98
|
-
.onUpdate((e) => {
|
|
99
|
-
setRotation(rotationStartRef.current + (e.rotation * 180) / Math.PI);
|
|
100
|
-
})
|
|
101
|
-
.onEnd(emitTransform);
|
|
102
|
-
|
|
103
|
-
const tapGesture = Gesture.Tap()
|
|
104
|
-
.runOnJS(true)
|
|
105
|
-
.onEnd(() => onPressRef.current());
|
|
106
|
-
|
|
107
|
-
const composed = Gesture.Exclusive(
|
|
108
|
-
Gesture.Simultaneous(panGesture, pinchGesture, rotationGesture),
|
|
109
|
-
tapGesture,
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
const isEmoji = isEmojiString(uri);
|
|
113
|
-
|
|
114
|
-
return (
|
|
115
|
-
<GestureDetector gesture={composed}>
|
|
116
|
-
<View
|
|
117
|
-
accessibilityLabel={isEmoji ? `Sticker ${uri}` : "Image sticker"}
|
|
118
|
-
accessibilityRole="button"
|
|
119
|
-
style={[
|
|
120
|
-
styles.container,
|
|
121
|
-
{
|
|
122
|
-
transform: [
|
|
123
|
-
{ translateX: position.x },
|
|
124
|
-
{ translateY: position.y },
|
|
125
|
-
{ rotate: `${rotation}deg` },
|
|
126
|
-
{ scale },
|
|
127
|
-
],
|
|
128
|
-
opacity,
|
|
129
|
-
zIndex: isSelected ? 100 : 50,
|
|
130
|
-
},
|
|
131
|
-
]}
|
|
132
|
-
>
|
|
133
|
-
<View
|
|
134
|
-
style={{
|
|
135
|
-
padding: tokens.spacing.xs,
|
|
136
|
-
borderRadius: tokens.borders.radius.sm,
|
|
137
|
-
borderWidth: isSelected ? 2 : 0,
|
|
138
|
-
borderColor: tokens.colors.primary,
|
|
139
|
-
borderStyle: "dashed",
|
|
140
|
-
backgroundColor: isSelected ? tokens.colors.primary + "10" : "transparent",
|
|
141
|
-
}}
|
|
142
|
-
>
|
|
143
|
-
{isEmoji ? (
|
|
144
|
-
<AtomicText style={{ fontSize: 48 }}>{uri}</AtomicText>
|
|
145
|
-
) : (
|
|
146
|
-
<Image
|
|
147
|
-
source={{ uri }}
|
|
148
|
-
style={{ width: 80, height: 80 }}
|
|
149
|
-
contentFit="contain"
|
|
150
|
-
accessibilityIgnoresInvertColors
|
|
151
|
-
/>
|
|
152
|
-
)}
|
|
153
|
-
</View>
|
|
154
|
-
</View>
|
|
155
|
-
</GestureDetector>
|
|
156
|
-
);
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
const styles = StyleSheet.create({
|
|
160
|
-
container: { position: "absolute" },
|
|
161
|
-
});
|