@idealyst/files 1.2.96
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 +94 -0
- package/src/components/DropZone.native.tsx +96 -0
- package/src/components/DropZone.styles.tsx +99 -0
- package/src/components/DropZone.web.tsx +178 -0
- package/src/components/FilePickerButton.native.tsx +82 -0
- package/src/components/FilePickerButton.styles.tsx +112 -0
- package/src/components/FilePickerButton.web.tsx +84 -0
- package/src/components/UploadProgress.native.tsx +203 -0
- package/src/components/UploadProgress.styles.tsx +90 -0
- package/src/components/UploadProgress.web.tsx +201 -0
- package/src/components/index.native.ts +8 -0
- package/src/components/index.ts +6 -0
- package/src/components/index.web.ts +8 -0
- package/src/constants.ts +336 -0
- package/src/examples/index.ts +181 -0
- package/src/hooks/createUseFilePickerHook.ts +169 -0
- package/src/hooks/createUseFileUploadHook.ts +173 -0
- package/src/hooks/index.native.ts +12 -0
- package/src/hooks/index.ts +12 -0
- package/src/hooks/index.web.ts +12 -0
- package/src/index.native.ts +142 -0
- package/src/index.ts +139 -0
- package/src/index.web.ts +142 -0
- package/src/permissions/index.native.ts +8 -0
- package/src/permissions/index.ts +8 -0
- package/src/permissions/index.web.ts +8 -0
- package/src/permissions/permissions.native.ts +177 -0
- package/src/permissions/permissions.web.ts +96 -0
- package/src/picker/FilePicker.native.ts +407 -0
- package/src/picker/FilePicker.web.ts +366 -0
- package/src/picker/index.native.ts +2 -0
- package/src/picker/index.ts +2 -0
- package/src/picker/index.web.ts +2 -0
- package/src/types.ts +990 -0
- package/src/uploader/ChunkedUploader.ts +312 -0
- package/src/uploader/FileUploader.native.ts +435 -0
- package/src/uploader/FileUploader.web.ts +350 -0
- package/src/uploader/UploadQueue.ts +519 -0
- package/src/uploader/index.native.ts +4 -0
- package/src/uploader/index.ts +4 -0
- package/src/uploader/index.web.ts +4 -0
- package/src/utils.ts +586 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React, { forwardRef, useCallback, useState } from 'react';
|
|
2
|
+
import { View, Text, Pressable, ActivityIndicator } from 'react-native';
|
|
3
|
+
import type { FilePickerButtonProps, FilePickerResult, FilePickerError } from '../types';
|
|
4
|
+
import { filePickerButtonStyles } from './FilePickerButton.styles';
|
|
5
|
+
import { useFilePicker } from '../hooks';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* FilePickerButton - A styled button that opens the file picker.
|
|
9
|
+
*/
|
|
10
|
+
export const FilePickerButton = forwardRef<View, FilePickerButtonProps>((props, ref) => {
|
|
11
|
+
const {
|
|
12
|
+
children = 'Select File',
|
|
13
|
+
pickerConfig,
|
|
14
|
+
onPick,
|
|
15
|
+
onError,
|
|
16
|
+
disabled = false,
|
|
17
|
+
loading = false,
|
|
18
|
+
variant = 'solid',
|
|
19
|
+
size = 'md',
|
|
20
|
+
intent = 'primary',
|
|
21
|
+
leftIcon = 'file-upload',
|
|
22
|
+
style,
|
|
23
|
+
testID,
|
|
24
|
+
} = props;
|
|
25
|
+
|
|
26
|
+
const { pick, isPicking } = useFilePicker({ config: pickerConfig });
|
|
27
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
28
|
+
|
|
29
|
+
const isDisabled = disabled || loading || isPicking;
|
|
30
|
+
const isLoading = loading || isPicking;
|
|
31
|
+
|
|
32
|
+
const handlePress = useCallback(async () => {
|
|
33
|
+
if (isDisabled) return;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const result: FilePickerResult = await pick();
|
|
37
|
+
if (!result.cancelled) {
|
|
38
|
+
onPick?.(result);
|
|
39
|
+
}
|
|
40
|
+
} catch (err) {
|
|
41
|
+
onError?.(err as FilePickerError);
|
|
42
|
+
}
|
|
43
|
+
}, [isDisabled, pick, onPick, onError]);
|
|
44
|
+
|
|
45
|
+
// Apply variants
|
|
46
|
+
filePickerButtonStyles.useVariants({
|
|
47
|
+
size,
|
|
48
|
+
intent,
|
|
49
|
+
variant,
|
|
50
|
+
disabled: isDisabled,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Pressable
|
|
55
|
+
ref={ref}
|
|
56
|
+
onPress={handlePress}
|
|
57
|
+
disabled={isDisabled}
|
|
58
|
+
onHoverIn={() => setIsHovered(true)}
|
|
59
|
+
onHoverOut={() => setIsHovered(false)}
|
|
60
|
+
style={[filePickerButtonStyles.button({ intent, variant }), style]}
|
|
61
|
+
testID={testID}
|
|
62
|
+
accessibilityRole="button"
|
|
63
|
+
accessibilityState={{ disabled: isDisabled }}
|
|
64
|
+
accessibilityLabel={typeof children === 'string' ? children : 'Select file'}
|
|
65
|
+
>
|
|
66
|
+
{isLoading ? (
|
|
67
|
+
<ActivityIndicator
|
|
68
|
+
size="small"
|
|
69
|
+
color={filePickerButtonStyles.spinner({ intent, variant }).color as string}
|
|
70
|
+
/>
|
|
71
|
+
) : leftIcon ? (
|
|
72
|
+
<Text style={filePickerButtonStyles.icon({ intent, variant })}>
|
|
73
|
+
{/* Icon placeholder - in a real implementation, use MDI icon */}
|
|
74
|
+
📁
|
|
75
|
+
</Text>
|
|
76
|
+
) : null}
|
|
77
|
+
<Text style={filePickerButtonStyles.text({ intent, variant })}>
|
|
78
|
+
{children}
|
|
79
|
+
</Text>
|
|
80
|
+
</Pressable>
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
FilePickerButton.displayName = 'FilePickerButton';
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, TouchableOpacity } from 'react-native';
|
|
3
|
+
import type { UploadProgressProps, Intent } from '../types';
|
|
4
|
+
import { uploadProgressStyles } from './UploadProgress.styles';
|
|
5
|
+
import { formatBytes, formatDuration } from '../utils';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* UploadProgress - Shows upload progress with controls (Native).
|
|
9
|
+
*/
|
|
10
|
+
export const UploadProgress: React.FC<UploadProgressProps> = (props) => {
|
|
11
|
+
const {
|
|
12
|
+
upload,
|
|
13
|
+
showFileName = true,
|
|
14
|
+
showFileSize = true,
|
|
15
|
+
showSpeed = false,
|
|
16
|
+
showETA = false,
|
|
17
|
+
showCancel = true,
|
|
18
|
+
showRetry = true,
|
|
19
|
+
onCancel,
|
|
20
|
+
onRetry,
|
|
21
|
+
variant = 'linear',
|
|
22
|
+
size = 'md',
|
|
23
|
+
style,
|
|
24
|
+
testID,
|
|
25
|
+
} = props;
|
|
26
|
+
|
|
27
|
+
// Determine intent based on state
|
|
28
|
+
const getIntent = (): Intent => {
|
|
29
|
+
switch (upload.state) {
|
|
30
|
+
case 'completed':
|
|
31
|
+
return 'success';
|
|
32
|
+
case 'failed':
|
|
33
|
+
return 'error';
|
|
34
|
+
case 'cancelled':
|
|
35
|
+
return 'secondary';
|
|
36
|
+
default:
|
|
37
|
+
return 'primary';
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const intent = getIntent();
|
|
42
|
+
|
|
43
|
+
// Apply variants
|
|
44
|
+
uploadProgressStyles.useVariants({
|
|
45
|
+
size,
|
|
46
|
+
intent,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Render state icon
|
|
50
|
+
const renderStateIcon = () => {
|
|
51
|
+
switch (upload.state) {
|
|
52
|
+
case 'completed':
|
|
53
|
+
return <Text style={uploadProgressStyles.stateIcon({ intent: 'success' })}>✓</Text>;
|
|
54
|
+
case 'failed':
|
|
55
|
+
return <Text style={uploadProgressStyles.stateIcon({ intent: 'error' })}>✗</Text>;
|
|
56
|
+
case 'cancelled':
|
|
57
|
+
return <Text style={uploadProgressStyles.stateIcon({ intent: 'secondary' })}>⊘</Text>;
|
|
58
|
+
case 'paused':
|
|
59
|
+
return <Text style={uploadProgressStyles.stateIcon({ intent: 'secondary' })}>⏸</Text>;
|
|
60
|
+
case 'pending':
|
|
61
|
+
return <Text style={uploadProgressStyles.stateIcon({ intent: 'secondary' })}>⏳</Text>;
|
|
62
|
+
default:
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Render progress bar
|
|
68
|
+
const renderProgressBar = () => {
|
|
69
|
+
if (upload.state !== 'uploading' && upload.state !== 'paused') {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<View style={uploadProgressStyles.progressContainer({})}>
|
|
75
|
+
<View
|
|
76
|
+
style={[
|
|
77
|
+
uploadProgressStyles.progressBar({ intent }),
|
|
78
|
+
{ width: `${upload.percentage}%` },
|
|
79
|
+
]}
|
|
80
|
+
/>
|
|
81
|
+
</View>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Render details
|
|
86
|
+
const renderDetails = () => {
|
|
87
|
+
const details: string[] = [];
|
|
88
|
+
|
|
89
|
+
if (showFileSize) {
|
|
90
|
+
details.push(`${formatBytes(upload.bytesUploaded)} / ${formatBytes(upload.bytesTotal)}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (showSpeed && upload.state === 'uploading' && upload.speed > 0) {
|
|
94
|
+
details.push(`${formatBytes(upload.speed)}/s`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (showETA && upload.state === 'uploading' && upload.estimatedTimeRemaining > 0) {
|
|
98
|
+
details.push(`${formatDuration(upload.estimatedTimeRemaining)} remaining`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (upload.currentChunk !== undefined && upload.totalChunks !== undefined) {
|
|
102
|
+
details.push(`Chunk ${upload.currentChunk}/${upload.totalChunks}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (details.length === 0) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<View style={uploadProgressStyles.detailsRow({})}>
|
|
111
|
+
{details.map((detail, index) => (
|
|
112
|
+
<Text key={index} style={uploadProgressStyles.detail({})}>
|
|
113
|
+
{detail}
|
|
114
|
+
</Text>
|
|
115
|
+
))}
|
|
116
|
+
</View>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Render actions
|
|
121
|
+
const renderActions = () => {
|
|
122
|
+
const actions: React.ReactNode[] = [];
|
|
123
|
+
|
|
124
|
+
if (showCancel && (upload.state === 'uploading' || upload.state === 'pending')) {
|
|
125
|
+
actions.push(
|
|
126
|
+
<TouchableOpacity
|
|
127
|
+
key="cancel"
|
|
128
|
+
onPress={onCancel}
|
|
129
|
+
style={uploadProgressStyles.actionButton({})}
|
|
130
|
+
accessibilityRole="button"
|
|
131
|
+
accessibilityLabel="Cancel upload"
|
|
132
|
+
activeOpacity={0.7}
|
|
133
|
+
>
|
|
134
|
+
<Text style={uploadProgressStyles.actionIcon({})}>✗</Text>
|
|
135
|
+
</TouchableOpacity>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (showRetry && upload.state === 'failed') {
|
|
140
|
+
actions.push(
|
|
141
|
+
<TouchableOpacity
|
|
142
|
+
key="retry"
|
|
143
|
+
onPress={onRetry}
|
|
144
|
+
style={uploadProgressStyles.actionButton({})}
|
|
145
|
+
accessibilityRole="button"
|
|
146
|
+
accessibilityLabel="Retry upload"
|
|
147
|
+
activeOpacity={0.7}
|
|
148
|
+
>
|
|
149
|
+
<Text style={uploadProgressStyles.actionIcon({})}>↻</Text>
|
|
150
|
+
</TouchableOpacity>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (actions.length === 0) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<View style={uploadProgressStyles.actions({})}>
|
|
160
|
+
{actions}
|
|
161
|
+
</View>
|
|
162
|
+
);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Render error message
|
|
166
|
+
const renderError = () => {
|
|
167
|
+
if (upload.state !== 'failed' || !upload.error) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<Text style={uploadProgressStyles.errorText({})}>
|
|
173
|
+
{upload.error.message}
|
|
174
|
+
</Text>
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<View style={[uploadProgressStyles.container({}), style]} testID={testID}>
|
|
180
|
+
{/* File info row */}
|
|
181
|
+
<View style={uploadProgressStyles.infoRow({})}>
|
|
182
|
+
{showFileName && (
|
|
183
|
+
<Text style={uploadProgressStyles.fileName({})} numberOfLines={1}>
|
|
184
|
+
{upload.file.name}
|
|
185
|
+
</Text>
|
|
186
|
+
)}
|
|
187
|
+
{renderStateIcon()}
|
|
188
|
+
{renderActions()}
|
|
189
|
+
</View>
|
|
190
|
+
|
|
191
|
+
{/* Progress bar */}
|
|
192
|
+
{renderProgressBar()}
|
|
193
|
+
|
|
194
|
+
{/* Details row */}
|
|
195
|
+
{renderDetails()}
|
|
196
|
+
|
|
197
|
+
{/* Error message */}
|
|
198
|
+
{renderError()}
|
|
199
|
+
</View>
|
|
200
|
+
);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
UploadProgress.displayName = 'UploadProgress';
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UploadProgress styles using defineStyle.
|
|
3
|
+
*/
|
|
4
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
5
|
+
import { defineStyle, ThemeStyleWrapper } from '@idealyst/theme';
|
|
6
|
+
import type { Theme as BaseTheme, Intent, Size } from '@idealyst/theme';
|
|
7
|
+
|
|
8
|
+
// Required: Unistyles must see StyleSheet usage to process this file
|
|
9
|
+
void StyleSheet;
|
|
10
|
+
|
|
11
|
+
// Wrap theme for $iterator support
|
|
12
|
+
type Theme = ThemeStyleWrapper<BaseTheme>;
|
|
13
|
+
|
|
14
|
+
export type UploadProgressVariants = {
|
|
15
|
+
size: Size;
|
|
16
|
+
intent: Intent;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type UploadProgressDynamicProps = {
|
|
20
|
+
intent?: Intent;
|
|
21
|
+
size?: Size;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const uploadProgressStyles = defineStyle('UploadProgress', (theme: Theme) => ({
|
|
25
|
+
container: (_props: UploadProgressDynamicProps) => ({
|
|
26
|
+
backgroundColor: theme.colors.surface.secondary,
|
|
27
|
+
borderRadius: 8,
|
|
28
|
+
padding: 12,
|
|
29
|
+
gap: 8,
|
|
30
|
+
}),
|
|
31
|
+
infoRow: (_props: UploadProgressDynamicProps) => ({
|
|
32
|
+
flexDirection: 'row',
|
|
33
|
+
alignItems: 'center',
|
|
34
|
+
justifyContent: 'space-between',
|
|
35
|
+
gap: 8,
|
|
36
|
+
}),
|
|
37
|
+
fileName: (_props: UploadProgressDynamicProps) => ({
|
|
38
|
+
flex: 1,
|
|
39
|
+
fontSize: 14,
|
|
40
|
+
fontWeight: '500',
|
|
41
|
+
color: theme.colors.text.primary,
|
|
42
|
+
}),
|
|
43
|
+
stateIcon: ({ intent = 'primary' }: UploadProgressDynamicProps) => ({
|
|
44
|
+
fontSize: 20,
|
|
45
|
+
color: theme.intents[intent].primary,
|
|
46
|
+
}),
|
|
47
|
+
progressContainer: (_props: UploadProgressDynamicProps) => ({
|
|
48
|
+
height: 8,
|
|
49
|
+
backgroundColor: theme.colors.surface.tertiary,
|
|
50
|
+
borderRadius: 4,
|
|
51
|
+
overflow: 'hidden',
|
|
52
|
+
}),
|
|
53
|
+
progressBar: ({ intent = 'primary' }: UploadProgressDynamicProps) => ({
|
|
54
|
+
height: '100%',
|
|
55
|
+
backgroundColor: theme.intents[intent].primary,
|
|
56
|
+
borderRadius: 4,
|
|
57
|
+
}),
|
|
58
|
+
detailsRow: (_props: UploadProgressDynamicProps) => ({
|
|
59
|
+
flexDirection: 'row',
|
|
60
|
+
alignItems: 'center',
|
|
61
|
+
justifyContent: 'space-between',
|
|
62
|
+
flexWrap: 'wrap',
|
|
63
|
+
gap: 8,
|
|
64
|
+
}),
|
|
65
|
+
detail: (_props: UploadProgressDynamicProps) => ({
|
|
66
|
+
fontSize: 12,
|
|
67
|
+
color: theme.colors.text.secondary,
|
|
68
|
+
}),
|
|
69
|
+
actions: (_props: UploadProgressDynamicProps) => ({
|
|
70
|
+
flexDirection: 'row',
|
|
71
|
+
alignItems: 'center',
|
|
72
|
+
gap: 8,
|
|
73
|
+
}),
|
|
74
|
+
actionButton: (_props: UploadProgressDynamicProps) => ({
|
|
75
|
+
padding: 4,
|
|
76
|
+
borderRadius: 4,
|
|
77
|
+
_web: {
|
|
78
|
+
cursor: 'pointer',
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
actionIcon: ({ intent = 'neutral' }: UploadProgressDynamicProps) => ({
|
|
82
|
+
fontSize: 20,
|
|
83
|
+
color: theme.intents[intent].primary,
|
|
84
|
+
}),
|
|
85
|
+
errorText: (_props: UploadProgressDynamicProps) => ({
|
|
86
|
+
fontSize: 12,
|
|
87
|
+
color: theme.intents.danger.primary,
|
|
88
|
+
marginTop: 4,
|
|
89
|
+
}),
|
|
90
|
+
}));
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, Pressable } from 'react-native';
|
|
3
|
+
import type { UploadProgressProps, Intent } from '../types';
|
|
4
|
+
import { uploadProgressStyles } from './UploadProgress.styles';
|
|
5
|
+
import { formatBytes, formatDuration } from '../utils';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* UploadProgress - Shows upload progress with controls (Web).
|
|
9
|
+
*/
|
|
10
|
+
export const UploadProgress: React.FC<UploadProgressProps> = (props) => {
|
|
11
|
+
const {
|
|
12
|
+
upload,
|
|
13
|
+
showFileName = true,
|
|
14
|
+
showFileSize = true,
|
|
15
|
+
showSpeed = false,
|
|
16
|
+
showETA = false,
|
|
17
|
+
showCancel = true,
|
|
18
|
+
showRetry = true,
|
|
19
|
+
onCancel,
|
|
20
|
+
onRetry,
|
|
21
|
+
variant = 'linear',
|
|
22
|
+
size = 'md',
|
|
23
|
+
style,
|
|
24
|
+
testID,
|
|
25
|
+
} = props;
|
|
26
|
+
|
|
27
|
+
// Determine intent based on state
|
|
28
|
+
const getIntent = (): Intent => {
|
|
29
|
+
switch (upload.state) {
|
|
30
|
+
case 'completed':
|
|
31
|
+
return 'success';
|
|
32
|
+
case 'failed':
|
|
33
|
+
return 'error';
|
|
34
|
+
case 'cancelled':
|
|
35
|
+
return 'secondary';
|
|
36
|
+
default:
|
|
37
|
+
return 'primary';
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const intent = getIntent();
|
|
42
|
+
|
|
43
|
+
// Apply variants
|
|
44
|
+
uploadProgressStyles.useVariants({
|
|
45
|
+
size,
|
|
46
|
+
intent,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Render state icon
|
|
50
|
+
const renderStateIcon = () => {
|
|
51
|
+
switch (upload.state) {
|
|
52
|
+
case 'completed':
|
|
53
|
+
return <Text style={uploadProgressStyles.stateIcon({ intent: 'success' })}>✓</Text>;
|
|
54
|
+
case 'failed':
|
|
55
|
+
return <Text style={uploadProgressStyles.stateIcon({ intent: 'error' })}>✗</Text>;
|
|
56
|
+
case 'cancelled':
|
|
57
|
+
return <Text style={uploadProgressStyles.stateIcon({ intent: 'secondary' })}>⊘</Text>;
|
|
58
|
+
case 'paused':
|
|
59
|
+
return <Text style={uploadProgressStyles.stateIcon({ intent: 'secondary' })}>⏸</Text>;
|
|
60
|
+
case 'pending':
|
|
61
|
+
return <Text style={uploadProgressStyles.stateIcon({ intent: 'secondary' })}>⏳</Text>;
|
|
62
|
+
default:
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Render progress bar
|
|
68
|
+
const renderProgressBar = () => {
|
|
69
|
+
if (upload.state !== 'uploading' && upload.state !== 'paused') {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<View style={uploadProgressStyles.progressContainer({})}>
|
|
75
|
+
<View
|
|
76
|
+
style={[
|
|
77
|
+
uploadProgressStyles.progressBar({ intent }),
|
|
78
|
+
{ width: `${upload.percentage}%` },
|
|
79
|
+
]}
|
|
80
|
+
/>
|
|
81
|
+
</View>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Render details
|
|
86
|
+
const renderDetails = () => {
|
|
87
|
+
const details: string[] = [];
|
|
88
|
+
|
|
89
|
+
if (showFileSize) {
|
|
90
|
+
details.push(`${formatBytes(upload.bytesUploaded)} / ${formatBytes(upload.bytesTotal)}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (showSpeed && upload.state === 'uploading' && upload.speed > 0) {
|
|
94
|
+
details.push(`${formatBytes(upload.speed)}/s`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (showETA && upload.state === 'uploading' && upload.estimatedTimeRemaining > 0) {
|
|
98
|
+
details.push(`${formatDuration(upload.estimatedTimeRemaining)} remaining`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (upload.currentChunk !== undefined && upload.totalChunks !== undefined) {
|
|
102
|
+
details.push(`Chunk ${upload.currentChunk}/${upload.totalChunks}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (details.length === 0) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<View style={uploadProgressStyles.detailsRow({})}>
|
|
111
|
+
{details.map((detail, index) => (
|
|
112
|
+
<Text key={index} style={uploadProgressStyles.detail({})}>
|
|
113
|
+
{detail}
|
|
114
|
+
</Text>
|
|
115
|
+
))}
|
|
116
|
+
</View>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Render actions
|
|
121
|
+
const renderActions = () => {
|
|
122
|
+
const actions: React.ReactNode[] = [];
|
|
123
|
+
|
|
124
|
+
if (showCancel && (upload.state === 'uploading' || upload.state === 'pending')) {
|
|
125
|
+
actions.push(
|
|
126
|
+
<Pressable
|
|
127
|
+
key="cancel"
|
|
128
|
+
onPress={onCancel}
|
|
129
|
+
style={uploadProgressStyles.actionButton({})}
|
|
130
|
+
accessibilityRole="button"
|
|
131
|
+
accessibilityLabel="Cancel upload"
|
|
132
|
+
>
|
|
133
|
+
<Text style={uploadProgressStyles.actionIcon({})}>✗</Text>
|
|
134
|
+
</Pressable>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (showRetry && upload.state === 'failed') {
|
|
139
|
+
actions.push(
|
|
140
|
+
<Pressable
|
|
141
|
+
key="retry"
|
|
142
|
+
onPress={onRetry}
|
|
143
|
+
style={uploadProgressStyles.actionButton({})}
|
|
144
|
+
accessibilityRole="button"
|
|
145
|
+
accessibilityLabel="Retry upload"
|
|
146
|
+
>
|
|
147
|
+
<Text style={uploadProgressStyles.actionIcon({})}>↻</Text>
|
|
148
|
+
</Pressable>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (actions.length === 0) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<View style={uploadProgressStyles.actions({})}>
|
|
158
|
+
{actions}
|
|
159
|
+
</View>
|
|
160
|
+
);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Render error message
|
|
164
|
+
const renderError = () => {
|
|
165
|
+
if (upload.state !== 'failed' || !upload.error) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<Text style={uploadProgressStyles.errorText({})}>
|
|
171
|
+
{upload.error.message}
|
|
172
|
+
</Text>
|
|
173
|
+
);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<View style={[uploadProgressStyles.container({}), style]} testID={testID}>
|
|
178
|
+
{/* File info row */}
|
|
179
|
+
<View style={uploadProgressStyles.infoRow({})}>
|
|
180
|
+
{showFileName && (
|
|
181
|
+
<Text style={uploadProgressStyles.fileName({})} numberOfLines={1}>
|
|
182
|
+
{upload.file.name}
|
|
183
|
+
</Text>
|
|
184
|
+
)}
|
|
185
|
+
{renderStateIcon()}
|
|
186
|
+
{renderActions()}
|
|
187
|
+
</View>
|
|
188
|
+
|
|
189
|
+
{/* Progress bar */}
|
|
190
|
+
{renderProgressBar()}
|
|
191
|
+
|
|
192
|
+
{/* Details row */}
|
|
193
|
+
{renderDetails()}
|
|
194
|
+
|
|
195
|
+
{/* Error message */}
|
|
196
|
+
{renderError()}
|
|
197
|
+
</View>
|
|
198
|
+
);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
UploadProgress.displayName = 'UploadProgress';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { FilePickerButton } from './FilePickerButton.native';
|
|
2
|
+
export { DropZone } from './DropZone.native';
|
|
3
|
+
export { UploadProgress } from './UploadProgress.native';
|
|
4
|
+
|
|
5
|
+
// Styles
|
|
6
|
+
export { filePickerButtonStyles } from './FilePickerButton.styles';
|
|
7
|
+
export { dropZoneStyles } from './DropZone.styles';
|
|
8
|
+
export { uploadProgressStyles } from './UploadProgress.styles';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Styles (platform-agnostic)
|
|
2
|
+
export { filePickerButtonStyles } from './FilePickerButton.styles';
|
|
3
|
+
export { dropZoneStyles } from './DropZone.styles';
|
|
4
|
+
export { uploadProgressStyles } from './UploadProgress.styles';
|
|
5
|
+
|
|
6
|
+
// Platform-specific components are exported from index.web.ts and index.native.ts
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { FilePickerButton } from './FilePickerButton.web';
|
|
2
|
+
export { DropZone } from './DropZone.web';
|
|
3
|
+
export { UploadProgress } from './UploadProgress.web';
|
|
4
|
+
|
|
5
|
+
// Styles
|
|
6
|
+
export { filePickerButtonStyles } from './FilePickerButton.styles';
|
|
7
|
+
export { dropZoneStyles } from './DropZone.styles';
|
|
8
|
+
export { uploadProgressStyles } from './UploadProgress.styles';
|