uplofile 1.1.1 → 2.0.0
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/dist/index.d.ts +49 -26
- package/dist/index.mjs +137 -36
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/// <reference lib="es2015.iterable" />
|
|
2
2
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
3
|
-
import React, { HTMLAttributes,
|
|
3
|
+
import React, { HTMLAttributes, DragEvent, RefObject, ChangeEvent, PropsWithChildren, ButtonHTMLAttributes } from 'react';
|
|
4
4
|
|
|
5
5
|
declare const Dropzone: ({ asChild, ...rest }: {
|
|
6
6
|
asChild?: boolean;
|
|
7
7
|
} & HTMLAttributes<HTMLElement>) => react_jsx_runtime.JSX.Element;
|
|
8
8
|
|
|
9
9
|
type UploadStatus = "idle" | "uploading" | "done" | "error" | "canceled" | "removing";
|
|
10
|
-
type UploadFileItem = {
|
|
10
|
+
type UploadFileItem<TMeta = any> = {
|
|
11
11
|
uid: string;
|
|
12
12
|
id?: string;
|
|
13
13
|
name: string;
|
|
@@ -17,15 +17,23 @@ type UploadFileItem = {
|
|
|
17
17
|
status: UploadStatus;
|
|
18
18
|
progress?: number;
|
|
19
19
|
error?: string;
|
|
20
|
-
|
|
20
|
+
meta?: TMeta;
|
|
21
21
|
};
|
|
22
22
|
type UploadResult = {
|
|
23
23
|
url: string;
|
|
24
24
|
id?: string;
|
|
25
25
|
};
|
|
26
|
-
type
|
|
26
|
+
type UplofileRootRef<TMeta = any> = {
|
|
27
|
+
setItems: (items: UploadFileItem<TMeta>[] | ((prev: UploadFileItem<TMeta>[]) => UploadFileItem<TMeta>[])) => void;
|
|
28
|
+
getItems: () => UploadFileItem<TMeta>[];
|
|
29
|
+
onDrop: (e: DragEvent) => void;
|
|
30
|
+
onDragOver: (e: DragEvent) => void;
|
|
31
|
+
openFileDialog: () => void;
|
|
32
|
+
actions: ItemActions;
|
|
33
|
+
};
|
|
34
|
+
type RootProps<TMeta = any> = PropsWithChildren<{
|
|
27
35
|
multiple?: boolean;
|
|
28
|
-
initial?: Array<Pick<UploadFileItem
|
|
36
|
+
initial?: Array<Pick<UploadFileItem<TMeta>, "uid" | "id" | "name" | "url" | "meta">>;
|
|
29
37
|
/**
|
|
30
38
|
* optimistic (default): remove from UI immediately, call onRemove in the background; if it fails, restore the item and show error.
|
|
31
39
|
* strict: call onRemove first; only remove from UI if it succeeds.
|
|
@@ -35,18 +43,18 @@ type RootProps = PropsWithChildren<{
|
|
|
35
43
|
maxCount?: number;
|
|
36
44
|
disabled?: boolean;
|
|
37
45
|
accept?: string;
|
|
38
|
-
onChange?: (items: UploadFileItem[]) => Promise<void> | void;
|
|
46
|
+
onChange?: (items: UploadFileItem<TMeta>[]) => Promise<void> | void;
|
|
39
47
|
upload: (file: File, signal: AbortSignal, setProgress?: (pct: number) => void) => Promise<UploadResult>;
|
|
40
|
-
onRemove?: (item: UploadFileItem
|
|
48
|
+
onRemove?: (item: UploadFileItem<TMeta>, signal: AbortSignal) => Promise<void | any>;
|
|
41
49
|
}>;
|
|
42
50
|
type ItemActions = {
|
|
43
51
|
cancel: (uid: string) => void;
|
|
44
52
|
remove: (uid: string) => void;
|
|
45
53
|
retry: (uid: string) => void;
|
|
46
54
|
};
|
|
47
|
-
type ImageUploaderContextValue = {
|
|
48
|
-
items: UploadFileItem[];
|
|
49
|
-
setItems: (items: UploadFileItem[]) => void;
|
|
55
|
+
type ImageUploaderContextValue<TMeta = any> = {
|
|
56
|
+
items: UploadFileItem<TMeta>[];
|
|
57
|
+
setItems: (items: UploadFileItem<TMeta>[]) => void;
|
|
50
58
|
disabled?: boolean;
|
|
51
59
|
multiple: boolean;
|
|
52
60
|
accept: string;
|
|
@@ -71,8 +79,8 @@ type ImageUploaderContextValue = {
|
|
|
71
79
|
hiddenInputValue: string;
|
|
72
80
|
name: string;
|
|
73
81
|
};
|
|
74
|
-
type TriggerRenderProps = {
|
|
75
|
-
items: UploadFileItem[];
|
|
82
|
+
type TriggerRenderProps<TMeta = any> = {
|
|
83
|
+
items: UploadFileItem<TMeta>[];
|
|
76
84
|
isUploading: boolean;
|
|
77
85
|
uploadingCount: number;
|
|
78
86
|
doneCount: number;
|
|
@@ -80,16 +88,16 @@ type TriggerRenderProps = {
|
|
|
80
88
|
totalProgress?: number;
|
|
81
89
|
open: () => void;
|
|
82
90
|
};
|
|
83
|
-
type PreviewRenderProps = {
|
|
84
|
-
items: UploadFileItem[];
|
|
85
|
-
setItems: (items: UploadFileItem[]) => void;
|
|
91
|
+
type PreviewRenderProps<TMeta = any> = {
|
|
92
|
+
items: UploadFileItem<TMeta>[];
|
|
93
|
+
setItems: (items: UploadFileItem<TMeta>[]) => void;
|
|
86
94
|
actions: ItemActions;
|
|
87
95
|
};
|
|
88
96
|
|
|
89
|
-
type Props = {
|
|
90
|
-
render?: (api: PreviewRenderProps) => React.ReactNode;
|
|
97
|
+
type Props<TMeta = any> = {
|
|
98
|
+
render?: (api: PreviewRenderProps<TMeta>) => React.ReactNode;
|
|
91
99
|
};
|
|
92
|
-
declare const Preview: ({ render }: Props) => string | number | boolean | Iterable<React.ReactNode> | react_jsx_runtime.JSX.Element | null | undefined;
|
|
100
|
+
declare const Preview: <TMeta = any>({ render }: Props<TMeta>) => string | number | boolean | Iterable<React.ReactNode> | react_jsx_runtime.JSX.Element | null | undefined;
|
|
93
101
|
declare const HiddenInput: ({ name }: {
|
|
94
102
|
name?: string;
|
|
95
103
|
}) => react_jsx_runtime.JSX.Element;
|
|
@@ -102,17 +110,32 @@ declare const Cancel: ({ uid, asChild, alwaysVisible, ...rest }: ButtonProps) =>
|
|
|
102
110
|
declare const Retry: ({ uid, asChild, ...rest }: ButtonProps) => react_jsx_runtime.JSX.Element;
|
|
103
111
|
declare const Remove: ({ uid, asChild, ...rest }: ButtonProps) => react_jsx_runtime.JSX.Element;
|
|
104
112
|
|
|
105
|
-
declare const Root:
|
|
113
|
+
declare const Root: React.ForwardRefExoticComponent<{
|
|
114
|
+
multiple?: boolean;
|
|
115
|
+
initial?: Pick<UploadFileItem<unknown>, "name" | "uid" | "id" | "url" | "meta">[] | undefined;
|
|
116
|
+
removeMode?: "optimistic" | "strict";
|
|
117
|
+
name?: string;
|
|
118
|
+
maxCount?: number;
|
|
119
|
+
disabled?: boolean;
|
|
120
|
+
accept?: string;
|
|
121
|
+
onChange?: ((items: UploadFileItem<unknown>[]) => Promise<void> | void) | undefined;
|
|
122
|
+
upload: (file: File, signal: AbortSignal, setProgress?: (pct: number) => void) => Promise<UploadResult>;
|
|
123
|
+
onRemove?: ((item: UploadFileItem<unknown>, signal: AbortSignal) => Promise<void | any>) | undefined;
|
|
124
|
+
} & {
|
|
125
|
+
children?: React.ReactNode | undefined;
|
|
126
|
+
} & React.RefAttributes<UplofileRootRef<unknown>>>;
|
|
106
127
|
|
|
107
|
-
declare const Trigger: ({ asChild, children, render, ...rest }: PropsWithChildren<{
|
|
128
|
+
declare const Trigger: <TMeta = any>({ asChild, children, render, ...rest }: PropsWithChildren<{
|
|
108
129
|
asChild?: boolean;
|
|
109
|
-
render?: (api: TriggerRenderProps) => React.ReactNode;
|
|
110
|
-
children?: React.ReactNode | ((api: TriggerRenderProps) => React.ReactNode);
|
|
130
|
+
render?: (api: TriggerRenderProps<TMeta>) => React.ReactNode;
|
|
131
|
+
children?: React.ReactNode | ((api: TriggerRenderProps<TMeta>) => React.ReactNode);
|
|
111
132
|
} & React.HTMLAttributes<HTMLElement>>) => react_jsx_runtime.JSX.Element;
|
|
112
133
|
|
|
113
|
-
declare const useUplofile: () => ImageUploaderContextValue
|
|
134
|
+
declare const useUplofile: <TMeta = any>() => ImageUploaderContextValue<TMeta>;
|
|
114
135
|
|
|
115
|
-
declare const
|
|
136
|
+
declare const getExtension: (path: string) => string | undefined;
|
|
137
|
+
declare const isVideoFile: (item: UploadFileItem<any>, extraExtensions?: string[]) => boolean;
|
|
138
|
+
declare const isImageFile: (item: UploadFileItem<any>, extraExtensions?: string[]) => boolean;
|
|
116
139
|
|
|
117
|
-
export { Cancel, Dropzone, HiddenInput, Preview, Remove, Retry, Root, Trigger, isVideoFile, useUplofile };
|
|
118
|
-
export type { ImageUploaderContextValue, ItemActions, RootProps, UploadFileItem, UploadResult, UploadStatus };
|
|
140
|
+
export { Cancel, Dropzone, HiddenInput, Preview, Remove, Retry, Root, Trigger, getExtension, isImageFile, isVideoFile, useUplofile };
|
|
141
|
+
export type { ImageUploaderContextValue, ItemActions, RootProps, UploadFileItem, UploadResult, UploadStatus, UplofileRootRef };
|
package/dist/index.mjs
CHANGED
|
@@ -1,45 +1,121 @@
|
|
|
1
1
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
2
|
import { Slot } from '@radix-ui/react-slot';
|
|
3
|
-
import { useState, useRef, useEffect, useMemo, useCallback, createContext, useContext } from 'react';
|
|
3
|
+
import { forwardRef, useState, useRef, useEffect, useMemo, useCallback, useImperativeHandle, createContext, useContext } from 'react';
|
|
4
4
|
|
|
5
5
|
const uid = ()=>Math.random().toString(36).slice(2, 10) + Date.now().toString(36).slice(-4);
|
|
6
|
-
const
|
|
6
|
+
const VIDEO_EXTENSIONS = [
|
|
7
|
+
"mp4",
|
|
8
|
+
"webm",
|
|
9
|
+
"ogg",
|
|
10
|
+
"mov",
|
|
11
|
+
"avi",
|
|
12
|
+
"mkv",
|
|
13
|
+
"wmv",
|
|
14
|
+
"flv",
|
|
15
|
+
"3gp",
|
|
16
|
+
"m4v",
|
|
17
|
+
"mpg",
|
|
18
|
+
"mpeg"
|
|
19
|
+
];
|
|
20
|
+
const IMAGE_EXTENSIONS = [
|
|
21
|
+
"jpg",
|
|
22
|
+
"jpeg",
|
|
23
|
+
"png",
|
|
24
|
+
"gif",
|
|
25
|
+
"webp",
|
|
26
|
+
"svg",
|
|
27
|
+
"avif",
|
|
28
|
+
"bmp",
|
|
29
|
+
"ico",
|
|
30
|
+
"tiff"
|
|
31
|
+
];
|
|
32
|
+
const getExtension = (path)=>{
|
|
33
|
+
try {
|
|
34
|
+
const url = new URL(path, "http://localhost");
|
|
35
|
+
const pathname = url.pathname;
|
|
36
|
+
const parts = pathname.split(".");
|
|
37
|
+
if (parts.length > 1) return parts.pop()?.toLowerCase();
|
|
38
|
+
return undefined;
|
|
39
|
+
} catch {
|
|
40
|
+
const parts = path.split("?")[0].split("#")[0].split(".");
|
|
41
|
+
if (parts.length > 1) return parts.pop()?.toLowerCase();
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const isVideoFile = (item, extraExtensions = [])=>{
|
|
7
46
|
if (item.file) {
|
|
8
47
|
return item.file.type.startsWith("video/");
|
|
9
48
|
}
|
|
49
|
+
const allExtensions = [
|
|
50
|
+
...VIDEO_EXTENSIONS,
|
|
51
|
+
...extraExtensions
|
|
52
|
+
];
|
|
53
|
+
if (item.url) {
|
|
54
|
+
const extension = getExtension(item.url);
|
|
55
|
+
if (extension && allExtensions.includes(extension)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (item.name) {
|
|
60
|
+
const extension = getExtension(item.name);
|
|
61
|
+
if (extension && allExtensions.includes(extension)) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
};
|
|
67
|
+
const isImageFile = (item, extraExtensions = [])=>{
|
|
68
|
+
if (item.file) {
|
|
69
|
+
return item.file.type.startsWith("image/");
|
|
70
|
+
}
|
|
71
|
+
const allExtensions = [
|
|
72
|
+
...IMAGE_EXTENSIONS,
|
|
73
|
+
...extraExtensions
|
|
74
|
+
];
|
|
10
75
|
if (item.url) {
|
|
11
|
-
const extension = item.url
|
|
12
|
-
|
|
13
|
-
"mp4",
|
|
14
|
-
"webm",
|
|
15
|
-
"ogg",
|
|
16
|
-
"mov",
|
|
17
|
-
"avi",
|
|
18
|
-
"mkv"
|
|
19
|
-
];
|
|
20
|
-
if (extension && videoExtensions.includes(extension)) {
|
|
76
|
+
const extension = getExtension(item.url);
|
|
77
|
+
if (extension && allExtensions.includes(extension)) {
|
|
21
78
|
return true;
|
|
22
79
|
}
|
|
23
80
|
}
|
|
24
81
|
if (item.name) {
|
|
25
|
-
const extension = item.name
|
|
26
|
-
|
|
27
|
-
"mp4",
|
|
28
|
-
"webm",
|
|
29
|
-
"ogg",
|
|
30
|
-
"mov",
|
|
31
|
-
"avi",
|
|
32
|
-
"mkv"
|
|
33
|
-
];
|
|
34
|
-
if (extension && videoExtensions.includes(extension)) {
|
|
82
|
+
const extension = getExtension(item.name);
|
|
83
|
+
if (extension && allExtensions.includes(extension)) {
|
|
35
84
|
return true;
|
|
36
85
|
}
|
|
37
86
|
}
|
|
38
87
|
return false;
|
|
39
88
|
};
|
|
89
|
+
const acceptsFile = (file, accept)=>{
|
|
90
|
+
if (!accept || accept.trim() === "") return true;
|
|
91
|
+
const type = (file.type || "").toLowerCase();
|
|
92
|
+
const name = (file.name || "").toLowerCase();
|
|
93
|
+
const tokens = accept.split(",").map((t)=>t.trim().toLowerCase()).filter(Boolean);
|
|
94
|
+
// If accept is malformed, be permissive (browser file input also ignores)
|
|
95
|
+
if (tokens.length === 0) return true;
|
|
96
|
+
// Helper: match extension token like .jpg or jpg
|
|
97
|
+
const hasExt = (token)=>{
|
|
98
|
+
const extToken = token.startsWith(".") ? token.slice(1) : token;
|
|
99
|
+
const fileExt = name.includes(".") ? name.split(".").pop() : undefined;
|
|
100
|
+
return !!fileExt && fileExt === extToken;
|
|
101
|
+
};
|
|
102
|
+
// Check any token matches
|
|
103
|
+
return tokens.some((token)=>{
|
|
104
|
+
if (token === "*/*") return true;
|
|
105
|
+
// MIME type with wildcard, e.g., image/*
|
|
106
|
+
if (token.endsWith("/*")) {
|
|
107
|
+
const prefix = token.slice(0, -1); // keep the slash
|
|
108
|
+
return type.startsWith(prefix);
|
|
109
|
+
}
|
|
110
|
+
// Full MIME type e.g., image/png
|
|
111
|
+
if (token.includes("/")) return type === token;
|
|
112
|
+
// File extensions, with or without a leading dot
|
|
113
|
+
return hasExt(token);
|
|
114
|
+
});
|
|
115
|
+
};
|
|
40
116
|
|
|
41
117
|
const UploaderCtx = /*#__PURE__*/ createContext(null);
|
|
42
|
-
const Root = ({ multiple = true, initial = [], onChange, upload, removeMode = "optimistic", onRemove, accept = "image/*", name = "
|
|
118
|
+
const Root = /*#__PURE__*/ forwardRef(({ multiple = true, initial = [], onChange, upload, removeMode = "optimistic", onRemove, accept = "image/*", name = "image", maxCount, disabled, children }, ref)=>{
|
|
43
119
|
const [items, setItems] = useState([]);
|
|
44
120
|
const controllers = useRef(new Map());
|
|
45
121
|
const removeControllers = useRef(new Map());
|
|
@@ -56,7 +132,8 @@ const Root = ({ multiple = true, initial = [], onChange, upload, removeMode = "o
|
|
|
56
132
|
id: it.id,
|
|
57
133
|
name: it.name,
|
|
58
134
|
url: it.url,
|
|
59
|
-
status: "done"
|
|
135
|
+
status: "done",
|
|
136
|
+
meta: it.meta
|
|
60
137
|
};
|
|
61
138
|
});
|
|
62
139
|
// Only hydrate if the user hasn't already added/modified items locally
|
|
@@ -131,8 +208,9 @@ const Root = ({ multiple = true, initial = [], onChange, upload, removeMode = "o
|
|
|
131
208
|
upload
|
|
132
209
|
]);
|
|
133
210
|
const selectFiles = useCallback((files)=>{
|
|
134
|
-
if (!files
|
|
135
|
-
const selected = Array.from(files);
|
|
211
|
+
if (!files) return;
|
|
212
|
+
const selected = Array.isArray(files) ? files : Array.from(files);
|
|
213
|
+
if (selected.length === 0) return;
|
|
136
214
|
const remaining = maxCount ? Math.max(0, maxCount - items.filter((i)=>i.status !== "canceled").length) : undefined;
|
|
137
215
|
const toUse = typeof remaining === "number" ? selected.slice(0, remaining) : selected;
|
|
138
216
|
const newItems = toUse.map((file)=>({
|
|
@@ -163,10 +241,15 @@ const Root = ({ multiple = true, initial = [], onChange, upload, removeMode = "o
|
|
|
163
241
|
const onDrop = useCallback((e)=>{
|
|
164
242
|
e.preventDefault();
|
|
165
243
|
if (disabled) return;
|
|
166
|
-
|
|
244
|
+
const dtFiles = e.dataTransfer?.files;
|
|
245
|
+
if (!dtFiles || dtFiles.length === 0) return;
|
|
246
|
+
const accepted = Array.from(dtFiles).filter((f)=>acceptsFile(f, accept));
|
|
247
|
+
if (accepted.length === 0) return;
|
|
248
|
+
selectFiles(accepted);
|
|
167
249
|
}, [
|
|
168
250
|
disabled,
|
|
169
|
-
selectFiles
|
|
251
|
+
selectFiles,
|
|
252
|
+
accept
|
|
170
253
|
]);
|
|
171
254
|
const onDragOver = useCallback((e)=>e.preventDefault(), []);
|
|
172
255
|
const actions = useMemo(()=>({
|
|
@@ -271,6 +354,21 @@ const Root = ({ multiple = true, initial = [], onChange, upload, removeMode = "o
|
|
|
271
354
|
hiddenInputValue,
|
|
272
355
|
name
|
|
273
356
|
};
|
|
357
|
+
// Expose imperative methods via ref
|
|
358
|
+
useImperativeHandle(ref, ()=>({
|
|
359
|
+
setItems: emitChange,
|
|
360
|
+
getItems: ()=>items,
|
|
361
|
+
onDrop,
|
|
362
|
+
onDragOver,
|
|
363
|
+
openFileDialog: ()=>inputRef.current?.click(),
|
|
364
|
+
actions
|
|
365
|
+
}), [
|
|
366
|
+
emitChange,
|
|
367
|
+
items,
|
|
368
|
+
onDrop,
|
|
369
|
+
onDragOver,
|
|
370
|
+
actions
|
|
371
|
+
]);
|
|
274
372
|
return /*#__PURE__*/ jsx(UploaderCtx.Provider, {
|
|
275
373
|
value: ctx,
|
|
276
374
|
children: /*#__PURE__*/ jsxs("div", {
|
|
@@ -285,7 +383,7 @@ const Root = ({ multiple = true, initial = [], onChange, upload, removeMode = "o
|
|
|
285
383
|
]
|
|
286
384
|
})
|
|
287
385
|
});
|
|
288
|
-
};
|
|
386
|
+
});
|
|
289
387
|
|
|
290
388
|
const useUplofile = ()=>{
|
|
291
389
|
const ctx = useContext(UploaderCtx);
|
|
@@ -582,33 +680,36 @@ const Cancel = ({ uid, asChild, alwaysVisible = false, ...rest })=>{
|
|
|
582
680
|
const Comp = asChild ? Slot : "button";
|
|
583
681
|
if (!isUploading && !alwaysVisible) return null;
|
|
584
682
|
return /*#__PURE__*/ jsx(Comp, {
|
|
683
|
+
...rest,
|
|
585
684
|
onClick: (e)=>{
|
|
586
685
|
e.stopPropagation();
|
|
587
686
|
actions.cancel(uid);
|
|
588
|
-
|
|
589
|
-
|
|
687
|
+
rest.onClick?.(e);
|
|
688
|
+
}
|
|
590
689
|
});
|
|
591
690
|
};
|
|
592
691
|
const Retry = ({ uid, asChild, ...rest })=>{
|
|
593
692
|
const { actions } = useUplofile();
|
|
594
693
|
const Comp = asChild ? Slot : "button";
|
|
595
694
|
return /*#__PURE__*/ jsx(Comp, {
|
|
695
|
+
...rest,
|
|
596
696
|
onClick: (e)=>{
|
|
597
697
|
e.stopPropagation();
|
|
598
698
|
actions.retry(uid);
|
|
599
|
-
|
|
600
|
-
|
|
699
|
+
rest.onClick?.(e);
|
|
700
|
+
}
|
|
601
701
|
});
|
|
602
702
|
};
|
|
603
703
|
const Remove = ({ uid, asChild, ...rest })=>{
|
|
604
704
|
const { actions } = useUplofile();
|
|
605
705
|
const Comp = asChild ? Slot : "button";
|
|
606
706
|
return /*#__PURE__*/ jsx(Comp, {
|
|
707
|
+
...rest,
|
|
607
708
|
onClick: (e)=>{
|
|
608
709
|
e.stopPropagation();
|
|
609
710
|
actions.remove(uid);
|
|
610
|
-
|
|
611
|
-
|
|
711
|
+
rest.onClick?.(e);
|
|
712
|
+
}
|
|
612
713
|
});
|
|
613
714
|
};
|
|
614
715
|
|
|
@@ -643,4 +744,4 @@ const Trigger = ({ asChild, children, render, ...rest })=>{
|
|
|
643
744
|
});
|
|
644
745
|
};
|
|
645
746
|
|
|
646
|
-
export { Cancel, Dropzone, HiddenInput, Preview, Remove, Retry, Root, Trigger, isVideoFile, useUplofile };
|
|
747
|
+
export { Cancel, Dropzone, HiddenInput, Preview, Remove, Retry, Root, Trigger, getExtension, isImageFile, isVideoFile, useUplofile };
|