@umituz/react-native-video-editor 1.1.64 → 1.1.65

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.
Files changed (29) hide show
  1. package/package.json +1 -1
  2. package/src/application/services/EditorService.ts +151 -0
  3. package/src/application/usecases/LayerUseCases.ts +192 -0
  4. package/src/infrastructure/repositories/LayerRepositoryImpl.ts +158 -0
  5. package/src/infrastructure/utils/debounce.utils.ts +69 -0
  6. package/src/infrastructure/utils/image-processing.utils.ts +73 -0
  7. package/src/infrastructure/utils/position-calculations.utils.ts +45 -161
  8. package/src/presentation/components/EditorTimeline.tsx +29 -11
  9. package/src/presentation/components/EditorToolPanel.tsx +71 -172
  10. package/src/presentation/components/LayerActionsMenu.tsx +97 -159
  11. package/src/presentation/components/SceneActionsMenu.tsx +34 -44
  12. package/src/presentation/components/SubtitleListPanel.tsx +54 -27
  13. package/src/presentation/components/SubtitleStylePicker.tsx +36 -156
  14. package/src/presentation/components/draggable-layer/LayerContent.tsx +7 -2
  15. package/src/presentation/components/generic/ActionMenu.tsx +110 -0
  16. package/src/presentation/components/generic/Editor.tsx +65 -0
  17. package/src/presentation/components/generic/Selector.tsx +96 -0
  18. package/src/presentation/components/generic/Toolbar.tsx +77 -0
  19. package/src/presentation/components/image-layer/ImagePreview.tsx +7 -1
  20. package/src/presentation/components/shape-layer/ColorPickerHorizontal.tsx +20 -55
  21. package/src/presentation/components/text-layer/ColorPicker.tsx +21 -55
  22. package/src/presentation/components/text-layer/FontSizeSelector.tsx +19 -50
  23. package/src/presentation/components/text-layer/OptionSelector.tsx +18 -55
  24. package/src/presentation/components/text-layer/TextAlignSelector.tsx +24 -51
  25. package/src/presentation/hooks/generic/useForm.ts +99 -0
  26. package/src/presentation/hooks/generic/useList.ts +117 -0
  27. package/src/presentation/hooks/useDraggableLayerGestures.ts +28 -25
  28. package/src/presentation/hooks/useEditorPlayback.ts +19 -2
  29. package/src/presentation/hooks/useMenuActions.tsx +19 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-video-editor",
3
- "version": "1.1.64",
3
+ "version": "1.1.65",
4
4
  "description": "Professional video editor with layer-based timeline, text/image/shape/audio/animation layers, and export functionality",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Editor Service
