@uploadista/react-native-core 0.0.3
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/.turbo/turbo-check.log +396 -0
- package/LICENSE +21 -0
- package/README.md +426 -0
- package/package.json +42 -0
- package/src/client/create-uploadista-client.ts +65 -0
- package/src/client/index.ts +4 -0
- package/src/components/CameraUploadButton.tsx +130 -0
- package/src/components/FileUploadButton.tsx +130 -0
- package/src/components/GalleryUploadButton.tsx +199 -0
- package/src/components/UploadList.tsx +214 -0
- package/src/components/UploadProgress.tsx +196 -0
- package/src/components/index.ts +19 -0
- package/src/hooks/index.ts +29 -0
- package/src/hooks/uploadista-context.ts +17 -0
- package/src/hooks/use-camera-upload.ts +38 -0
- package/src/hooks/use-file-upload.ts +40 -0
- package/src/hooks/use-flow-upload.ts +242 -0
- package/src/hooks/use-gallery-upload.ts +65 -0
- package/src/hooks/use-multi-upload.ts +363 -0
- package/src/hooks/use-upload-metrics.ts +82 -0
- package/src/hooks/use-upload.ts +378 -0
- package/src/hooks/use-uploadista-client.ts +23 -0
- package/src/hooks/use-uploadista-context.ts +20 -0
- package/src/index.ts +111 -0
- package/src/types/index.ts +2 -0
- package/src/types/types.ts +359 -0
- package/src/types/upload-input.ts +1 -0
- package/src/utils/fileHelpers.ts +201 -0
- package/src/utils/index.ts +36 -0
- package/src/utils/permissions.ts +177 -0
- package/src/utils/uriHelpers.ts +148 -0
- package/test-compile.ts +5 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
|
|
2
|
+
import type { UploadState } from "../hooks/use-upload";
|
|
3
|
+
import { formatFileSize } from "../utils";
|
|
4
|
+
|
|
5
|
+
export interface UploadProgressProps {
|
|
6
|
+
/** Upload state information */
|
|
7
|
+
state: UploadState;
|
|
8
|
+
/** Optional custom label */
|
|
9
|
+
label?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Component to display upload progress with percentage, size, and speed
|
|
14
|
+
*/
|
|
15
|
+
export function UploadProgress({ state, label }: UploadProgressProps) {
|
|
16
|
+
const getStatusColor = () => {
|
|
17
|
+
switch (state.status) {
|
|
18
|
+
case "uploading":
|
|
19
|
+
return "#007AFF";
|
|
20
|
+
case "success":
|
|
21
|
+
return "#34C759";
|
|
22
|
+
case "error":
|
|
23
|
+
case "aborted":
|
|
24
|
+
return "#FF3B30";
|
|
25
|
+
default:
|
|
26
|
+
return "#999999";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const renderContent = () => {
|
|
31
|
+
switch (state.status) {
|
|
32
|
+
case "idle":
|
|
33
|
+
return (
|
|
34
|
+
<View style={styles.container}>
|
|
35
|
+
<Text style={styles.label}>{label || "Ready to upload"}</Text>
|
|
36
|
+
</View>
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
case "uploading":
|
|
40
|
+
return (
|
|
41
|
+
<View style={styles.container}>
|
|
42
|
+
<View style={styles.headerRow}>
|
|
43
|
+
<Text style={styles.label}>{label || "Uploading"}</Text>
|
|
44
|
+
<Text style={styles.percentage}>{state.progress}%</Text>
|
|
45
|
+
</View>
|
|
46
|
+
|
|
47
|
+
{/* Progress bar */}
|
|
48
|
+
<View style={styles.progressBarContainer}>
|
|
49
|
+
<View
|
|
50
|
+
style={[
|
|
51
|
+
styles.progressBar,
|
|
52
|
+
{
|
|
53
|
+
width: `${state.progress}%`,
|
|
54
|
+
backgroundColor: getStatusColor(),
|
|
55
|
+
},
|
|
56
|
+
]}
|
|
57
|
+
/>
|
|
58
|
+
</View>
|
|
59
|
+
|
|
60
|
+
{/* Details row */}
|
|
61
|
+
<View style={styles.detailsRow}>
|
|
62
|
+
<Text style={styles.detail}>
|
|
63
|
+
{formatFileSize(state.bytesUploaded)} /{" "}
|
|
64
|
+
{formatFileSize(state.totalBytes || 0)}
|
|
65
|
+
</Text>
|
|
66
|
+
</View>
|
|
67
|
+
</View>
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
case "success":
|
|
71
|
+
return (
|
|
72
|
+
<View style={styles.container}>
|
|
73
|
+
<View style={styles.headerRow}>
|
|
74
|
+
<Text style={[styles.label, { color: getStatusColor() }]}>
|
|
75
|
+
{label || "Upload complete"}
|
|
76
|
+
</Text>
|
|
77
|
+
<Text style={[styles.percentage, { color: getStatusColor() }]}>
|
|
78
|
+
✓
|
|
79
|
+
</Text>
|
|
80
|
+
</View>
|
|
81
|
+
<Text style={[styles.detail, { color: getStatusColor() }]}>
|
|
82
|
+
{formatFileSize(state.totalBytes || 0)}
|
|
83
|
+
</Text>
|
|
84
|
+
</View>
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
case "error":
|
|
88
|
+
return (
|
|
89
|
+
<View style={styles.container}>
|
|
90
|
+
<View style={styles.headerRow}>
|
|
91
|
+
<Text style={[styles.label, { color: getStatusColor() }]}>
|
|
92
|
+
{label || "Upload failed"}
|
|
93
|
+
</Text>
|
|
94
|
+
<Text style={[styles.percentage, { color: getStatusColor() }]}>
|
|
95
|
+
✕
|
|
96
|
+
</Text>
|
|
97
|
+
</View>
|
|
98
|
+
{state.error && (
|
|
99
|
+
<Text style={[styles.detail, { color: getStatusColor() }]}>
|
|
100
|
+
{state.error.message}
|
|
101
|
+
</Text>
|
|
102
|
+
)}
|
|
103
|
+
</View>
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
case "aborted":
|
|
107
|
+
return (
|
|
108
|
+
<View style={styles.container}>
|
|
109
|
+
<Text style={[styles.label, { color: getStatusColor() }]}>
|
|
110
|
+
{label || "Upload cancelled"}
|
|
111
|
+
</Text>
|
|
112
|
+
</View>
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
default:
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<View
|
|
122
|
+
style={[
|
|
123
|
+
styles.wrapper,
|
|
124
|
+
{
|
|
125
|
+
borderLeftColor: getStatusColor(),
|
|
126
|
+
},
|
|
127
|
+
]}
|
|
128
|
+
>
|
|
129
|
+
{state.status === "uploading" && (
|
|
130
|
+
<ActivityIndicator
|
|
131
|
+
size="small"
|
|
132
|
+
color={getStatusColor()}
|
|
133
|
+
style={styles.spinner}
|
|
134
|
+
/>
|
|
135
|
+
)}
|
|
136
|
+
{renderContent()}
|
|
137
|
+
</View>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const styles = StyleSheet.create({
|
|
142
|
+
wrapper: {
|
|
143
|
+
flexDirection: "row",
|
|
144
|
+
alignItems: "flex-start",
|
|
145
|
+
paddingVertical: 8,
|
|
146
|
+
paddingHorizontal: 12,
|
|
147
|
+
borderLeftWidth: 4,
|
|
148
|
+
backgroundColor: "#f5f5f5",
|
|
149
|
+
borderRadius: 4,
|
|
150
|
+
gap: 8,
|
|
151
|
+
},
|
|
152
|
+
spinner: {
|
|
153
|
+
marginTop: 4,
|
|
154
|
+
},
|
|
155
|
+
container: {
|
|
156
|
+
flex: 1,
|
|
157
|
+
gap: 4,
|
|
158
|
+
},
|
|
159
|
+
headerRow: {
|
|
160
|
+
flexDirection: "row",
|
|
161
|
+
justifyContent: "space-between",
|
|
162
|
+
alignItems: "center",
|
|
163
|
+
},
|
|
164
|
+
label: {
|
|
165
|
+
fontSize: 14,
|
|
166
|
+
fontWeight: "600",
|
|
167
|
+
color: "#333333",
|
|
168
|
+
flex: 1,
|
|
169
|
+
},
|
|
170
|
+
percentage: {
|
|
171
|
+
fontSize: 14,
|
|
172
|
+
fontWeight: "600",
|
|
173
|
+
color: "#007AFF",
|
|
174
|
+
minWidth: 36,
|
|
175
|
+
textAlign: "right",
|
|
176
|
+
},
|
|
177
|
+
progressBarContainer: {
|
|
178
|
+
height: 4,
|
|
179
|
+
backgroundColor: "#e0e0e0",
|
|
180
|
+
borderRadius: 2,
|
|
181
|
+
overflow: "hidden",
|
|
182
|
+
},
|
|
183
|
+
progressBar: {
|
|
184
|
+
height: "100%",
|
|
185
|
+
borderRadius: 2,
|
|
186
|
+
},
|
|
187
|
+
detailsRow: {
|
|
188
|
+
flexDirection: "row",
|
|
189
|
+
justifyContent: "space-between",
|
|
190
|
+
alignItems: "center",
|
|
191
|
+
},
|
|
192
|
+
detail: {
|
|
193
|
+
fontSize: 12,
|
|
194
|
+
color: "#666666",
|
|
195
|
+
},
|
|
196
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Native UI Components for Uploadista
|
|
3
|
+
* Provides unstyled, customizable components for upload workflows
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
CameraUploadButton,
|
|
8
|
+
type CameraUploadButtonProps,
|
|
9
|
+
} from "./CameraUploadButton";
|
|
10
|
+
export {
|
|
11
|
+
FileUploadButton,
|
|
12
|
+
type FileUploadButtonProps,
|
|
13
|
+
} from "./FileUploadButton";
|
|
14
|
+
export {
|
|
15
|
+
GalleryUploadButton,
|
|
16
|
+
type GalleryUploadButtonProps,
|
|
17
|
+
} from "./GalleryUploadButton";
|
|
18
|
+
export { UploadList, type UploadListProps } from "./UploadList";
|
|
19
|
+
export { UploadProgress, type UploadProgressProps } from "./UploadProgress";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Core context and client
|
|
2
|
+
export {
|
|
3
|
+
UploadistaContext,
|
|
4
|
+
type UploadistaContextType,
|
|
5
|
+
} from "./uploadista-context";
|
|
6
|
+
export { useCameraUpload } from "./use-camera-upload";
|
|
7
|
+
export { useFileUpload } from "./use-file-upload";
|
|
8
|
+
export { useFlowUpload } from "./use-flow-upload";
|
|
9
|
+
export { useGalleryUpload } from "./use-gallery-upload";
|
|
10
|
+
// Multi-upload hooks
|
|
11
|
+
export type {
|
|
12
|
+
MultiUploadState,
|
|
13
|
+
UploadItemState,
|
|
14
|
+
} from "./use-multi-upload";
|
|
15
|
+
export { useMultiUpload } from "./use-multi-upload";
|
|
16
|
+
// Upload hooks
|
|
17
|
+
export type {
|
|
18
|
+
UploadState,
|
|
19
|
+
UploadStatus,
|
|
20
|
+
UseUploadOptions,
|
|
21
|
+
UseUploadReturn,
|
|
22
|
+
} from "./use-upload";
|
|
23
|
+
export { useUpload } from "./use-upload";
|
|
24
|
+
export { useUploadMetrics } from "./use-upload-metrics";
|
|
25
|
+
export type {
|
|
26
|
+
UseUploadistaClientOptions,
|
|
27
|
+
UseUploadistaClientReturn,
|
|
28
|
+
} from "./use-uploadista-client";
|
|
29
|
+
export { useUploadistaContext } from "./use-uploadista-context";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { UploadistaEvent } from "@uploadista/client-core";
|
|
2
|
+
import { createContext } from "react";
|
|
3
|
+
import type { FileSystemProvider } from "../types";
|
|
4
|
+
import type { UseUploadistaClientReturn } from "./use-uploadista-client";
|
|
5
|
+
|
|
6
|
+
export interface UploadistaContextType extends UseUploadistaClientReturn {
|
|
7
|
+
fileSystemProvider: FileSystemProvider;
|
|
8
|
+
/**
|
|
9
|
+
* Subscribe to events (used internally by hooks)
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
subscribeToEvents: (handler: (event: UploadistaEvent) => void) => () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const UploadistaContext = createContext<
|
|
16
|
+
UploadistaContextType | undefined
|
|
17
|
+
>(undefined);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import type { UseCameraUploadOptions } from "../types";
|
|
3
|
+
import { useUpload } from "./use-upload";
|
|
4
|
+
import { useUploadistaContext } from "./use-uploadista-context";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hook for capturing photos and uploading them
|
|
8
|
+
* Handles camera permissions and capture flow
|
|
9
|
+
* @param options - Camera upload configuration
|
|
10
|
+
* @returns Upload state and camera capture/upload function
|
|
11
|
+
*/
|
|
12
|
+
export function useCameraUpload(options?: UseCameraUploadOptions) {
|
|
13
|
+
const { fileSystemProvider } = useUploadistaContext();
|
|
14
|
+
const uploadHook = useUpload({
|
|
15
|
+
metadata: options?.metadata,
|
|
16
|
+
onSuccess: options?.onSuccess,
|
|
17
|
+
onError: options?.onError,
|
|
18
|
+
onProgress: options?.onProgress,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Capture and upload photo
|
|
22
|
+
const captureAndUpload = useCallback(async () => {
|
|
23
|
+
try {
|
|
24
|
+
// Capture photo with camera
|
|
25
|
+
const photo = await fileSystemProvider.pickCamera(options?.cameraOptions);
|
|
26
|
+
|
|
27
|
+
// Upload captured photo
|
|
28
|
+
await uploadHook.upload(photo);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error("Camera capture error:", error);
|
|
31
|
+
}
|
|
32
|
+
}, [fileSystemProvider, options?.cameraOptions, uploadHook]);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
...uploadHook,
|
|
36
|
+
captureAndUpload,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import type { UseFileUploadOptions } from "../types";
|
|
3
|
+
import { useUpload } from "./use-upload";
|
|
4
|
+
import { useUploadistaContext } from "./use-uploadista-context";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hook for selecting and uploading generic files (documents, etc.)
|
|
8
|
+
* @param options - File upload configuration
|
|
9
|
+
* @returns Upload state and file picker/upload function
|
|
10
|
+
*/
|
|
11
|
+
export function useFileUpload(options?: UseFileUploadOptions) {
|
|
12
|
+
const { fileSystemProvider } = useUploadistaContext();
|
|
13
|
+
const uploadHook = useUpload({
|
|
14
|
+
metadata: options?.metadata,
|
|
15
|
+
onSuccess: options?.onSuccess,
|
|
16
|
+
onError: options?.onError,
|
|
17
|
+
onProgress: options?.onProgress,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Pick and upload file
|
|
21
|
+
const pickAndUpload = useCallback(async () => {
|
|
22
|
+
try {
|
|
23
|
+
// Pick file
|
|
24
|
+
const file = await fileSystemProvider.pickDocument({
|
|
25
|
+
allowedTypes: options?.allowedTypes,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Upload file
|
|
29
|
+
await uploadHook.upload(file);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error("File selection error:", error);
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}, [fileSystemProvider, options?.allowedTypes, uploadHook]);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
...uploadHook,
|
|
38
|
+
pickAndUpload,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import type { UploadFile } from "@uploadista/core/types";
|
|
2
|
+
import { useCallback, useRef, useState } from "react";
|
|
3
|
+
import type { FilePickResult, UseFlowUploadOptions } from "../types";
|
|
4
|
+
import { useUploadistaContext } from "./use-uploadista-context";
|
|
5
|
+
|
|
6
|
+
export type FlowUploadStatus =
|
|
7
|
+
| "idle"
|
|
8
|
+
| "uploading"
|
|
9
|
+
| "processing"
|
|
10
|
+
| "success"
|
|
11
|
+
| "error"
|
|
12
|
+
| "aborted";
|
|
13
|
+
|
|
14
|
+
export interface FlowUploadState {
|
|
15
|
+
status: FlowUploadStatus;
|
|
16
|
+
progress: number;
|
|
17
|
+
bytesUploaded: number;
|
|
18
|
+
totalBytes: number | null;
|
|
19
|
+
jobId: string | null;
|
|
20
|
+
error: Error | null;
|
|
21
|
+
result: unknown | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const initialState: FlowUploadState = {
|
|
25
|
+
status: "idle",
|
|
26
|
+
progress: 0,
|
|
27
|
+
bytesUploaded: 0,
|
|
28
|
+
totalBytes: null,
|
|
29
|
+
jobId: null,
|
|
30
|
+
error: null,
|
|
31
|
+
result: null,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Hook for uploading files through a flow pipeline with full state management.
|
|
36
|
+
* Provides upload progress tracking, flow execution monitoring, error handling, and abort functionality.
|
|
37
|
+
*
|
|
38
|
+
* Must be used within an UploadistaProvider.
|
|
39
|
+
*
|
|
40
|
+
* @param options - Flow upload configuration
|
|
41
|
+
* @returns Flow upload state and control methods
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* function MyComponent() {
|
|
46
|
+
* const flowUpload = useFlowUpload({
|
|
47
|
+
* flowId: 'image-processing-flow',
|
|
48
|
+
* storageId: 'my-storage',
|
|
49
|
+
* onSuccess: (result) => console.log('Flow complete:', result),
|
|
50
|
+
* onError: (error) => console.error('Flow failed:', error),
|
|
51
|
+
* onProgress: (progress) => console.log('Progress:', progress + '%'),
|
|
52
|
+
* });
|
|
53
|
+
*
|
|
54
|
+
* const handlePickFile = async () => {
|
|
55
|
+
* const file = await fileSystemProvider.pickDocument();
|
|
56
|
+
* if (file) {
|
|
57
|
+
* await flowUpload.upload(file);
|
|
58
|
+
* }
|
|
59
|
+
* };
|
|
60
|
+
*
|
|
61
|
+
* return (
|
|
62
|
+
* <View>
|
|
63
|
+
* <Button title="Pick File" onPress={handlePickFile} />
|
|
64
|
+
* {flowUpload.isUploading && <Text>Progress: {flowUpload.state.progress}%</Text>}
|
|
65
|
+
* {flowUpload.state.jobId && <Text>Job ID: {flowUpload.state.jobId}</Text>}
|
|
66
|
+
* {flowUpload.state.error && <Text>Error: {flowUpload.state.error.message}</Text>}
|
|
67
|
+
* <Button title="Abort" onPress={flowUpload.abort} disabled={!flowUpload.isActive} />
|
|
68
|
+
* </View>
|
|
69
|
+
* );
|
|
70
|
+
* }
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export function useFlowUpload(options: UseFlowUploadOptions) {
|
|
74
|
+
const { client, fileSystemProvider } = useUploadistaContext();
|
|
75
|
+
const [state, setState] = useState<FlowUploadState>(initialState);
|
|
76
|
+
const abortControllerRef = useRef<{ abort: () => void } | null>(null);
|
|
77
|
+
const lastFileRef = useRef<FilePickResult | null>(null);
|
|
78
|
+
|
|
79
|
+
const updateState = useCallback((update: Partial<FlowUploadState>) => {
|
|
80
|
+
setState((prev) => ({ ...prev, ...update }));
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
const reset = useCallback(() => {
|
|
84
|
+
if (abortControllerRef.current) {
|
|
85
|
+
abortControllerRef.current.abort();
|
|
86
|
+
abortControllerRef.current = null;
|
|
87
|
+
}
|
|
88
|
+
setState(initialState);
|
|
89
|
+
lastFileRef.current = null;
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
const abort = useCallback(() => {
|
|
93
|
+
if (abortControllerRef.current) {
|
|
94
|
+
abortControllerRef.current.abort();
|
|
95
|
+
abortControllerRef.current = null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
updateState({
|
|
99
|
+
status: "aborted",
|
|
100
|
+
});
|
|
101
|
+
}, [updateState]);
|
|
102
|
+
|
|
103
|
+
const upload = useCallback(
|
|
104
|
+
async (file: FilePickResult) => {
|
|
105
|
+
// Reset any previous state
|
|
106
|
+
setState({
|
|
107
|
+
...initialState,
|
|
108
|
+
status: "uploading",
|
|
109
|
+
totalBytes: file.size,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
lastFileRef.current = file;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
// Read file content
|
|
116
|
+
const fileContent = await fileSystemProvider.readFile(file.uri);
|
|
117
|
+
|
|
118
|
+
// Create a Blob from the file content
|
|
119
|
+
// Convert ArrayBuffer to Uint8Array for better compatibility
|
|
120
|
+
const data =
|
|
121
|
+
fileContent instanceof ArrayBuffer
|
|
122
|
+
? new Uint8Array(fileContent)
|
|
123
|
+
: fileContent;
|
|
124
|
+
// Note: Using any cast here because React Native Blob accepts BufferSource
|
|
125
|
+
// but TypeScript's lib.dom.d.ts Blob type doesn't include it
|
|
126
|
+
// biome-ignore lint/suspicious/noExplicitAny: React Native Blob accepts BufferSource
|
|
127
|
+
const blob = new Blob([data as any], {
|
|
128
|
+
type: file.mimeType || "application/octet-stream",
|
|
129
|
+
// biome-ignore lint/suspicious/noExplicitAny: BlobPropertyBag type differs by platform
|
|
130
|
+
} as any);
|
|
131
|
+
|
|
132
|
+
// use the Blob (for React Native)
|
|
133
|
+
const uploadInput = blob;
|
|
134
|
+
|
|
135
|
+
// Start the flow upload using the client
|
|
136
|
+
const uploadPromise = client.uploadWithFlow(
|
|
137
|
+
uploadInput,
|
|
138
|
+
{
|
|
139
|
+
flowId: options.flowId,
|
|
140
|
+
storageId: options.storageId,
|
|
141
|
+
outputNodeId: options.outputNodeId,
|
|
142
|
+
metadata: options.metadata as Record<string, string> | undefined,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
onJobStart: () => {
|
|
146
|
+
updateState({
|
|
147
|
+
status: "processing",
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
onProgress: (
|
|
152
|
+
_uploadId: string,
|
|
153
|
+
bytesUploaded: number,
|
|
154
|
+
totalBytes: number | null,
|
|
155
|
+
) => {
|
|
156
|
+
const progress = totalBytes
|
|
157
|
+
? Math.round((bytesUploaded / totalBytes) * 100)
|
|
158
|
+
: 0;
|
|
159
|
+
|
|
160
|
+
updateState({
|
|
161
|
+
progress,
|
|
162
|
+
bytesUploaded,
|
|
163
|
+
totalBytes,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
options.onProgress?.(progress, bytesUploaded, totalBytes);
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
onChunkComplete: (
|
|
170
|
+
chunkSize: number,
|
|
171
|
+
bytesAccepted: number,
|
|
172
|
+
bytesTotal: number | null,
|
|
173
|
+
) => {
|
|
174
|
+
options.onChunkComplete?.(chunkSize, bytesAccepted, bytesTotal);
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
onSuccess: (result: UploadFile) => {
|
|
178
|
+
updateState({
|
|
179
|
+
status: "success",
|
|
180
|
+
result,
|
|
181
|
+
progress: 100,
|
|
182
|
+
bytesUploaded: result.size || 0,
|
|
183
|
+
totalBytes: result.size || null,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
options.onSuccess?.(result);
|
|
187
|
+
abortControllerRef.current = null;
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
onError: (error: Error) => {
|
|
191
|
+
updateState({
|
|
192
|
+
status: "error",
|
|
193
|
+
error,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
options.onError?.(error);
|
|
197
|
+
abortControllerRef.current = null;
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Handle the promise to get the abort controller
|
|
203
|
+
const controller = await uploadPromise;
|
|
204
|
+
abortControllerRef.current = controller;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
updateState({
|
|
207
|
+
status: "error",
|
|
208
|
+
error: error as Error,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
options.onError?.(error as Error);
|
|
212
|
+
abortControllerRef.current = null;
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
[client, fileSystemProvider, options, updateState],
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const retry = useCallback(() => {
|
|
219
|
+
if (
|
|
220
|
+
lastFileRef.current &&
|
|
221
|
+
(state.status === "error" || state.status === "aborted")
|
|
222
|
+
) {
|
|
223
|
+
upload(lastFileRef.current);
|
|
224
|
+
}
|
|
225
|
+
}, [upload, state.status]);
|
|
226
|
+
|
|
227
|
+
const isActive =
|
|
228
|
+
state.status === "uploading" || state.status === "processing";
|
|
229
|
+
const canRetry =
|
|
230
|
+
(state.status === "error" || state.status === "aborted") &&
|
|
231
|
+
lastFileRef.current !== null;
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
state,
|
|
235
|
+
upload,
|
|
236
|
+
abort,
|
|
237
|
+
reset,
|
|
238
|
+
retry,
|
|
239
|
+
isActive,
|
|
240
|
+
canRetry,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import type { FilePickResult, UseGalleryUploadOptions } from "../types";
|
|
3
|
+
import { useMultiUpload } from "./use-multi-upload";
|
|
4
|
+
import { useUploadistaContext } from "./use-uploadista-context";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hook for selecting and uploading photos/videos from gallery
|
|
8
|
+
* Handles batch selection and concurrent uploads
|
|
9
|
+
* @param options - Gallery upload configuration
|
|
10
|
+
* @returns Upload state and gallery selection/upload function
|
|
11
|
+
*/
|
|
12
|
+
export function useGalleryUpload(options?: UseGalleryUploadOptions) {
|
|
13
|
+
const { fileSystemProvider } = useUploadistaContext();
|
|
14
|
+
const uploadHook = useMultiUpload({
|
|
15
|
+
maxConcurrent: 3,
|
|
16
|
+
metadata: options?.metadata,
|
|
17
|
+
onSuccess: options?.onSuccess,
|
|
18
|
+
onError: options?.onError,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Select and upload media from gallery
|
|
22
|
+
const selectAndUpload = useCallback(async () => {
|
|
23
|
+
try {
|
|
24
|
+
let media: FilePickResult | FilePickResult[];
|
|
25
|
+
|
|
26
|
+
// Select appropriate media type
|
|
27
|
+
if (options?.mediaType === "video") {
|
|
28
|
+
media = await fileSystemProvider.pickVideo({
|
|
29
|
+
allowMultiple: options?.allowMultiple ?? true,
|
|
30
|
+
});
|
|
31
|
+
} else if (options?.mediaType === "photo") {
|
|
32
|
+
media = await fileSystemProvider.pickImage({
|
|
33
|
+
allowMultiple: options?.allowMultiple ?? true,
|
|
34
|
+
});
|
|
35
|
+
} else {
|
|
36
|
+
// For 'mixed' or default, use pickImage first (can be extended to support both)
|
|
37
|
+
media = await fileSystemProvider.pickImage({
|
|
38
|
+
allowMultiple: options?.allowMultiple ?? true,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Handle single or multiple files
|
|
43
|
+
const files = Array.isArray(media) ? media : [media];
|
|
44
|
+
|
|
45
|
+
// Add files and start upload
|
|
46
|
+
const itemIds = uploadHook.addFiles(files);
|
|
47
|
+
await uploadHook.startUploads();
|
|
48
|
+
|
|
49
|
+
return itemIds;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error("Gallery selection error:", error);
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}, [
|
|
55
|
+
fileSystemProvider,
|
|
56
|
+
options?.allowMultiple,
|
|
57
|
+
options?.mediaType,
|
|
58
|
+
uploadHook,
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
...uploadHook,
|
|
63
|
+
selectAndUpload,
|
|
64
|
+
};
|
|
65
|
+
}
|