Dropzone: dedupe, video preview
This commit is contained in:
parent
0ea92244b2
commit
c8f11f89a9
|
@ -18,7 +18,7 @@
|
|||
"dropDescription": "You can upload an image by dragging it into here.",
|
||||
"error": {
|
||||
"title": "Uh-Oh, an error happened!",
|
||||
"unsupported": "This filetype cannot be uploaded.\nSupported types are: jpg, png, gif"
|
||||
"unsupported": "This filetype cannot be uploaded.\nSupported types are: jpg, png, gif, mp4"
|
||||
},
|
||||
"aria": {
|
||||
"dialogLabel": "Upload dialog"
|
||||
|
|
|
@ -50,12 +50,19 @@ const FullscreenDropzoneStyles: ComponentStyles<
|
|||
justifyContent: "space-between",
|
||||
outline: "none",
|
||||
marginBottom: "20px",
|
||||
flexShrink: "0",
|
||||
"& > h1": {
|
||||
lineHeight: "40px",
|
||||
},
|
||||
},
|
||||
fullscreenDropzoneDialogContent_buttons: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
"& > *:first-child": {
|
||||
marginRight: "10px",
|
||||
marginRight: "15px",
|
||||
},
|
||||
"& > *:last-child": {
|
||||
margin: "0",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,7 +5,9 @@ import { ManagedClasses } from "@microsoft/fast-jss-manager-react";
|
|||
*/
|
||||
export interface DropzoneUploadClassNameContract {
|
||||
dropzoneUpload?: string;
|
||||
dropzoneUpload_image?: string;
|
||||
dropzoneUpload_rejectIcon?: string;
|
||||
dropzoneUpload_preview?: string;
|
||||
dropzoneUpload_background?: string;
|
||||
dropzoneUpload_details?: string;
|
||||
dropzoneUpload_error?: string;
|
||||
dropzoneUpload_progress?: string;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useEffect, useCallback, Fragment } from "react";
|
||||
import { FaExclamation, FaCheck } from "react-icons/fa";
|
||||
import React, { useState, useEffect, useCallback, Fragment, useRef } from "react";
|
||||
import { FaExclamation, FaCheck, FaRegFrownOpen } from "react-icons/fa";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Progress,
|
||||
|
@ -22,7 +22,7 @@ import {
|
|||
backgroundColor,
|
||||
} from "@microsoft/fast-components-styles-msft";
|
||||
import { parseColorHexRGBA } from "@microsoft/fast-colors";
|
||||
import { ProgressIcon } from "../../_DesignSystem";
|
||||
import { ProgressIcon, getScaleFactorByConstraints } from "../../_DesignSystem";
|
||||
import axios from "../../_interceptedAxios";
|
||||
|
||||
const DropzoneUploadStyles: ComponentStyles<
|
||||
|
@ -40,8 +40,15 @@ const DropzoneUploadStyles: ComponentStyles<
|
|||
transform: "translateY(0%)",
|
||||
},
|
||||
},
|
||||
dropzoneUpload_image: {
|
||||
objectFit: "contain",
|
||||
dropzoneUpload_preview: {
|
||||
position: "absolute",
|
||||
left: "0",
|
||||
top: "0",
|
||||
},
|
||||
dropzoneUpload_background: {
|
||||
filter: "blur(14px)",
|
||||
transform: "scale(1.1)",
|
||||
opacity: ".8",
|
||||
},
|
||||
dropzoneUpload_details: {
|
||||
position: "absolute",
|
||||
|
@ -74,6 +81,16 @@ const DropzoneUploadStyles: ComponentStyles<
|
|||
fontSize: "4em",
|
||||
color: neutralForegroundRest,
|
||||
},
|
||||
dropzoneUpload_rejectIcon: {
|
||||
width: "55px",
|
||||
height: "55px",
|
||||
position: "absolute",
|
||||
top: "0",
|
||||
bottom: "0",
|
||||
left: "0",
|
||||
right: "0",
|
||||
margin: "auto",
|
||||
},
|
||||
};
|
||||
|
||||
const ProgressStyles: ComponentStyles<ProgressClassNameContract, DesignSystem> = {
|
||||
|
@ -85,99 +102,233 @@ const ProgressStyles: ComponentStyles<ProgressClassNameContract, DesignSystem> =
|
|||
progress_circularSVG__page: {},
|
||||
};
|
||||
|
||||
const DropzoneUpload = (props: DropzoneUploadProps) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [resData, setResData] = useState({ id: null, file_url: null });
|
||||
|
||||
const onUploadProgress = (prog: ProgressEvent) =>
|
||||
setProgress(Math.round((prog.loaded * 100) / prog.total));
|
||||
|
||||
const uploadFile = useCallback(async () => {
|
||||
const formData = new FormData();
|
||||
formData.set("data", props.file);
|
||||
|
||||
try {
|
||||
const res = await axios.post(window.location.origin + "/api/upload.php", formData, {
|
||||
onUploadProgress,
|
||||
});
|
||||
setResData(res.data);
|
||||
} catch (err) {
|
||||
console.log("An error happened!\n", err);
|
||||
setErrorMessage(err.i18n ? t(err.i18n) : err.message || "Unknkown Error!");
|
||||
return null;
|
||||
}
|
||||
}, [props.file, t]);
|
||||
|
||||
// onMount
|
||||
useEffect(() => {
|
||||
console.log("updated!");
|
||||
if (props.rejected) {
|
||||
let savedTimeout = setTimeout(props.onRemoveRequest, 6000);
|
||||
return () => clearTimeout(savedTimeout);
|
||||
}
|
||||
|
||||
// Uploading process
|
||||
if (resData.id || errorMessage) return () => {};
|
||||
uploadFile();
|
||||
return () => {};
|
||||
}, [errorMessage, props.onRemoveRequest, props.rejected, resData.id, uploadFile]);
|
||||
|
||||
return (
|
||||
<div className={props.managedClasses.dropzoneUpload}>
|
||||
{!props.rejected && props.preview && (
|
||||
<img
|
||||
src={props.preview}
|
||||
alt={props.file.name}
|
||||
className={props.managedClasses.dropzoneUpload_image}
|
||||
width="200"
|
||||
height="200"
|
||||
/>
|
||||
)}
|
||||
<footer className={props.managedClasses.dropzoneUpload_details} tabIndex={0}>
|
||||
<Label
|
||||
className={
|
||||
props.rejected || errorMessage || resData.id
|
||||
? props.managedClasses.dropzoneUpload_error
|
||||
: ""
|
||||
}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{props.rejected ? (
|
||||
t("upload.error.unsupported")
|
||||
) : errorMessage ? (
|
||||
errorMessage
|
||||
) : resData.id ? (
|
||||
<Fragment>
|
||||
{t("upload.finished", { filename: resData.id })}
|
||||
<br />
|
||||
<Hypertext href={resData.file_url} target="_">
|
||||
{t("upload.visit")}
|
||||
</Hypertext>
|
||||
</Fragment>
|
||||
) : (
|
||||
props.file.name
|
||||
)}
|
||||
</Label>
|
||||
</footer>
|
||||
<div className={props.managedClasses.dropzoneUpload_progress}>
|
||||
{props.rejected || errorMessage.length > 0 ? (
|
||||
<ProgressIcon icon={FaExclamation} />
|
||||
) : progress === 100 ? (
|
||||
<ProgressIcon icon={FaCheck} />
|
||||
) : (
|
||||
<Progress
|
||||
jssStyleSheet={ProgressStyles}
|
||||
circular={true}
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
value={progress}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
/**
|
||||
* Centers and scales a given ImageSource,
|
||||
* so that it fits onto a specified canvas
|
||||
*/
|
||||
const alignImageToCanvas = (
|
||||
source: CanvasImageSource,
|
||||
srcWidth: number,
|
||||
srcHeight: number,
|
||||
ctx: CanvasRenderingContext2D
|
||||
) => {
|
||||
const canvas = ctx.canvas;
|
||||
const factor = getScaleFactorByConstraints(
|
||||
srcWidth,
|
||||
srcHeight,
|
||||
canvas.width,
|
||||
canvas.height
|
||||
);
|
||||
const deltaX = (canvas.width - srcWidth * factor) / 2;
|
||||
const deltaY = (canvas.height - srcHeight * factor) / 2;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(
|
||||
source,
|
||||
0,
|
||||
0,
|
||||
srcWidth,
|
||||
srcHeight,
|
||||
deltaX,
|
||||
deltaY,
|
||||
srcWidth * factor,
|
||||
srcHeight * factor
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Centers and scales a given ImageSource,
|
||||
* so that it fills the entire canvas space
|
||||
*/
|
||||
const coverCanvasWithImage = (
|
||||
source: CanvasImageSource,
|
||||
srcWidth: number,
|
||||
srcHeight: number,
|
||||
ctx: CanvasRenderingContext2D
|
||||
) => {
|
||||
const canvas = ctx.canvas;
|
||||
let factor = 0;
|
||||
|
||||
if (srcHeight > srcWidth) factor = canvas.width / srcWidth;
|
||||
else factor = canvas.height / srcHeight;
|
||||
|
||||
const deltaX = (canvas.width - srcWidth * factor) / 2;
|
||||
const deltaY = (canvas.height - srcHeight * factor) / 2;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(
|
||||
source,
|
||||
0,
|
||||
0,
|
||||
srcWidth,
|
||||
srcHeight,
|
||||
deltaX,
|
||||
deltaY,
|
||||
srcWidth * factor,
|
||||
srcHeight * factor
|
||||
);
|
||||
};
|
||||
|
||||
const DropzoneUpload: React.ComponentType<DropzoneUploadProps> = React.memo(
|
||||
({ managedClasses, rejected, file, onRemoveRequest, preview }) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const [hasStartedUpload, setUploadState] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [resData, setResData] = useState({ id: null, file_url: null });
|
||||
|
||||
/**
|
||||
* Ref of the canvas that is used to depict the currently uploaded item
|
||||
*/
|
||||
const canvasRef = useRef<HTMLCanvasElement>();
|
||||
|
||||
/**
|
||||
* Ref of the canvas that is used as a background element
|
||||
*/
|
||||
const canvasBgRef = useRef<HTMLCanvasElement>();
|
||||
|
||||
const onUploadProgress = (prog: ProgressEvent) =>
|
||||
setProgress(Math.round((prog.loaded * 100) / prog.total));
|
||||
|
||||
const uploadFile = useCallback(async () => {
|
||||
const formData = new FormData();
|
||||
formData.set("data", file);
|
||||
setUploadState(true);
|
||||
|
||||
try {
|
||||
const res = await axios.post(
|
||||
window.location.origin + "/api/upload.php",
|
||||
formData,
|
||||
{ onUploadProgress }
|
||||
);
|
||||
setResData(res.data);
|
||||
} catch (err) {
|
||||
console.log("An error happened!\n", err);
|
||||
setErrorMessage(err.i18n ? t(err.i18n) : err.message || "Unknkown Error!");
|
||||
return null;
|
||||
}
|
||||
}, [file, t]);
|
||||
|
||||
useEffect(() => {
|
||||
// Request umounting if upload has been rejected
|
||||
if (rejected) {
|
||||
let savedTimeout = setTimeout(onRemoveRequest, 6000);
|
||||
return () => clearTimeout(savedTimeout);
|
||||
}
|
||||
|
||||
// Abort if upload has already started
|
||||
if (hasStartedUpload) return;
|
||||
uploadFile();
|
||||
}, [hasStartedUpload, onRemoveRequest, rejected, uploadFile]);
|
||||
|
||||
/**
|
||||
* Preview image generation.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (rejected || !preview) return;
|
||||
|
||||
const type = file.type.split("/")[0];
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas.getContext("2d");
|
||||
const ctxBg = canvasBgRef.current.getContext("2d");
|
||||
|
||||
console.log(preview, file.type, canvasRef.current, type);
|
||||
|
||||
if (type === "image") {
|
||||
const img = new Image();
|
||||
img.addEventListener("load", () => {
|
||||
alignImageToCanvas(img, img.width, img.height, ctx);
|
||||
coverCanvasWithImage(img, img.width, img.height, ctxBg);
|
||||
});
|
||||
img.src = preview;
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "video") {
|
||||
const video = document.createElement("video");
|
||||
video.style.position = "fixed";
|
||||
video.autoplay = false;
|
||||
video.addEventListener("loadeddata", () => {
|
||||
const seekHandler = () => {
|
||||
video.removeEventListener("seeked", seekHandler);
|
||||
video.pause();
|
||||
console.log("seeked!");
|
||||
alignImageToCanvas(video, video.videoWidth, video.videoHeight, ctx);
|
||||
coverCanvasWithImage(video, video.videoWidth, video.videoHeight, ctxBg);
|
||||
};
|
||||
video.addEventListener("seeked", seekHandler);
|
||||
video.currentTime = 0;
|
||||
video.play();
|
||||
});
|
||||
video.src = preview;
|
||||
document.body.appendChild(video);
|
||||
}
|
||||
}, [file.type, preview, rejected]);
|
||||
|
||||
return (
|
||||
<div className={managedClasses.dropzoneUpload}>
|
||||
{!rejected ? (
|
||||
preview && (
|
||||
<>
|
||||
<canvas
|
||||
ref={canvasBgRef}
|
||||
className={managedClasses.dropzoneUpload_background}
|
||||
width="200"
|
||||
height="200"
|
||||
/>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className={managedClasses.dropzoneUpload_preview}
|
||||
width="200"
|
||||
height="200"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<FaRegFrownOpen className={managedClasses.dropzoneUpload_rejectIcon} />
|
||||
)}
|
||||
<footer className={managedClasses.dropzoneUpload_details} tabIndex={0}>
|
||||
<Label
|
||||
className={
|
||||
rejected || errorMessage || resData.id
|
||||
? managedClasses.dropzoneUpload_error
|
||||
: ""
|
||||
}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{rejected ? (
|
||||
t("upload.error.unsupported")
|
||||
) : errorMessage ? (
|
||||
errorMessage
|
||||
) : resData.id ? (
|
||||
<Fragment>
|
||||
{t("upload.finished", { filename: resData.id })}
|
||||
<br />
|
||||
<Hypertext href={resData.file_url} target="_">
|
||||
{t("upload.visit")}
|
||||
</Hypertext>
|
||||
</Fragment>
|
||||
) : (
|
||||
file.name
|
||||
)}
|
||||
</Label>
|
||||
</footer>
|
||||
<div className={managedClasses.dropzoneUpload_progress}>
|
||||
{rejected || errorMessage.length > 0 ? (
|
||||
<ProgressIcon icon={FaExclamation} />
|
||||
) : progress === 100 ? (
|
||||
<ProgressIcon icon={FaCheck} />
|
||||
) : (
|
||||
<Progress
|
||||
jssStyleSheet={ProgressStyles}
|
||||
circular={true}
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
value={progress}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default manageJss(DropzoneUploadStyles)(DropzoneUpload);
|
||||
|
|
|
@ -29,11 +29,7 @@ const DropzoneUploadManager = (props: DropzoneUploadManagerProps) => {
|
|||
|
||||
const manageDropDataChange = useCallback(() => {
|
||||
const { dropData } = props;
|
||||
if (dropData.acceptedFiles.length <= 0) {
|
||||
console.log("bingo");
|
||||
}
|
||||
|
||||
console.log("manageDropDataChange");
|
||||
const acceptedUploadList = dropData.acceptedFiles.map(file => {
|
||||
return {
|
||||
key: uniqueId(new Date().getTime() + ""),
|
||||
|
@ -56,7 +52,6 @@ const DropzoneUploadManager = (props: DropzoneUploadManagerProps) => {
|
|||
...acceptedUploadList,
|
||||
...rejectedUploadList,
|
||||
];
|
||||
console.log(updatedUploadList);
|
||||
changeUploadList(updatedUploadList);
|
||||
}, [props, uploadList]);
|
||||
|
||||
|
@ -71,8 +66,6 @@ const DropzoneUploadManager = (props: DropzoneUploadManagerProps) => {
|
|||
[]
|
||||
);
|
||||
|
||||
//
|
||||
|
||||
// Props change listener
|
||||
useEffect(() => {
|
||||
if (props.dropData !== dropData) {
|
||||
|
@ -102,20 +95,6 @@ const DropzoneUploadManager = (props: DropzoneUploadManagerProps) => {
|
|||
)}
|
||||
</div>
|
||||
);
|
||||
/*
|
||||
return (
|
||||
(dropData.acceptedFiles.length > 0 || dropData.rejectedFiles.length > 0) && (
|
||||
<Fragment>
|
||||
<FaExclamationTriangle className={props.managedClasses.dropzoneUploadManagerIcon} />
|
||||
<Heading tag={HeadingTag.h1} size={2}>
|
||||
{t("upload.error.title")}
|
||||
</Heading>
|
||||
<Heading tag={HeadingTag.h2} size={5}>
|
||||
{errorMessage}
|
||||
</Heading>
|
||||
</Fragment>
|
||||
)
|
||||
);*/
|
||||
};
|
||||
|
||||
export default manageJss(styles)(DropzoneUploadManager);
|
||||
|
|
|
@ -10,6 +10,41 @@ import { useMemo } from "react";
|
|||
* @param constraintsY Height of the boundary the element in question should align to
|
||||
* @returns A factor that tells by how much the element in question needs to scale-down.
|
||||
*/
|
||||
export const getScaleFactorByConstraints = (
|
||||
width: number,
|
||||
height: number,
|
||||
constraintsX: number,
|
||||
constraintsY: number
|
||||
) => {
|
||||
let newScaleFactor = 1;
|
||||
const aspectRatio = width / height;
|
||||
const adjustedConstraintsX = constraintsX;
|
||||
|
||||
if (width - adjustedConstraintsX > 0 || height - constraintsY > 0) {
|
||||
if (constraintsY * aspectRatio <= adjustedConstraintsX) {
|
||||
const adaptedWidth = width * (constraintsY / height);
|
||||
newScaleFactor = adaptedWidth / width;
|
||||
} else {
|
||||
const adaptedHeight = height * (adjustedConstraintsX / width);
|
||||
newScaleFactor = adaptedHeight / height;
|
||||
}
|
||||
}
|
||||
|
||||
return newScaleFactor;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hookified version of getScaleFactorByConstraints.
|
||||
*
|
||||
* Determines whether an element fits inside of specific constraints and returns a factor
|
||||
* that can be used to scale down the element in question if needed.
|
||||
*
|
||||
* @param width Original width of the element in question
|
||||
* @param height Original height of the element in question
|
||||
* @param constraintsX Width of the boundary the element in question should align to
|
||||
* @param constraintsY Height of the boundary the element in question should align to
|
||||
* @returns A factor that tells by how much the element in question needs to scale-down.
|
||||
*/
|
||||
export const useScaleFactor = (
|
||||
width: number,
|
||||
height: number,
|
||||
|
@ -23,25 +58,12 @@ export const useScaleFactor = (
|
|||
typeof constraintsY !== "number"
|
||||
) {
|
||||
throw new Error(
|
||||
'Hook "useWindowBreakpoint" needs four arguments: width, height, constraintsX, constraintsY'
|
||||
'Hook "useScaleFactor" requires four arguments: width, height, constraintsX, constraintsY'
|
||||
);
|
||||
}
|
||||
|
||||
return useMemo(() => {
|
||||
let newScaleFactor = 1;
|
||||
const aspectRatio = width / height;
|
||||
const adjustedConstraintsX = constraintsX;
|
||||
|
||||
if (width - adjustedConstraintsX > 0 || height - constraintsY > 0) {
|
||||
if (constraintsY * aspectRatio <= adjustedConstraintsX) {
|
||||
const adaptedWidth = width * (constraintsY / height);
|
||||
newScaleFactor = adaptedWidth / width;
|
||||
} else {
|
||||
const adaptedHeight = height * (adjustedConstraintsX / width);
|
||||
newScaleFactor = adaptedHeight / height;
|
||||
}
|
||||
}
|
||||
|
||||
return newScaleFactor;
|
||||
}, [constraintsX, constraintsY, height, width]);
|
||||
return useMemo(
|
||||
() => getScaleFactorByConstraints(width, height, constraintsX, constraintsY),
|
||||
[width, height, constraintsX, constraintsY]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
import { iconToGlyph } from "./Utils/iconToGlyph";
|
||||
import ProgressIcon from "./ProgressIcon/ProgressIcon";
|
||||
import useMotionValueFactory from "./Hooks/useMotionValueFactory";
|
||||
import { useScaleFactor } from "./Hooks/useScaleFactor";
|
||||
import { useScaleFactor, getScaleFactorByConstraints } from "./Hooks/useScaleFactor";
|
||||
import { ToastManager, toast } from "./Toasts/toast";
|
||||
import TabBar from "./Tabs/TabBar/TabBar";
|
||||
import TabViewer from "./Tabs/TabViewer/TabViewer";
|
||||
|
@ -37,6 +37,7 @@ export {
|
|||
RouteInterceptionManager,
|
||||
useRouteInterception,
|
||||
useScaleFactor,
|
||||
getScaleFactorByConstraints,
|
||||
ToastManager,
|
||||
toast,
|
||||
TabBar,
|
||||
|
|
Loading…
Reference in New Issue