@umituz/react-native-design-system 4.25.10 → 4.25.12
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/gallery/gallery-download.service.ts +69 -0
- package/src/gallery/gallery-save.service.ts +80 -0
- package/src/gallery/index.ts +3 -0
- package/src/gallery/types.ts +11 -0
- package/src/index.ts +49 -17
- package/src/utilities/clipboard/ClipboardUtils.ts +67 -0
- package/src/utilities/clipboard/index.ts +5 -0
- package/src/utilities/index.ts +6 -0
- package/src/utilities/sharing/domain/entities/Share.ts +104 -0
- package/src/utilities/sharing/domain/entities/SharingUtils.ts +111 -0
- package/src/utilities/sharing/index.ts +33 -0
- package/src/utilities/sharing/infrastructure/services/SharingService.ts +165 -0
- package/src/utilities/sharing/presentation/hooks/useSharing.ts +116 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-design-system",
|
|
3
|
-
"version": "4.25.
|
|
3
|
+
"version": "4.25.12",
|
|
4
4
|
"description": "Universal design system for React Native apps - Consolidated package with atoms, molecules, organisms, theme, typography, responsive, safe area, exception, infinite scroll, UUID, image, timezone, offline, onboarding, and loading utilities",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gallery Download Service
|
|
3
|
+
* Single Responsibility: Download remote media to local storage
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { FileSystemService } from "../filesystem";
|
|
7
|
+
import { validateImageUri, getFileExtension } from "../image";
|
|
8
|
+
import { timezoneService } from "../timezone";
|
|
9
|
+
import type { DownloadMediaResult } from "./types";
|
|
10
|
+
|
|
11
|
+
const generateFilename = (uri: string, prefix: string): string => {
|
|
12
|
+
const extension = getFileExtension(uri) || "jpg";
|
|
13
|
+
const timestamp = timezoneService.formatDateToString(new Date());
|
|
14
|
+
const randomId = Math.random().toString(36).substring(2, 10);
|
|
15
|
+
return `${prefix}_${timestamp}_${randomId}.${extension}`;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
class GalleryDownloadService {
|
|
19
|
+
async downloadMedia(
|
|
20
|
+
mediaUri: string,
|
|
21
|
+
prefix: string = "media",
|
|
22
|
+
): Promise<DownloadMediaResult> {
|
|
23
|
+
try {
|
|
24
|
+
const validationResult = validateImageUri(mediaUri, "Media");
|
|
25
|
+
if (!validationResult.isValid) {
|
|
26
|
+
return {
|
|
27
|
+
success: false,
|
|
28
|
+
error: validationResult.error || "Invalid media file",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const filename = generateFilename(mediaUri, prefix);
|
|
33
|
+
const documentDir = FileSystemService.getDocumentDirectory();
|
|
34
|
+
const fileUri = `${documentDir}${filename}`;
|
|
35
|
+
|
|
36
|
+
const downloadResult = await FileSystemService.downloadFile(
|
|
37
|
+
mediaUri,
|
|
38
|
+
fileUri,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (!downloadResult.success || !downloadResult.uri) {
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
error: downloadResult.error || "Download failed",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
success: true,
|
|
50
|
+
localUri: downloadResult.uri,
|
|
51
|
+
};
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
error: error instanceof Error ? error.message : "Download failed",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
isRemoteUrl(uri: string): boolean {
|
|
61
|
+
return uri.startsWith("http://") || uri.startsWith("https://");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async cleanupFile(fileUri: string): Promise<void> {
|
|
65
|
+
await FileSystemService.deleteFile(fileUri);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const galleryDownloadService = new GalleryDownloadService();
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gallery Save Service
|
|
3
|
+
* Single Responsibility: Save media to device gallery
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as MediaLibrary from "expo-media-library";
|
|
7
|
+
import { validateImageUri } from "../image";
|
|
8
|
+
import { galleryDownloadService } from "./gallery-download.service";
|
|
9
|
+
import type { SaveMediaResult } from "./types";
|
|
10
|
+
|
|
11
|
+
const requestMediaPermissions = async (): Promise<boolean> => {
|
|
12
|
+
try {
|
|
13
|
+
const { status } = await MediaLibrary.requestPermissionsAsync();
|
|
14
|
+
return status === "granted";
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
class GallerySaveService {
|
|
21
|
+
async saveToGallery(
|
|
22
|
+
mediaUri: string,
|
|
23
|
+
prefix?: string,
|
|
24
|
+
): Promise<SaveMediaResult> {
|
|
25
|
+
try {
|
|
26
|
+
const validationResult = validateImageUri(mediaUri, "Media");
|
|
27
|
+
if (!validationResult.isValid) {
|
|
28
|
+
return {
|
|
29
|
+
success: false,
|
|
30
|
+
error: validationResult.error || "Invalid media file",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const hasPermission = await requestMediaPermissions();
|
|
35
|
+
if (!hasPermission) {
|
|
36
|
+
return {
|
|
37
|
+
success: false,
|
|
38
|
+
error: "Media library permission denied",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let localUri = mediaUri;
|
|
43
|
+
const isRemote = galleryDownloadService.isRemoteUrl(mediaUri);
|
|
44
|
+
|
|
45
|
+
if (isRemote) {
|
|
46
|
+
const downloadResult = await galleryDownloadService.downloadMedia(
|
|
47
|
+
mediaUri,
|
|
48
|
+
prefix,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (!downloadResult.success || !downloadResult.localUri) {
|
|
52
|
+
return {
|
|
53
|
+
success: false,
|
|
54
|
+
error: downloadResult.error || "Download failed",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
localUri = downloadResult.localUri;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const asset = await MediaLibrary.createAssetAsync(localUri);
|
|
62
|
+
|
|
63
|
+
if (isRemote && localUri) {
|
|
64
|
+
await galleryDownloadService.cleanupFile(localUri);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
success: true,
|
|
69
|
+
fileUri: asset.uri,
|
|
70
|
+
};
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return {
|
|
73
|
+
success: false,
|
|
74
|
+
error: error instanceof Error ? error.message : "Save failed",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const gallerySaveService = new GallerySaveService();
|
package/src/index.ts
CHANGED
|
@@ -2,16 +2,8 @@
|
|
|
2
2
|
* @umituz/react-native-design-system
|
|
3
3
|
* Universal design system for React Native apps
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - @umituz/react-native-design-system/gallery (expo-media-library)
|
|
8
|
-
* - @umituz/react-native-design-system/media (expo-file-system, expo-image-picker, expo-media-library)
|
|
9
|
-
* - @umituz/react-native-design-system/filesystem (expo-file-system)
|
|
10
|
-
* - @umituz/react-native-design-system/image (expo-file-system, expo-image-manipulator)
|
|
11
|
-
* - @umituz/react-native-design-system/device (expo-device, expo-secure-store)
|
|
12
|
-
* - @umituz/react-native-design-system/offline (expo-network)
|
|
13
|
-
* - @umituz/react-native-design-system/onboarding (expo-video)
|
|
14
|
-
* - @umituz/react-native-design-system/storage (expo-secure-store)
|
|
5
|
+
* Consolidated package including all modules.
|
|
6
|
+
* Sub-path imports also available (e.g. @umituz/react-native-design-system/media)
|
|
15
7
|
*/
|
|
16
8
|
|
|
17
9
|
// =============================================================================
|
|
@@ -29,6 +21,11 @@ export * from "./typography";
|
|
|
29
21
|
// =============================================================================
|
|
30
22
|
export * from "./responsive";
|
|
31
23
|
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// DEVICE EXPORTS
|
|
26
|
+
// =============================================================================
|
|
27
|
+
export * from "./device";
|
|
28
|
+
|
|
32
29
|
// =============================================================================
|
|
33
30
|
// ATOMS EXPORTS
|
|
34
31
|
// =============================================================================
|
|
@@ -64,27 +61,67 @@ export * from "./exception";
|
|
|
64
61
|
// =============================================================================
|
|
65
62
|
export * from "./infinite-scroll";
|
|
66
63
|
|
|
64
|
+
// =============================================================================
|
|
65
|
+
// UUID EXPORTS
|
|
66
|
+
// =============================================================================
|
|
67
|
+
export * from "./uuid";
|
|
68
|
+
|
|
67
69
|
// =============================================================================
|
|
68
70
|
// TIMEZONE EXPORTS
|
|
69
71
|
// =============================================================================
|
|
70
72
|
export * from "./timezone";
|
|
71
73
|
|
|
74
|
+
// =============================================================================
|
|
75
|
+
// OFFLINE EXPORTS
|
|
76
|
+
// =============================================================================
|
|
77
|
+
export * from "./offline";
|
|
78
|
+
|
|
79
|
+
// =============================================================================
|
|
80
|
+
// IMAGE EXPORTS
|
|
81
|
+
// =============================================================================
|
|
82
|
+
export * from "./image";
|
|
83
|
+
|
|
72
84
|
// =============================================================================
|
|
73
85
|
// HAPTICS EXPORTS
|
|
74
86
|
// =============================================================================
|
|
75
87
|
export * from "./haptics";
|
|
76
88
|
|
|
89
|
+
// =============================================================================
|
|
90
|
+
// MEDIA EXPORTS
|
|
91
|
+
// =============================================================================
|
|
92
|
+
export * from "./media";
|
|
93
|
+
|
|
77
94
|
// =============================================================================
|
|
78
95
|
// VARIANT UTILITIES
|
|
79
96
|
// =============================================================================
|
|
80
97
|
export * from "./presentation/utils/variants";
|
|
81
98
|
|
|
99
|
+
// =============================================================================
|
|
100
|
+
// UTILITIES
|
|
101
|
+
// =============================================================================
|
|
102
|
+
export * from "./utilities";
|
|
103
|
+
|
|
82
104
|
// =============================================================================
|
|
83
105
|
// UTILS EXPORTS (Logger, formatters, validators)
|
|
84
106
|
// =============================================================================
|
|
85
107
|
export { logger, Logger } from "./utils/logger";
|
|
86
108
|
export type { LoggerConfig } from "./utils/logger";
|
|
87
109
|
|
|
110
|
+
// =============================================================================
|
|
111
|
+
// STORAGE EXPORTS
|
|
112
|
+
// =============================================================================
|
|
113
|
+
export * from "./storage";
|
|
114
|
+
|
|
115
|
+
// =============================================================================
|
|
116
|
+
// ONBOARDING EXPORTS
|
|
117
|
+
// =============================================================================
|
|
118
|
+
export * from "./onboarding";
|
|
119
|
+
|
|
120
|
+
// =============================================================================
|
|
121
|
+
// FILESYSTEM EXPORTS
|
|
122
|
+
// =============================================================================
|
|
123
|
+
export * from "./filesystem";
|
|
124
|
+
|
|
88
125
|
// =============================================================================
|
|
89
126
|
// TANSTACK EXPORTS
|
|
90
127
|
// =============================================================================
|
|
@@ -101,16 +138,11 @@ export * from "./loading";
|
|
|
101
138
|
export * from "./init";
|
|
102
139
|
|
|
103
140
|
// =============================================================================
|
|
104
|
-
//
|
|
141
|
+
// GALLERY EXPORTS
|
|
105
142
|
// =============================================================================
|
|
106
|
-
export * from "./
|
|
143
|
+
export * from "./gallery";
|
|
107
144
|
|
|
108
145
|
// =============================================================================
|
|
109
146
|
// CAROUSEL EXPORTS
|
|
110
147
|
// =============================================================================
|
|
111
148
|
export * from "./carousel";
|
|
112
|
-
|
|
113
|
-
// =============================================================================
|
|
114
|
-
// OFFLINE EXPORTS
|
|
115
|
-
// =============================================================================
|
|
116
|
-
export * from "./offline";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clipboard Utilities
|
|
3
|
+
*
|
|
4
|
+
* Simple wrapper around expo-clipboard for copy/paste functionality
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { ClipboardUtils } from '@umituz/react-native-design-system';
|
|
9
|
+
*
|
|
10
|
+
* // Copy text
|
|
11
|
+
* await ClipboardUtils.copyToClipboard('Hello World');
|
|
12
|
+
*
|
|
13
|
+
* // Paste text
|
|
14
|
+
* const text = await ClipboardUtils.getFromClipboard();
|
|
15
|
+
*
|
|
16
|
+
* // Check if clipboard has content
|
|
17
|
+
* const hasContent = await ClipboardUtils.hasContent();
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import * as Clipboard from 'expo-clipboard';
|
|
22
|
+
|
|
23
|
+
export class ClipboardUtils {
|
|
24
|
+
/**
|
|
25
|
+
* Copy text to clipboard
|
|
26
|
+
*/
|
|
27
|
+
static async copyToClipboard(text: string): Promise<void> {
|
|
28
|
+
try {
|
|
29
|
+
await Clipboard.setStringAsync(text);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get text from clipboard
|
|
37
|
+
*/
|
|
38
|
+
static async getFromClipboard(): Promise<string> {
|
|
39
|
+
try {
|
|
40
|
+
return await Clipboard.getStringAsync();
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if clipboard has content
|
|
48
|
+
*/
|
|
49
|
+
static async hasContent(): Promise<boolean> {
|
|
50
|
+
try {
|
|
51
|
+
return await Clipboard.hasStringAsync();
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Clear clipboard
|
|
59
|
+
*/
|
|
60
|
+
static async clear(): Promise<void> {
|
|
61
|
+
try {
|
|
62
|
+
await Clipboard.setStringAsync('');
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sharing Domain - Core Entities
|
|
3
|
+
*
|
|
4
|
+
* This file defines core types and interfaces for sharing functionality.
|
|
5
|
+
* Handles system share sheet using expo-sharing.
|
|
6
|
+
*
|
|
7
|
+
* @domain sharing
|
|
8
|
+
* @layer domain/entities
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Share options for sharing content
|
|
13
|
+
*/
|
|
14
|
+
export interface ShareOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Dialog title (Android only)
|
|
17
|
+
*/
|
|
18
|
+
dialogTitle?: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* MIME type of the file being shared
|
|
22
|
+
*/
|
|
23
|
+
mimeType?: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* UTI (Uniform Type Identifier) for the file (iOS only)
|
|
27
|
+
*/
|
|
28
|
+
UTI?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Share result
|
|
33
|
+
*/
|
|
34
|
+
export interface ShareResult {
|
|
35
|
+
success: boolean;
|
|
36
|
+
error?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Common MIME types for sharing
|
|
41
|
+
*/
|
|
42
|
+
export const MIME_TYPES = {
|
|
43
|
+
// Images
|
|
44
|
+
IMAGE_JPEG: 'image/jpeg',
|
|
45
|
+
IMAGE_PNG: 'image/png',
|
|
46
|
+
IMAGE_GIF: 'image/gif',
|
|
47
|
+
IMAGE_WEBP: 'image/webp',
|
|
48
|
+
|
|
49
|
+
// Videos
|
|
50
|
+
VIDEO_MP4: 'video/mp4',
|
|
51
|
+
VIDEO_QUICKTIME: 'video/quicktime',
|
|
52
|
+
VIDEO_AVI: 'video/avi',
|
|
53
|
+
|
|
54
|
+
// Audio
|
|
55
|
+
AUDIO_MP3: 'audio/mpeg',
|
|
56
|
+
AUDIO_WAV: 'audio/wav',
|
|
57
|
+
AUDIO_AAC: 'audio/aac',
|
|
58
|
+
|
|
59
|
+
// Documents
|
|
60
|
+
PDF: 'application/pdf',
|
|
61
|
+
TEXT: 'text/plain',
|
|
62
|
+
JSON: 'application/json',
|
|
63
|
+
ZIP: 'application/zip',
|
|
64
|
+
|
|
65
|
+
// Generic
|
|
66
|
+
OCTET_STREAM: 'application/octet-stream',
|
|
67
|
+
} as const;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* iOS UTI (Uniform Type Identifiers)
|
|
71
|
+
*/
|
|
72
|
+
export const UTI_TYPES = {
|
|
73
|
+
// Images
|
|
74
|
+
IMAGE: 'public.image',
|
|
75
|
+
JPEG: 'public.jpeg',
|
|
76
|
+
PNG: 'public.png',
|
|
77
|
+
|
|
78
|
+
// Videos
|
|
79
|
+
VIDEO: 'public.video',
|
|
80
|
+
MOVIE: 'public.movie',
|
|
81
|
+
|
|
82
|
+
// Audio
|
|
83
|
+
AUDIO: 'public.audio',
|
|
84
|
+
MP3: 'public.mp3',
|
|
85
|
+
|
|
86
|
+
// Documents
|
|
87
|
+
PDF: 'com.adobe.pdf',
|
|
88
|
+
TEXT: 'public.text',
|
|
89
|
+
JSON: 'public.json',
|
|
90
|
+
|
|
91
|
+
// Generic
|
|
92
|
+
DATA: 'public.data',
|
|
93
|
+
CONTENT: 'public.content',
|
|
94
|
+
} as const;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Sharing constants
|
|
98
|
+
*/
|
|
99
|
+
export const SHARING_CONSTANTS = {
|
|
100
|
+
DEFAULT_DIALOG_TITLE: 'Share',
|
|
101
|
+
DEFAULT_MIME_TYPE: MIME_TYPES.OCTET_STREAM,
|
|
102
|
+
} as const;
|
|
103
|
+
|
|
104
|
+
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sharing Utilities
|
|
3
|
+
* Helper functions for sharing functionality
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { MIME_TYPES, UTI_TYPES, SHARING_CONSTANTS, ShareOptions } from './Share';
|
|
7
|
+
|
|
8
|
+
export class SharingUtils {
|
|
9
|
+
/**
|
|
10
|
+
* Get MIME type from file extension
|
|
11
|
+
*/
|
|
12
|
+
static getMimeTypeFromExtension(filename: string): string {
|
|
13
|
+
const extension = filename.split('.').pop()?.toLowerCase();
|
|
14
|
+
|
|
15
|
+
switch (extension) {
|
|
16
|
+
// Images
|
|
17
|
+
case 'jpg':
|
|
18
|
+
case 'jpeg':
|
|
19
|
+
return MIME_TYPES.IMAGE_JPEG;
|
|
20
|
+
case 'png':
|
|
21
|
+
return MIME_TYPES.IMAGE_PNG;
|
|
22
|
+
case 'gif':
|
|
23
|
+
return MIME_TYPES.IMAGE_GIF;
|
|
24
|
+
case 'webp':
|
|
25
|
+
return MIME_TYPES.IMAGE_WEBP;
|
|
26
|
+
|
|
27
|
+
// Videos
|
|
28
|
+
case 'mp4':
|
|
29
|
+
return MIME_TYPES.VIDEO_MP4;
|
|
30
|
+
case 'mov':
|
|
31
|
+
return MIME_TYPES.VIDEO_QUICKTIME;
|
|
32
|
+
case 'avi':
|
|
33
|
+
return MIME_TYPES.VIDEO_AVI;
|
|
34
|
+
|
|
35
|
+
// Audio
|
|
36
|
+
case 'mp3':
|
|
37
|
+
return MIME_TYPES.AUDIO_MP3;
|
|
38
|
+
case 'wav':
|
|
39
|
+
return MIME_TYPES.AUDIO_WAV;
|
|
40
|
+
case 'aac':
|
|
41
|
+
return MIME_TYPES.AUDIO_AAC;
|
|
42
|
+
|
|
43
|
+
// Documents
|
|
44
|
+
case 'pdf':
|
|
45
|
+
return MIME_TYPES.PDF;
|
|
46
|
+
case 'txt':
|
|
47
|
+
return MIME_TYPES.TEXT;
|
|
48
|
+
case 'json':
|
|
49
|
+
return MIME_TYPES.JSON;
|
|
50
|
+
case 'zip':
|
|
51
|
+
return MIME_TYPES.ZIP;
|
|
52
|
+
|
|
53
|
+
default:
|
|
54
|
+
return MIME_TYPES.OCTET_STREAM;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get UTI from file extension (iOS)
|
|
60
|
+
*/
|
|
61
|
+
static getUTIFromExtension(filename: string): string {
|
|
62
|
+
const extension = filename.split('.').pop()?.toLowerCase();
|
|
63
|
+
|
|
64
|
+
switch (extension) {
|
|
65
|
+
// Images
|
|
66
|
+
case 'jpg':
|
|
67
|
+
case 'jpeg':
|
|
68
|
+
return UTI_TYPES.JPEG;
|
|
69
|
+
case 'png':
|
|
70
|
+
return UTI_TYPES.PNG;
|
|
71
|
+
case 'gif':
|
|
72
|
+
case 'webp':
|
|
73
|
+
return UTI_TYPES.IMAGE;
|
|
74
|
+
|
|
75
|
+
// Videos
|
|
76
|
+
case 'mp4':
|
|
77
|
+
case 'mov':
|
|
78
|
+
case 'avi':
|
|
79
|
+
return UTI_TYPES.VIDEO;
|
|
80
|
+
|
|
81
|
+
// Audio
|
|
82
|
+
case 'mp3':
|
|
83
|
+
return UTI_TYPES.MP3;
|
|
84
|
+
case 'wav':
|
|
85
|
+
case 'aac':
|
|
86
|
+
return UTI_TYPES.AUDIO;
|
|
87
|
+
|
|
88
|
+
// Documents
|
|
89
|
+
case 'pdf':
|
|
90
|
+
return UTI_TYPES.PDF;
|
|
91
|
+
case 'txt':
|
|
92
|
+
return UTI_TYPES.TEXT;
|
|
93
|
+
case 'json':
|
|
94
|
+
return UTI_TYPES.JSON;
|
|
95
|
+
|
|
96
|
+
default:
|
|
97
|
+
return UTI_TYPES.DATA;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Prepare share options from filename
|
|
103
|
+
*/
|
|
104
|
+
static prepareShareOptions(filename: string, dialogTitle?: string): ShareOptions {
|
|
105
|
+
return {
|
|
106
|
+
dialogTitle: dialogTitle || SHARING_CONSTANTS.DEFAULT_DIALOG_TITLE,
|
|
107
|
+
mimeType: SharingUtils.getMimeTypeFromExtension(filename),
|
|
108
|
+
UTI: SharingUtils.getUTIFromExtension(filename),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sharing Domain - Barrel Export
|
|
3
|
+
* Provides system share sheet functionality using expo-sharing.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// DOMAIN LAYER - ENTITIES
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
export type {
|
|
11
|
+
ShareOptions,
|
|
12
|
+
ShareResult,
|
|
13
|
+
} from './domain/entities/Share';
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
MIME_TYPES,
|
|
17
|
+
UTI_TYPES,
|
|
18
|
+
SHARING_CONSTANTS,
|
|
19
|
+
} from './domain/entities/Share';
|
|
20
|
+
|
|
21
|
+
export { SharingUtils } from './domain/entities/SharingUtils';
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// INFRASTRUCTURE LAYER - SERVICES
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
export { SharingService } from './infrastructure/services/SharingService';
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// PRESENTATION LAYER - HOOKS
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
export { useSharing } from './presentation/hooks/useSharing';
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sharing Domain - Sharing Service
|
|
3
|
+
*
|
|
4
|
+
* Service for sharing files using expo-sharing.
|
|
5
|
+
* Provides abstraction layer for system share sheet.
|
|
6
|
+
*
|
|
7
|
+
* @domain sharing
|
|
8
|
+
* @layer infrastructure/services
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as Sharing from 'expo-sharing';
|
|
12
|
+
import { FileSystemService } from '../../../../filesystem';
|
|
13
|
+
import type { ShareOptions, ShareResult } from '../../domain/entities/Share';
|
|
14
|
+
import { SharingUtils } from '../../domain/entities/SharingUtils';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sharing service for sharing files via system share sheet
|
|
18
|
+
*/
|
|
19
|
+
export class SharingService {
|
|
20
|
+
/**
|
|
21
|
+
* Check if sharing is available on device
|
|
22
|
+
*/
|
|
23
|
+
static async isAvailable(): Promise<boolean> {
|
|
24
|
+
try {
|
|
25
|
+
return await Sharing.isAvailableAsync();
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Share a file via system share sheet
|
|
33
|
+
*
|
|
34
|
+
* @param uri - File URI to share (can be local or remote http/https)
|
|
35
|
+
* @param options - Share options (dialog title, MIME type, UTI)
|
|
36
|
+
* @returns ShareResult with success status
|
|
37
|
+
*
|
|
38
|
+
* USAGE:
|
|
39
|
+
* ```typescript
|
|
40
|
+
* // Basic share (local)
|
|
41
|
+
* await SharingService.shareFile('file:///path/to/image.jpg');
|
|
42
|
+
*
|
|
43
|
+
* // Remote file (will be downloaded)
|
|
44
|
+
* await SharingService.shareFile('https://example.com/image.jpg');
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
static async shareFile(uri: string, options?: ShareOptions): Promise<ShareResult> {
|
|
48
|
+
try {
|
|
49
|
+
// Check availability
|
|
50
|
+
const available = await SharingService.isAvailable();
|
|
51
|
+
if (!available) {
|
|
52
|
+
return {
|
|
53
|
+
success: false,
|
|
54
|
+
error: 'Sharing is not available on this device',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let shareUri = uri;
|
|
59
|
+
|
|
60
|
+
// Handle remote URLs
|
|
61
|
+
if (uri.startsWith('http://') || uri.startsWith('https://')) {
|
|
62
|
+
try {
|
|
63
|
+
const filename = uri.split('/').pop()?.split('?')[0] || `share-${Date.now()}`;
|
|
64
|
+
// Ensure we have an extension if possible, or default to bin/jpg?
|
|
65
|
+
// Better to rely on what we have, or let the caller specify mimeType to infer extension?
|
|
66
|
+
// For now, nice and simple:
|
|
67
|
+
const localUri = FileSystemService.generateFilePath(filename);
|
|
68
|
+
const result = await FileSystemService.downloadFile(uri, localUri);
|
|
69
|
+
|
|
70
|
+
if (!result.success) {
|
|
71
|
+
return { success: false, error: result.error || 'Failed to download file' };
|
|
72
|
+
}
|
|
73
|
+
shareUri = result.uri!;
|
|
74
|
+
} catch (downloadError) {
|
|
75
|
+
return {
|
|
76
|
+
success: false,
|
|
77
|
+
error: downloadError instanceof Error ? downloadError.message : 'Failed to download file'
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Share file
|
|
83
|
+
await Sharing.shareAsync(shareUri, options);
|
|
84
|
+
|
|
85
|
+
return { success: true };
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return {
|
|
88
|
+
success: false,
|
|
89
|
+
error: error instanceof Error ? error.message : 'Failed to share file',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Share a file with automatic MIME type detection
|
|
96
|
+
*
|
|
97
|
+
* @param uri - File URI to share
|
|
98
|
+
* @param filename - Filename (used for MIME type detection)
|
|
99
|
+
* @param dialogTitle - Optional dialog title (Android)
|
|
100
|
+
* @returns ShareResult with success status
|
|
101
|
+
*
|
|
102
|
+
* USAGE:
|
|
103
|
+
* ```typescript
|
|
104
|
+
* // Auto-detect MIME type from filename
|
|
105
|
+
* await SharingService.shareWithAutoType(
|
|
106
|
+
* 'file:///path/to/file.jpg',
|
|
107
|
+
* 'photo.jpg',
|
|
108
|
+
* 'Share Photo'
|
|
109
|
+
* );
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
static async shareWithAutoType(
|
|
113
|
+
uri: string,
|
|
114
|
+
filename: string,
|
|
115
|
+
dialogTitle?: string
|
|
116
|
+
): Promise<ShareResult> {
|
|
117
|
+
const options = SharingUtils.prepareShareOptions(filename, dialogTitle);
|
|
118
|
+
return await SharingService.shareFile(uri, options);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Share multiple files (if supported)
|
|
123
|
+
*
|
|
124
|
+
* NOTE: expo-sharing currently only supports sharing one file at a time.
|
|
125
|
+
* This method shares files sequentially.
|
|
126
|
+
*
|
|
127
|
+
* @param uris - Array of file URIs to share
|
|
128
|
+
* @param options - Share options
|
|
129
|
+
* @returns ShareResult with success status
|
|
130
|
+
*/
|
|
131
|
+
static async shareMultipleFiles(
|
|
132
|
+
uris: string[],
|
|
133
|
+
options?: ShareOptions
|
|
134
|
+
): Promise<ShareResult> {
|
|
135
|
+
try {
|
|
136
|
+
if (uris.length === 0) {
|
|
137
|
+
return {
|
|
138
|
+
success: false,
|
|
139
|
+
error: 'No files to share',
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check availability
|
|
144
|
+
const available = await SharingService.isAvailable();
|
|
145
|
+
if (!available) {
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
error: 'Sharing is not available on this device',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Share files sequentially
|
|
153
|
+
for (const uri of uris) {
|
|
154
|
+
await Sharing.shareAsync(uri, options);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { success: true };
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return {
|
|
160
|
+
success: false,
|
|
161
|
+
error: error instanceof Error ? error.message : 'Failed to share files',
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sharing Domain - useSharing Hook
|
|
3
|
+
*
|
|
4
|
+
* React hook for sharing files.
|
|
5
|
+
* Provides system share sheet functionality with state management.
|
|
6
|
+
*
|
|
7
|
+
* @domain sharing
|
|
8
|
+
* @layer presentation/hooks
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useCallback, useMemo } from 'react';
|
|
12
|
+
import { SharingService } from '../../infrastructure/services/SharingService';
|
|
13
|
+
import type { ShareOptions } from '../../domain/entities/Share';
|
|
14
|
+
import { useAsyncOperation } from '../../../../utils/hooks';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* useSharing hook for sharing files via system share sheet
|
|
18
|
+
*/
|
|
19
|
+
export const useSharing = () => {
|
|
20
|
+
// Check availability on mount
|
|
21
|
+
const availabilityOp = useAsyncOperation<boolean, string>(
|
|
22
|
+
() => SharingService.isAvailable(),
|
|
23
|
+
{
|
|
24
|
+
immediate: true,
|
|
25
|
+
initialData: false,
|
|
26
|
+
errorHandler: () => 'Failed to check sharing availability',
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Share operations
|
|
31
|
+
const shareOp = useAsyncOperation<boolean, string>(
|
|
32
|
+
async (uri: string, options?: ShareOptions) => {
|
|
33
|
+
const result = await SharingService.shareFile(uri, options);
|
|
34
|
+
if (!result.success) {
|
|
35
|
+
throw new Error(result.error || 'Failed to share file');
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
immediate: false,
|
|
41
|
+
errorHandler: (err) => err instanceof Error ? err.message : 'Failed to share file',
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const shareWithAutoTypeOp = useAsyncOperation<boolean, string>(
|
|
46
|
+
async (uri: string, filename: string, dialogTitle?: string) => {
|
|
47
|
+
const result = await SharingService.shareWithAutoType(uri, filename, dialogTitle);
|
|
48
|
+
if (!result.success) {
|
|
49
|
+
throw new Error(result.error || 'Failed to share file');
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
immediate: false,
|
|
55
|
+
errorHandler: (err) => err instanceof Error ? err.message : 'Failed to share file',
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const shareMultipleOp = useAsyncOperation<boolean, string>(
|
|
60
|
+
async (uris: string[], options?: ShareOptions) => {
|
|
61
|
+
const result = await SharingService.shareMultipleFiles(uris, options);
|
|
62
|
+
if (!result.success) {
|
|
63
|
+
throw new Error(result.error || 'Failed to share files');
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
immediate: false,
|
|
69
|
+
errorHandler: (err) => err instanceof Error ? err.message : 'Failed to share files',
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const share = useCallback(
|
|
74
|
+
async (uri: string, options?: ShareOptions): Promise<boolean> => {
|
|
75
|
+
const result = await shareOp.execute(uri, options);
|
|
76
|
+
return result ?? false;
|
|
77
|
+
},
|
|
78
|
+
[shareOp]
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const shareWithAutoType = useCallback(
|
|
82
|
+
async (uri: string, filename: string, dialogTitle?: string): Promise<boolean> => {
|
|
83
|
+
const result = await shareWithAutoTypeOp.execute(uri, filename, dialogTitle);
|
|
84
|
+
return result ?? false;
|
|
85
|
+
},
|
|
86
|
+
[shareWithAutoTypeOp]
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const shareMultiple = useCallback(
|
|
90
|
+
async (uris: string[], options?: ShareOptions): Promise<boolean> => {
|
|
91
|
+
const result = await shareMultipleOp.execute(uris, options);
|
|
92
|
+
return result ?? false;
|
|
93
|
+
},
|
|
94
|
+
[shareMultipleOp]
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return useMemo(() => ({
|
|
98
|
+
share,
|
|
99
|
+
shareWithAutoType,
|
|
100
|
+
shareMultiple,
|
|
101
|
+
isAvailable: availabilityOp.data ?? false,
|
|
102
|
+
isSharing: shareOp.isLoading || shareWithAutoTypeOp.isLoading || shareMultipleOp.isLoading,
|
|
103
|
+
error: shareOp.error || shareWithAutoTypeOp.error || shareMultipleOp.error,
|
|
104
|
+
}), [
|
|
105
|
+
share,
|
|
106
|
+
shareWithAutoType,
|
|
107
|
+
shareMultiple,
|
|
108
|
+
availabilityOp.data,
|
|
109
|
+
shareOp.isLoading,
|
|
110
|
+
shareWithAutoTypeOp.isLoading,
|
|
111
|
+
shareMultipleOp.isLoading,
|
|
112
|
+
shareOp.error,
|
|
113
|
+
shareWithAutoTypeOp.error,
|
|
114
|
+
shareMultipleOp.error,
|
|
115
|
+
]);
|
|
116
|
+
};
|