3
+ * Single Responsibility: High-level editor operations coordination
4
+ * Application Service Layer
5
+ *
6
+ * Orchestrates multiple use cases and domain operations
7
+ */
8
+
9
+ import type { VideoProject, Scene, Layer } from "../../domain/entities/video-project.types";
10
+ import type {
11
+ CreateLayerRequest,
12
+ UpdateLayerRequest,
13
+ DeleteLayerRequest,
14
+ } from "../usecases/LayerUseCases";
15
+ import {
16
+ CreateLayerUseCase,
17
+ UpdateLayerUseCase,
18
+ DeleteLayerUseCase,
19
+ DuplicateLayerUseCase,
20
+ ReorderLayerUseCase,
21
+ } from "../usecases/LayerUseCases";
22
+ import type { LayerRepository } from "../usecases/LayerUseCases";
23
+
24
+ export interface EditorServiceConfig {
25
+ layerRepository: LayerRepository;
26
+ }
27
+
28
+ /**
29
+ * Application service that coordinates editor operations
30
+ * Provides high-level API for the presentation layer
31
+ */
32
+ export class EditorService {
33
+ private readonly createLayerUseCase: CreateLayerUseCase;
34
+ private readonly updateLayerUseCase: UpdateLayerUseCase;
35
+ private readonly deleteLayerUseCase: DeleteLayerUseCase;
36
+ private readonly duplicateLayerUseCase: DuplicateLayerUseCase;
37
+ private readonly reorderLayerUseCase: ReorderLayerUseCase;
38
+
39
+ constructor(config: EditorServiceConfig) {
40
+ this.createLayerUseCase = new CreateLayerUseCase(config.layerRepository);
41
+ this.updateLayerUseCase = new UpdateLayerUseCase(config.layerRepository);
42
+ this.deleteLayerUseCase = new DeleteLayerUseCase(config.layerRepository);
43
+ this.duplicateLayerUseCase = new DuplicateLayerUseCase(config.layerRepository);
44
+ this.reorderLayerUseCase = new ReorderLayerUseCase(config.layerRepository);
45
+ }
46
+
47
+ // Layer Operations
48
+ async createLayer(request: CreateLayerRequest): Promise<Layer> {
49
+ return await this.createLayerUseCase.execute(request);
50
+ }
51
+
52
+ async updateLayer(request: UpdateLayerRequest): Promise<void> {
53
+ await this.updateLayerUseCase.execute(request);
54
+ }
55
+
56
+ async deleteLayer(request: DeleteLayerRequest): Promise<void> {
57
+ await this.deleteLayerUseCase.execute(request);
58
+ }
59
+
60
+ async duplicateLayer(sceneId: string, layerId: string): Promise<Layer> {
61
+ return await this.duplicateLayerUseCase.execute(sceneId, layerId);
62
+ }
63
+
64
+ async reorderLayer(
65
+ sceneId: string,
66
+ layerId: string,
67
+ direction: "front" | "back" | "up" | "down",
68
+ ): Promise<void> {
69
+ await this.reorderLayerUseCase.execute(sceneId, layerId, direction);
70
+ }
71
+
72
+ // Batch Operations
73
+ async batchUpdateLayers(
74
+ sceneId: string,
75
+ updates: Array<{ layerId: string; changes: Partial<Layer> }>,
76
+ ): Promise<void> {
77
+ for (const { layerId, changes } of updates) {
78
+ await this.updateLayer({ sceneId, layerId, updates: changes });
79
+ }
80
+ }
81
+
82
+ async duplicateMultipleLayers(
83
+ sceneId: string,
84
+ layerIds: string[],
85
+ ): Promise<Layer[]> {
86
+ const duplicated: Layer[] = [];
87
+
88
+ for (const layerId of layerIds) {
89
+ const layer = await this.duplicateLayer(sceneId, layerId);
90
+ duplicated.push(layer);
91
+ }
92
+
93
+ return duplicated;
94
+ }
95
+
96
+ async deleteMultipleLayers(sceneId: string, layerIds: string[]): Promise<void> {
97
+ for (const layerId of layerIds) {
98
+ await this.deleteLayer({ sceneId, layerId });
99
+ }
100
+ }
101
+
102
+ // Query Operations
103
+ getLayersByScene(scenes: Scene[], sceneId: string): Layer[] {
104
+ const scene = scenes.find((s) => s.id === sceneId);
105
+ return scene?.layers || [];
106
+ }
107
+
108
+ getLayersByType(scenes: Scene[], type: Layer["type"]): Layer[] {
109
+ const allLayers = scenes.flatMap((s) => s.layers);
110
+ return allLayers.filter((l) => l.type === type);
111
+ }
112
+
113
+ getLayerById(scenes: Scene[], layerId: string): Layer | undefined {
114
+ for (const scene of scenes) {
115
+ const layer = scene.layers.find((l) => l.id === layerId);
116
+ if (layer) return layer;
117
+ }
118
+ return undefined;
119
+ }
120
+
121
+ // Validation
122
+ validateLayer(layer: Partial<Layer>): { valid: boolean; errors: string[] } {
123
+ const errors: string[] = [];
124
+
125
+ if (!layer.type) {
126
+ errors.push("Layer type is required");
127
+ }
128
+
129
+ if (layer.type === "image" && !(layer as any).uri) {
130
+ errors.push("Image layer requires URI");
131
+ }
132
+
133
+ if (layer.type === "text" && !(layer as any).content) {
134
+ errors.push("Text layer requires content");
135
+ }
136
+
137
+ return {
138
+ valid: errors.length === 0,
139
+ errors,
140
+ };
141
+ }
142
+
143
+ // Export Helpers
144
+ prepareProjectForExport(project: VideoProject): VideoProject {
145
+ // Clean up temporary data
146
+ // Optimize images
147
+ // Validate all layers
148
+ // Return export-ready project
149
+ return project;
150
+ }
151
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Layer Use Cases
3
+ * Single Responsibility: Layer business logic orchestration
4
+ * Domain-Driven Design: Application Layer
5
+ *
6
+ * This layer orchestrates domain operations and coordinates between entities.
7
+ * It doesn't contain business logic, but coordinates domain services.
8
+ */
9
+
10
+ import type { Layer, ImageLayer, TextLayer, ShapeLayer } from "../../domain/entities/video-project.types";
11
+ import type { AddImageLayerData, AddTextLayerData, AddShapeLayerData } from "../../domain/entities/video-project.types";
12
+
13
+ // Repository Interface (Dependency Inversion)
14
+ export interface LayerRepository {
15
+ addLayer(sceneId: string, layer: Layer): Promise<void>;
16
+ updateLayer(sceneId: string, layerId: string, updates: Partial<Layer>): Promise<void>;
17
+ deleteLayer(sceneId: string, layerId: string): Promise<void>;
18
+ duplicateLayer(sceneId: string, layerId: string): Promise<Layer>;
19
+ reorderLayer(sceneId: string, layerId: string, direction: "front" | "back" | "up" | "down"): Promise<void>;
20
+ }
21
+
22
+ // Request/Response DTOs
23
+ export interface CreateLayerRequest {
24
+ sceneId: string;
25
+ layerType: "image" | "text" | "shape" | "video";
26
+ data: AddImageLayerData | AddTextLayerData | AddShapeLayerData;
27
+ }
28
+
29
+ export interface UpdateLayerRequest {
30
+ sceneId: string;
31
+ layerId: string;
32
+ updates: Partial<Layer>;
33
+ }
34
+
35
+ export interface DeleteLayerRequest {
36
+ sceneId: string;
37
+ layerId: string;
38
+ }
39
+
40
+ // Use Case: Create Layer
41
+ export class CreateLayerUseCase {
42
+ constructor(private readonly layerRepository: LayerRepository) {}
43
+
44
+ async execute(request: CreateLayerRequest): Promise<Layer> {
45
+ const { sceneId, layerType, data } = request;
46
+
47
+ // Validate request
48
+ if (!sceneId) {
49
+ throw new Error("Scene ID is required");
50
+ }
51
+
52
+ // Create layer through domain service
53
+ const layer = this.createLayerByType(layerType, data);
54
+
55
+ // Persist through repository
56
+ await this.layerRepository.addLayer(sceneId, layer);
57
+
58
+ return layer;
59
+ }
60
+
61
+ private createLayerByType(
62
+ type: string,
63
+ data: AddImageLayerData | AddTextLayerData | AddShapeLayerData,
64
+ ): Layer {
65
+ switch (type) {
66
+ case "image":
67
+ return this.createImageLayer(data as AddImageLayerData);
68
+ case "text":
69
+ return this.createTextLayer(data as AddTextLayerData);
70
+ case "shape":
71
+ return this.createShapeLayer(data as AddShapeLayerData);
72
+ default:
73
+ throw new Error(`Unsupported layer type: ${type}`);
74
+ }
75
+ }
76
+
77
+ private createImageLayer(data: AddImageLayerData): ImageLayer {
78
+ return {
79
+ id: this.generateId(),
80
+ type: "image",
81
+ uri: data.uri || "",
82
+ position: { x: 15, y: 30 },
83
+ size: { width: 70, height: 40 },
84
+ rotation: 0,
85
+ opacity: data.opacity ?? 1,
86
+ };
87
+ }
88
+
89
+ private createTextLayer(data: AddTextLayerData): TextLayer {
90
+ return {
91
+ id: this.generateId(),
92
+ type: "text",
93
+ content: data.content || "",
94
+ position: { x: 10, y: 40 },
95
+ size: { width: 80, height: 20 },
96
+ rotation: 0,
97
+ opacity: 1,
98
+ fontSize: data.fontSize || 48,
99
+ fontFamily: data.fontFamily || "System",
100
+ fontWeight: (data.fontWeight as TextLayer["fontWeight"]) || "bold",
101
+ color: data.color || "#000000",
102
+ textAlign: data.textAlign || "center",
103
+ };
104
+ }
105
+
106
+ private createShapeLayer(data: AddShapeLayerData): ShapeLayer {
107
+ return {
108
+ id: this.generateId(),
109
+ type: "shape",
110
+ shape: (data.shape as ShapeLayer["shape"]) || "rectangle",
111
+ position: { x: 25, y: 25 },
112
+ size: { width: 50, height: 50 },
113
+ rotation: 0,
114
+ opacity: data.opacity ?? 1,
115
+ fillColor: data.fillColor || "#000000",
116
+ borderColor: data.borderColor,
117
+ borderWidth: data.borderWidth,
118
+ };
119
+ }
120
+
121
+ private generateId(): string {
122
+ return `layer_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
123
+ }
124
+ }
125
+
126
+ // Use Case: Update Layer
127
+ export class UpdateLayerUseCase {
128
+ constructor(private readonly layerRepository: LayerRepository) {}
129
+
130
+ async execute(request: UpdateLayerRequest): Promise<void> {
131
+ const { sceneId, layerId, updates } = request;
132
+
133
+ // Validate
134
+ if (!sceneId || !layerId) {
135
+ throw new Error("Scene ID and Layer ID are required");
136
+ }
137
+
138
+ // Persist through repository
139
+ await this.layerRepository.updateLayer(sceneId, layerId, updates);
140
+ }
141
+ }
142
+
143
+ // Use Case: Delete Layer
144
+ export class DeleteLayerUseCase {
145
+ constructor(private readonly layerRepository: LayerRepository) {}
146
+
147
+ async execute(request: DeleteLayerRequest): Promise<void> {
148
+ const { sceneId, layerId } = request;
149
+
150
+ // Validate
151
+ if (!sceneId || !layerId) {
152
+ throw new Error("Scene ID and Layer ID are required");
153
+ }
154
+
155
+ // Persist through repository
156
+ await this.layerRepository.deleteLayer(sceneId, layerId);
157
+ }
158
+ }
159
+
160
+ // Use Case: Duplicate Layer
161
+ export class DuplicateLayerUseCase {
162
+ constructor(private readonly layerRepository: LayerRepository) {}
163
+
164
+ async execute(sceneId: string, layerId: string): Promise<Layer> {
165
+ // Validate
166
+ if (!sceneId || !layerId) {
167
+ throw new Error("Scene ID and Layer ID are required");
168
+ }
169
+
170
+ // Persist through repository
171
+ return await this.layerRepository.duplicateLayer(sceneId, layerId);
172
+ }
173
+ }
174
+
175
+ // Use Case: Reorder Layer
176
+ export class ReorderLayerUseCase {
177
+ constructor(private readonly layerRepository: LayerRepository) {}
178
+
179
+ async execute(
180
+ sceneId: string,
181
+ layerId: string,
182
+ direction: "front" | "back" | "up" | "down",
183
+ ): Promise<void> {
184
+ // Validate
185
+ if (!sceneId || !layerId) {
186
+ throw new Error("Scene ID and Layer ID are required");
187
+ }
188
+
189
+ // Persist through repository
190
+ await this.layerRepository.reorderLayer(sceneId, layerId, direction);
191
+ }
192
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Layer Repository Implementation
3
+ * Single Responsibility: Persist layer data
4
+ * Infrastructure Layer
5
+ *
6
+ * Implements the LayerRepository interface from the application layer.
7
+ * Handles data persistence and retrieval.
8
+ */
9
+
10
+ import type { Layer, Scene } from "../../domain/entities/video-project.types";
11
+ import type { LayerRepository } from "../../application/usecases/LayerUseCases";
12
+
13
+ /**
14
+ * In-memory implementation of LayerRepository
15
+ * In production, this would connect to a database, API, or state management
16
+ */
17
+ export class LayerRepositoryImpl implements LayerRepository {
18
+ constructor(
19
+ private readonly getScenes: () => Scene[],
20
+ private readonly setScenes: (scenes: Scene[]) => void,
21
+ ) {}
22
+
23
+ async addLayer(sceneId: string, layer: Layer): Promise<void> {
24
+ const scenes = this.getScenes();
25
+ const sceneIndex = scenes.findIndex((s) => s.id === sceneId);
26
+
27
+ if (sceneIndex === -1) {
28
+ throw new Error(`Scene not found: ${sceneId}`);
29
+ }
30
+
31
+ scenes[sceneIndex].layers.push(layer);
32
+ this.setScenes([...scenes]);
33
+ }
34
+
35
+ async updateLayer(
36
+ sceneId: string,
37
+ layerId: string,
38
+ updates: Partial<Layer>,
39
+ ): Promise<void> {
40
+ const scenes = this.getScenes();
41
+ const sceneIndex = scenes.findIndex((s) => s.id === sceneId);
42
+
43
+ if (sceneIndex === -1) {
44
+ throw new Error(`Scene not found: ${sceneId}`);
45
+ }
46
+
47
+ const layerIndex = scenes[sceneIndex].layers.findIndex((l) => l.id === layerId);
48
+
49
+ if (layerIndex === -1) {
50
+ throw new Error(`Layer not found: ${layerId}`);
51
+ }
52
+
53
+ scenes[sceneIndex].layers[layerIndex] = {
54
+ ...scenes[sceneIndex].layers[layerIndex],
55
+ ...updates,
56
+ } as Layer;
57
+
58
+ this.setScenes([...scenes]);
59
+ }
60
+
61
+ async deleteLayer(sceneId: string, layerId: string): Promise<void> {
62
+ const scenes = this.getScenes();
63
+ const sceneIndex = scenes.findIndex((s) => s.id === sceneId);
64
+
65
+ if (sceneIndex === -1) {
66
+ throw new Error(`Scene not found: ${sceneId}`);
67
+ }
68
+
69
+ scenes[sceneIndex].layers = scenes[sceneIndex].layers.filter(
70
+ (l) => l.id !== layerId,
71
+ );
72
+
73
+ this.setScenes([...scenes]);
74
+ }
75
+
76
+ async duplicateLayer(sceneId: string, layerId: string): Promise<Layer> {
77
+ const scenes = this.getScenes();
78
+ const sceneIndex = scenes.findIndex((s) => s.id === sceneId);
79
+
80
+ if (sceneIndex === -1) {
81
+ throw new Error(`Scene not found: ${sceneId}`);
82
+ }
83
+
84
+ const layer = scenes[sceneIndex].layers.find((l) => l.id === layerId);
85
+
86
+ if (!layer) {
87
+ throw new Error(`Layer not found: ${layerId}`);
88
+ }
89
+
90
+ const duplicated: Layer = {
91
+ ...layer,
92
+ id: `layer_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
93
+ position: {
94
+ x: layer.position.x + 5,
95
+ y: layer.position.y + 5,
96
+ },
97
+ };
98
+
99
+ scenes[sceneIndex].layers.push(duplicated);
100
+ this.setScenes([...scenes]);
101
+
102
+ return duplicated;
103
+ }
104
+
105
+ async reorderLayer(
106
+ sceneId: string,
107
+ layerId: string,
108
+ direction: "front" | "back" | "up" | "down",
109
+ ): Promise<void> {
110
+ const scenes = this.getScenes();
111
+ const sceneIndex = scenes.findIndex((s) => s.id === sceneId);
112
+
113
+ if (sceneIndex === -1) {
114
+ throw new Error(`Scene not found: ${sceneId}`);
115
+ }
116
+
117
+ const layers = scenes[sceneIndex].layers;
118
+ const layerIndex = layers.findIndex((l) => l.id === layerId);
119
+
120
+ if (layerIndex === -1) {
121
+ throw new Error(`Layer not found: ${layerId}`);
122
+ }
123
+
124
+ const [layer] = layers.splice(layerIndex, 1);
125
+
126
+ switch (direction) {
127
+ case "front":
128
+ layers.push(layer);
129
+ break;
130
+ case "back":
131
+ layers.unshift(layer);
132
+ break;
133
+ case "up":
134
+ if (layerIndex < layers.length) {
135
+ layers.splice(layerIndex + 1, 0, layer);
136
+ }
137
+ break;
138
+ case "down":
139
+ if (layerIndex > 0) {
140
+ layers.splice(layerIndex - 1, 0, layer);
141
+ }
142
+ break;
143
+ }
144
+
145
+ scenes[sceneIndex].layers = layers;
146
+ this.setScenes([...scenes]);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Factory function to create repository with state management integration
152
+ */
153
+ export function createLayerRepository(
154
+ getScenes: () => Scene[],
155
+ setScenes: (scenes: Scene[]) => void,
156
+ ): LayerRepository {
157
+ return new LayerRepositoryImpl(getScenes, setScenes);
158
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Debounce & Throttle Utilities
3
+ * PERFORMANCE: Prevents excessive function calls
4
+ * - Debounce: Wait before executing (search, auto-save)
5
+ * - Throttle: Execute at most once per interval (scroll, resize)
6
+ */
7
+
8
+ /**
9
+ * Debounce function execution
10
+ * @param func Function to debounce
11
+ * @param wait Wait time in ms (default: 300)
12
+ * @returns Debounced function
13
+ */
14
+ export function debounce<T extends (...args: unknown[]) => unknown>(
15
+ func: T,
16
+ wait: number = 300,
17
+ ): (...args: Parameters<T>) => void {
18
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
19
+
20
+ return function debounced(...args: Parameters<T>) {
21
+ if (timeoutId) {
22
+ clearTimeout(timeoutId);
23
+ }
24
+
25
+ timeoutId = setTimeout(() => {
26
+ func(...args);
27
+ timeoutId = null;
28
+ }, wait);
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Throttle function execution
34
+ * @param func Function to throttle
35
+ * @param limit Minimum time between calls in ms (default: 100)
36
+ * @returns Throttled function
37
+ */
38
+ export function throttle<T extends (...args: unknown[]) => unknown>(
39
+ func: T,
40
+ limit: number = 100,
41
+ ): (...args: Parameters<T>) => void {
42
+ let inThrottle = false;
43
+ let lastResult: ReturnType<T>;
44
+
45
+ return function throttled(...args: Parameters<T>) {
46
+ if (!inThrottle) {
47
+ lastResult = func(...args) as ReturnType<T>;
48
+ inThrottle = true;
49
+
50
+ setTimeout(() => {
51
+ inThrottle = false;
52
+ }, limit);
53
+ }
54
+
55
+ return lastResult;
56
+ };
57
+ }
58
+
59
+ /**
60
+ * React hook for debounced value
61
+ * @param value Value to debounce
62
+ * @param _delay Delay in ms (default: 500)
63
+ * @returns Debounced value
64
+ */
65
+ export function useDebouncedValue<T>(value: T, _delay: number = 500): T {
66
+ // This would be implemented in a separate hook file
67
+ // For now, just return the value
68
+ return value;
69
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Image Processing Utilities
3
+ * PERFORMANCE: Optimizes image handling for memory efficiency
4
+ * NOTE: Requires expo-image-manipulator package (graceful degradation if not installed)
5
+ */
6
+
7
+ let ImageManipulator: any = null;
8
+ try {
9
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
10
+ ImageManipulator = require('expo-image-manipulator');
11
+ } catch {
12
+ ImageManipulator = null;
13
+ }
14
+
15
+ export async function resizeImage(uri: string, maxWidth: number = 1024, maxHeight: number = 1024): Promise<string> {
16
+ if (!ImageManipulator) return uri;
17
+ try {
18
+ const result = await ImageManipulator.manipulateAsync(uri, [{ resize: { width: maxWidth, height: maxHeight } }], { compress: 0.8, format: ImageManipulator.SaveFormat.JPEG });
19
+ return result.uri;
20
+ } catch (error) {
21
+ console.warn('Image resize failed:', error);
22
+ return uri;
23
+ }
24
+ }
25
+
26
+ export async function generateThumbnail(uri: string, size: number = 200): Promise<string> {
27
+ if (!ImageManipulator) return uri;
28
+ try {
29
+ const result = await ImageManipulator.manipulateAsync(uri, [{ resize: { width: size, height: size } }], { compress: 0.7, format: ImageManipulator.SaveFormat.JPEG });
30
+ return result.uri;
31
+ } catch (error) {
32
+ console.warn('Thumbnail generation failed:', error);
33
+ return uri;
34
+ }
35
+ }
36
+
37
+ export async function compressImage(uri: string, quality: number = 0.8): Promise<string> {
38
+ if (!ImageManipulator) return uri;
39
+ try {
40
+ const result = await ImageManipulator.manipulateAsync(uri, [], { compress: quality, format: ImageManipulator.SaveFormat.JPEG });
41
+ return result.uri;
42
+ } catch (error) {
43
+ console.warn('Image compression failed:', error);
44
+ return uri;
45
+ }
46
+ }
47
+
48
+ export async function getImageDimensions(uri: string): Promise<{ width: number; height: number } | null> {
49
+ if (!ImageManipulator) return null;
50
+ try {
51
+ const result = await ImageManipulator.manipulateAsync(uri, [], { compress: 1, format: ImageManipulator.SaveFormat.JPEG });
52
+ return { width: result.width, height: result.height };
53
+ } catch (error) {
54
+ console.warn('Failed to get image dimensions:', error);
55
+ return null;
56
+ }
57
+ }
58
+
59
+ export async function optimizeImageForEditor(uri: string): Promise<string> {
60
+ const MAX_DIMENSION = 1920;
61
+ if (!ImageManipulator) return uri;
62
+ try {
63
+ const dimensions = await getImageDimensions(uri);
64
+ if (!dimensions) return uri;
65
+ if (dimensions.width > MAX_DIMENSION || dimensions.height > MAX_DIMENSION) {
66
+ return await resizeImage(uri, MAX_DIMENSION, MAX_DIMENSION);
67
+ }
68
+ return await compressImage(uri, 0.85);
69
+ } catch (error) {
70
+ console.warn('Image optimization failed:', error);
71
+ return uri;
72
+ }
73
+ }