2024-03-18 14:12:27 +00:00
|
|
|
import Uppy, {debugLogger, type State, UppyFile} from "@uppy/core"
|
2024-03-12 12:42:17 +00:00
|
|
|
|
|
|
|
import Tus from "@uppy/tus"
|
|
|
|
import toArray from "@uppy/utils/lib/toArray"
|
|
|
|
|
2024-03-18 14:12:27 +00:00
|
|
|
import {type ChangeEvent, useCallback, useEffect, useMemo, useRef, useState} from "react"
|
|
|
|
import DropTarget, {type DropTargetOptions} from "./uppy-dropzone"
|
|
|
|
import {useSdk} from "~/components/lib/sdk-context.js";
|
|
|
|
import UppyFileUpload from "~/components/lib/uppy-file-upload.js";
|
2024-03-18 19:32:47 +00:00
|
|
|
import {PROTOCOL_S5, Sdk} from "@lumeweb/portal-sdk";
|
2024-03-18 22:51:41 +00:00
|
|
|
import {S5Client, HashProgressEvent} from "@lumeweb/s5-js";
|
2024-03-12 12:42:17 +00:00
|
|
|
|
|
|
|
const LISTENING_EVENTS = [
|
|
|
|
"upload",
|
|
|
|
"upload-success",
|
|
|
|
"upload-error",
|
|
|
|
"file-added",
|
|
|
|
"file-removed",
|
|
|
|
"files-added"
|
|
|
|
] as const
|
|
|
|
|
|
|
|
export function useUppy({
|
|
|
|
endpoint
|
|
|
|
}: {
|
|
|
|
endpoint: string
|
|
|
|
}) {
|
2024-03-18 14:12:27 +00:00
|
|
|
const sdk = useSdk()
|
|
|
|
|
|
|
|
const [uploadLimit, setUploadLimit] = useState<number>(0)
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
async function getUploadLimit() {
|
|
|
|
try {
|
|
|
|
const limit = await sdk.account!().uploadLimit();
|
|
|
|
setUploadLimit(limit);
|
|
|
|
} catch (err) {
|
|
|
|
console.log('Error occured while fetching upload limit', err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
getUploadLimit();
|
|
|
|
}, []);
|
|
|
|
|
2024-03-12 12:42:17 +00:00
|
|
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
|
|
const [targetRef, _setTargetRef] = useState<HTMLElement | null>(null)
|
|
|
|
const uppyInstance = useRef<Uppy>()
|
|
|
|
const setRef = useCallback(
|
|
|
|
(element: HTMLElement | null) => _setTargetRef(element),
|
|
|
|
[]
|
|
|
|
)
|
2024-03-13 17:07:03 +00:00
|
|
|
const [, setUppyState] = useState<State>()
|
2024-03-12 14:32:00 +00:00
|
|
|
const [state, setState] = useState<
|
|
|
|
"completed" | "idle" | "initializing" | "error" | "uploading"
|
|
|
|
>("initializing")
|
2024-03-12 12:42:17 +00:00
|
|
|
|
|
|
|
const [inputProps, setInputProps] = useState<
|
|
|
|
| {
|
|
|
|
ref: typeof inputRef
|
|
|
|
type: "file"
|
|
|
|
onChange: (event: ChangeEvent<HTMLInputElement>) => void
|
|
|
|
}
|
|
|
|
| object
|
|
|
|
>({})
|
|
|
|
const getRootProps = useMemo(
|
|
|
|
() => () => {
|
|
|
|
return {
|
|
|
|
ref: setRef,
|
|
|
|
onClick: () => {
|
|
|
|
if (inputRef.current) {
|
|
|
|
//@ts-expect-error -- dumb html
|
|
|
|
inputRef.current.value = null
|
|
|
|
inputRef.current.click()
|
|
|
|
console.log("clicked", { input: inputRef.current })
|
|
|
|
}
|
|
|
|
},
|
|
|
|
role: "presentation"
|
|
|
|
}
|
|
|
|
},
|
|
|
|
[setRef]
|
|
|
|
)
|
|
|
|
const removeFile = useCallback(
|
|
|
|
(id: string) => {
|
|
|
|
uppyInstance.current?.removeFile(id)
|
|
|
|
},
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
[targetRef, uppyInstance]
|
|
|
|
)
|
|
|
|
const cancelAll = useCallback(
|
2024-03-12 14:32:00 +00:00
|
|
|
() => uppyInstance.current?.cancelAll({ reason: "user" }),
|
2024-03-12 12:42:17 +00:00
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
[targetRef, uppyInstance]
|
|
|
|
)
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (!targetRef) return
|
|
|
|
|
2024-03-18 19:32:47 +00:00
|
|
|
const tusPreprocessor = async (fileIDs: string[]) => {
|
|
|
|
for(const fileID of fileIDs) {
|
|
|
|
const file = uppyInstance.current?.getFile(fileID) as UppyFile
|
|
|
|
// @ts-ignore
|
|
|
|
if (file.uploader === "tus") {
|
2024-03-18 22:51:41 +00:00
|
|
|
const hashProgressCb = (event: HashProgressEvent) => {
|
|
|
|
uppyInstance.current?.emit("preprocess-progress", file, {
|
|
|
|
uploadStarted: false,
|
|
|
|
bytesUploaded: 0,
|
|
|
|
preprocess: {
|
|
|
|
mode: "determinate",
|
|
|
|
message: "Hashing file...",
|
|
|
|
value: Math.round((event.total / event.total) * 100)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
const options = await sdk.protocols!().get<S5Client>(PROTOCOL_S5).getSdk().getTusOptions(file.data as File, {}, {onHashProgress: hashProgressCb})
|
2024-03-18 19:32:47 +00:00
|
|
|
uppyInstance.current?.setFileState(fileID, {
|
2024-03-18 20:48:29 +00:00
|
|
|
tus: options,
|
|
|
|
meta: {
|
|
|
|
...options.metadata,
|
|
|
|
...file.meta,
|
|
|
|
}
|
2024-03-18 19:32:47 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-18 14:12:27 +00:00
|
|
|
const uppy = new Uppy({
|
|
|
|
logger: debugLogger,
|
|
|
|
onBeforeUpload: (files) => {
|
|
|
|
for (const file of Object.entries(files)) {
|
|
|
|
// @ts-ignore
|
|
|
|
file[1].uploader = file[1].size > uploadLimit ? "tus" : "file";
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
}).use(DropTarget, {
|
2024-03-12 12:42:17 +00:00
|
|
|
target: targetRef
|
|
|
|
} as DropTargetOptions)
|
|
|
|
|
|
|
|
uppyInstance.current = uppy
|
|
|
|
setInputProps({
|
|
|
|
ref: inputRef,
|
|
|
|
type: "file",
|
|
|
|
onChange: (event) => {
|
|
|
|
const files = toArray(event.target.files)
|
|
|
|
if (files.length > 0) {
|
|
|
|
uppyInstance.current?.log("[DragDrop] Files selected through input")
|
|
|
|
uppyInstance.current?.addFiles(files)
|
|
|
|
}
|
|
|
|
|
2024-03-18 16:37:22 +00:00
|
|
|
uppy.iteratePlugins((plugin) => {
|
|
|
|
uppy.removePlugin(plugin);
|
|
|
|
});
|
|
|
|
|
|
|
|
uppy.use(UppyFileUpload, { sdk: sdk as Sdk })
|
|
|
|
|
|
|
|
let useTus = false;
|
|
|
|
|
|
|
|
uppyInstance.current?.getFiles().forEach((file) => {
|
|
|
|
if (file.size > uploadLimit) {
|
|
|
|
useTus = true;
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
if (useTus) {
|
2024-03-18 22:20:24 +00:00
|
|
|
uppy.use(Tus, { endpoint: endpoint, limit: 6, parallelUploads: 10 })
|
2024-03-18 19:32:47 +00:00
|
|
|
uppy.addPreProcessor(tusPreprocessor)
|
2024-03-18 16:37:22 +00:00
|
|
|
}
|
|
|
|
|
2024-03-12 12:42:17 +00:00
|
|
|
// We clear the input after a file is selected, because otherwise
|
|
|
|
// change event is not fired in Chrome and Safari when a file
|
|
|
|
// with the same name is selected.
|
|
|
|
// ___Why not use value="" on <input/> instead?
|
|
|
|
// Because if we use that method of clearing the input,
|
|
|
|
// Chrome will not trigger change if we drop the same file twice (Issue #768).
|
|
|
|
// @ts-expect-error TS freaks out, but this is fine
|
|
|
|
// eslint-disable-next-line no-param-reassign
|
|
|
|
event.target.value = null
|
|
|
|
}
|
|
|
|
})
|
2024-03-18 19:32:47 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
2024-03-12 12:42:17 +00:00
|
|
|
uppy.on("complete", (result) => {
|
|
|
|
if (result.failed.length === 0) {
|
|
|
|
console.log("Upload successful üòÄ")
|
2024-03-12 14:32:00 +00:00
|
|
|
setState("completed")
|
2024-03-12 12:42:17 +00:00
|
|
|
} else {
|
|
|
|
console.warn("Upload failed üòû")
|
2024-03-12 14:32:00 +00:00
|
|
|
setState("error")
|
2024-03-12 12:42:17 +00:00
|
|
|
}
|
|
|
|
console.log("successful files:", result.successful)
|
|
|
|
console.log("failed files:", result.failed)
|
|
|
|
})
|
|
|
|
|
2024-03-12 14:32:00 +00:00
|
|
|
const setStateCb = (event: (typeof LISTENING_EVENTS)[number]) => {
|
|
|
|
switch (event) {
|
|
|
|
case "upload":
|
|
|
|
setState("uploading")
|
|
|
|
break
|
|
|
|
case "upload-error":
|
|
|
|
setState("error")
|
|
|
|
break
|
|
|
|
default:
|
|
|
|
break
|
|
|
|
}
|
|
|
|
setUppyState(uppy.getState())
|
2024-03-12 12:42:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for (const event of LISTENING_EVENTS) {
|
2024-03-12 14:32:00 +00:00
|
|
|
uppy.on(event, function cb() {
|
|
|
|
setStateCb(event)
|
|
|
|
})
|
2024-03-12 12:42:17 +00:00
|
|
|
}
|
2024-03-12 14:32:00 +00:00
|
|
|
setState("idle")
|
2024-03-18 14:12:27 +00:00
|
|
|
}, [targetRef, endpoint, uploadLimit])
|
2024-03-12 12:42:17 +00:00
|
|
|
|
2024-03-12 14:36:44 +00:00
|
|
|
useEffect(() => {
|
2024-03-12 12:42:17 +00:00
|
|
|
return () => {
|
2024-03-12 14:36:44 +00:00
|
|
|
uppyInstance.current?.cancelAll({ reason: "unmount" })
|
|
|
|
uppyInstance.current?.logout()
|
|
|
|
uppyInstance.current?.close()
|
|
|
|
uppyInstance.current = undefined
|
2024-03-12 12:42:17 +00:00
|
|
|
}
|
2024-03-12 14:36:44 +00:00
|
|
|
}, [])
|
2024-03-12 12:42:17 +00:00
|
|
|
return {
|
2024-03-12 14:32:00 +00:00
|
|
|
getFiles: () => uppyInstance.current?.getFiles() ?? [],
|
2024-03-12 12:42:17 +00:00
|
|
|
error: uppyInstance.current?.getState,
|
|
|
|
state,
|
|
|
|
upload: () =>
|
|
|
|
uppyInstance.current?.upload() ??
|
|
|
|
new Error("[useUppy] Uppy has not initialized yet."),
|
|
|
|
getInputProps: () => inputProps,
|
|
|
|
getRootProps,
|
|
|
|
removeFile,
|
2024-03-12 14:32:00 +00:00
|
|
|
cancelAll
|
2024-03-12 12:42:17 +00:00
|
|
|
}
|
|
|
|
}
|