+ GIF Generation button in Inspector
This commit is contained in:
parent
426ac760c5
commit
0ea92244b2
|
@ -7,6 +7,8 @@
|
|||
"deleteSelected_plural": "Delete selected items",
|
||||
"selectedDeleted": "Successfully deleted {{count}} item",
|
||||
"selectedDeleted_plural": "Successfully deleted {{count}} items",
|
||||
"generate": "Generate GIF",
|
||||
"generate_plural": "Generate GIFs",
|
||||
"upload": {
|
||||
"title": "Upload a file",
|
||||
"select": "Select File",
|
||||
|
|
|
@ -8,7 +8,9 @@
|
|||
"size": "Size",
|
||||
"untitled": "-",
|
||||
"download": "Download File",
|
||||
"source": "View Original",
|
||||
"source": "Direct Link",
|
||||
"sourceVideo": "Video Link",
|
||||
"sourceGif": "GIF Link",
|
||||
"gif": {
|
||||
"alt": "Animated GIF image of the video",
|
||||
"load": "Loading GIF image...",
|
||||
|
|
|
@ -13,11 +13,11 @@ const VideoThumbnailGenerator: LoadableComponent<{}> = loadable(() =>
|
|||
)
|
||||
);
|
||||
|
||||
//const VideoGifGenerator: LoadableComponent<{}> = loadable(() =>
|
||||
// import(
|
||||
// /* webpackChunkName: "VideoGifGenerator" */ "../_Workers/VideoGifGenerator/VideoGifGenerator"
|
||||
// )
|
||||
//);
|
||||
const VideoGifGenerator: LoadableComponent<{}> = loadable(() =>
|
||||
import(
|
||||
/* webpackChunkName: "VideoGifGenerator" */ "../_Workers/VideoGifGenerator/VideoGifGenerator"
|
||||
)
|
||||
);
|
||||
|
||||
const styles: ComponentStyles<AppContainerClassNameContract, DesignSystem> = {
|
||||
container: {
|
||||
|
@ -39,7 +39,7 @@ const AppContainer: React.ComponentType<AppContainerProps> = ({ managedClasses }
|
|||
</Suspense>
|
||||
</Router>
|
||||
{isLoggedIn && <VideoThumbnailGenerator />}
|
||||
{/*isLoggedIn && <VideoGifGenerator />*/}
|
||||
{isLoggedIn && <VideoGifGenerator />}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import React, { useState } from "react";
|
||||
import { FVSidebarFooterProps } from "./FVSidebarFooter.props";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, ButtonAppearance } from "../../../_DesignSystem";
|
||||
import { FaMagic } from "react-icons/fa";
|
||||
import { generateGifFromVideo } from "../../../_Workers/VideoGifGenerator/VideoGifGeneratorEvents";
|
||||
|
||||
/**
|
||||
* Button in FVSidebar: Adds the currently insepcted video to the GIF generation queue.
|
||||
*/
|
||||
const FVSidebarConvertButton: React.ComponentType<FVSidebarFooterProps> = ({
|
||||
fileData,
|
||||
}) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const [isDisabled, setDisabledState] = useState<boolean>(false);
|
||||
|
||||
const onDelete = (e: React.MouseEvent<HTMLElement>) => {
|
||||
setDisabledState(true);
|
||||
generateGifFromVideo(fileData);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
appearance={ButtonAppearance.stealth}
|
||||
icon={FaMagic}
|
||||
onClick={onDelete}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{t("generate")}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default FVSidebarConvertButton;
|
|
@ -7,14 +7,18 @@ import {
|
|||
DesignSystem,
|
||||
neutralForegroundRest,
|
||||
neutralLayerL2,
|
||||
neutralFillStealthHover,
|
||||
} from "@microsoft/fast-components-styles-msft";
|
||||
import { ButtonClassNameContract } from "@microsoft/fast-components-class-name-contracts-msft";
|
||||
import manageJss, { ComponentStyles } from "@microsoft/fast-jss-manager-react";
|
||||
import { Button, ButtonAppearance, isLoggedIn } from "../../../_DesignSystem";
|
||||
import { FaDownload, FaExternalLinkSquareAlt } from "react-icons/fa";
|
||||
import { FaDownload, FaLink, FaImage, FaVideo } from "react-icons/fa";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Hypertext } from "@microsoft/fast-components-react-msft";
|
||||
import { designSystemContext } from "@microsoft/fast-jss-manager-react/dist/context";
|
||||
import loadable from "@loadable/component";
|
||||
import { IconType } from "react-icons/lib";
|
||||
import FVSidebarConvertButton from "./FVSidebarConvertButton";
|
||||
|
||||
const FVSidebarDeleteButton = loadable(() => import("./FVSidebarDeleteButton"));
|
||||
|
||||
|
@ -46,15 +50,15 @@ const styles: ComponentStyles<FVSidebarFooterClassNameContract, DesignSystem> =
|
|||
display: "flex",
|
||||
fontSize: "14px",
|
||||
"& > button, & > a": {
|
||||
display: "flex",
|
||||
background: "transparent",
|
||||
fontWeight: "600",
|
||||
flexGrow: "1",
|
||||
flex: "1 1 0px",
|
||||
padding: "45px 15px 45px 15px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignContent: "center",
|
||||
borderRadius: "0",
|
||||
"& > svg": {
|
||||
"& > svg, & > div": {
|
||||
marginBottom: "12px",
|
||||
marginLeft: "8px",
|
||||
width: "25px",
|
||||
|
@ -64,9 +68,43 @@ const styles: ComponentStyles<FVSidebarFooterClassNameContract, DesignSystem> =
|
|||
},
|
||||
};
|
||||
|
||||
const overlapIconButtonStyles: ComponentStyles<ButtonClassNameContract, DesignSystem> = {
|
||||
button__disabled: {},
|
||||
button: {
|
||||
"& > div": {
|
||||
position: "relative",
|
||||
"& > svg": {
|
||||
width: "25px",
|
||||
height: "25px",
|
||||
"&:last-child": {
|
||||
backgroundColor: neutralLayerL2,
|
||||
padding: "0px 2px",
|
||||
position: "absolute",
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
right: "-8px",
|
||||
bottom: "-6px",
|
||||
},
|
||||
},
|
||||
},
|
||||
"&:hover:enabled, a&:not($button__disabled):hover": {
|
||||
"& > div > svg:last-child": {
|
||||
backgroundColor: neutralFillStealthHover,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Other possible color for later use:
|
||||
// #1399dc
|
||||
|
||||
const OverlapIcon: (overlappingIcon: IconType) => IconType = Icon => props => (
|
||||
<div className={props.className}>
|
||||
<FaLink />
|
||||
<Icon />
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Footer part of FVSidebar.
|
||||
*
|
||||
|
@ -112,13 +150,28 @@ const FVSidebarFooter: React.ComponentType<FVSidebarFooterProps> = ({
|
|||
{t("download")}
|
||||
</Button>
|
||||
<Button
|
||||
jssStyleSheet={fileData.has_gif ? overlapIconButtonStyles : null}
|
||||
appearance={ButtonAppearance.stealth}
|
||||
icon={FaExternalLinkSquareAlt}
|
||||
icon={fileData.has_gif ? OverlapIcon(FaVideo) : FaLink}
|
||||
href={`${window.location.origin}/${fileData.id}.${fileData.extension}`}
|
||||
target="_blank"
|
||||
>
|
||||
{t("source")}
|
||||
{fileData.has_gif ? t("sourceVideo") : t("source")}
|
||||
</Button>
|
||||
{fileData.has_gif ? (
|
||||
<Button
|
||||
jssStyleSheet={overlapIconButtonStyles}
|
||||
appearance={ButtonAppearance.stealth}
|
||||
icon={OverlapIcon(FaImage)}
|
||||
href={`${window.location.origin}/${fileData.id}.gif`}
|
||||
target="_blank"
|
||||
>
|
||||
{t("sourceGif")}
|
||||
</Button>
|
||||
) : (
|
||||
isLoggedIn &&
|
||||
fileData.extension === "mp4" && <FVSidebarConvertButton fileData={fileData} />
|
||||
)}
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
|
|
|
@ -2,6 +2,7 @@ import React, { useEffect, useCallback, useRef, useState } from "react";
|
|||
import { toast } from "../../_DesignSystem";
|
||||
import gifJS from "gif.js";
|
||||
import axios from "../../_interceptedAxios";
|
||||
import gifGenEventEmitter from "./VideoGifGeneratorEvents";
|
||||
|
||||
/**
|
||||
* gif.js can use a faster quantization through WASM
|
||||
|
@ -39,22 +40,27 @@ const gif = new gifJS({
|
|||
|
||||
const VideoGifGenerator: React.ComponentType<{}> = props => {
|
||||
/**
|
||||
* An array containing video IDs that need GIF equivalents.
|
||||
* An array containing videos that need GIF equivalents.
|
||||
*/
|
||||
const taskList = useRef<{ id: string }[]>([]);
|
||||
const taskList = useRef<Window["fileData"][]>([]);
|
||||
|
||||
/**
|
||||
* An array containing every finished conversion in this session.
|
||||
*/
|
||||
const finishedList = useRef<Window["fileData"][]>([]);
|
||||
|
||||
/**
|
||||
* Current progress with the conversion of a video
|
||||
*/
|
||||
type progressPercentage = number;
|
||||
const [progressPercentage, setProgress] = useState(null);
|
||||
type progressPercentage = number | null;
|
||||
const [progressPercentage, setProgress] = useState<number | null>(null);
|
||||
|
||||
/**
|
||||
* ID of the toast that is used to display the progress
|
||||
* of generating and uploading a GIF
|
||||
*/
|
||||
type toastId = string;
|
||||
const [toastId, setToastId] = useState<string>(null);
|
||||
type toastId = string | null;
|
||||
const [toastId, setToastId] = useState<string | null>(null);
|
||||
const [curFileId, setCurFileId] = useState(null);
|
||||
|
||||
/**
|
||||
|
@ -123,7 +129,13 @@ const VideoGifGenerator: React.ComponentType<{}> = props => {
|
|||
*/
|
||||
const shiftToNextGif = useCallback(() => {
|
||||
taskList.current.shift();
|
||||
if (taskList.current.length <= 0) return;
|
||||
|
||||
// Reset if finished
|
||||
if (taskList.current.length <= 0) {
|
||||
setCurFileId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
generateGif(taskList.current[0].id);
|
||||
}, [generateGif]);
|
||||
|
||||
|
@ -177,8 +189,6 @@ const VideoGifGenerator: React.ComponentType<{}> = props => {
|
|||
}
|
||||
);
|
||||
|
||||
if (isStitchRequest)
|
||||
console.log("Wow, we're at chunk " + chunkNum + " / " + maxChunkAmount);
|
||||
if (!isStitchRequest) uploadGifChunk(chunkNum + 1);
|
||||
else shiftToNextGif();
|
||||
} catch (err) {
|
||||
|
@ -198,6 +208,10 @@ const VideoGifGenerator: React.ComponentType<{}> = props => {
|
|||
[shiftToNextGif]
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieve list of videos not having a GIF equivalent
|
||||
*/
|
||||
/*
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
|
@ -206,7 +220,6 @@ const VideoGifGenerator: React.ComponentType<{}> = props => {
|
|||
});
|
||||
|
||||
if (typeof res.data.length !== "undefined" && res.data.length > 0) {
|
||||
console.log(res.data);
|
||||
taskList.current = res.data;
|
||||
generateGif(taskList.current[0].id);
|
||||
}
|
||||
|
@ -217,7 +230,37 @@ const VideoGifGenerator: React.ComponentType<{}> = props => {
|
|||
})();
|
||||
return () => {};
|
||||
}, [generateGif]);
|
||||
*/
|
||||
|
||||
/**
|
||||
* Enqueuing listeners.
|
||||
*
|
||||
* We use a custom EventEmitter instance in order to communicate
|
||||
* with other components easily without using contexts and forcing
|
||||
* everything to rerender constantly.
|
||||
*/
|
||||
useEffect(() => {
|
||||
const addFileToQueue = (fileData: Window["fileData"]) => {
|
||||
// Don't enqueue a duplicate unnecessarily
|
||||
const dupe = (data: Window["fileData"]) => fileData.id === data.id;
|
||||
if (
|
||||
taskList.current.findIndex(dupe) > -1 ||
|
||||
finishedList.current.findIndex(dupe) > -1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
taskList.current.push(fileData);
|
||||
if (!curFileId) generateGif(taskList.current[0].id);
|
||||
};
|
||||
|
||||
gifGenEventEmitter.on("add", addFileToQueue);
|
||||
return () => gifGenEventEmitter.off("add", addFileToQueue);
|
||||
}, [curFileId, generateGif]);
|
||||
|
||||
/**
|
||||
* Gif.js listeners
|
||||
*/
|
||||
useEffect(() => {
|
||||
const gifFinishHandler = uploadGif;
|
||||
const handleGifProgress = (progress: number) => {
|
||||
|
@ -232,20 +275,40 @@ const VideoGifGenerator: React.ComponentType<{}> = props => {
|
|||
};
|
||||
}, [uploadGif]);
|
||||
|
||||
/**
|
||||
* Toast notification dispatcher.
|
||||
*
|
||||
* It enqueues a toast if a GIF is being generated and dismisses
|
||||
* it after every enqueued video has been processed.
|
||||
*/
|
||||
useEffect(() => {
|
||||
let toastId: string = null;
|
||||
toastId =
|
||||
toast("Spinning up GIF generator...", "", { progress: 0, type: "info" }) + "";
|
||||
setToastId(toastId);
|
||||
|
||||
// Create toast if a GIF is being generated
|
||||
if (!toastId && curFileId) {
|
||||
toastId =
|
||||
toast("Spinning up GIF generator...", "", { progress: 0, type: "info" }) + "";
|
||||
setToastId(toastId);
|
||||
}
|
||||
|
||||
// Dismiss unused toasts
|
||||
if (toastId && !curFileId) {
|
||||
toast.dismiss(toastId);
|
||||
toastId = null;
|
||||
setToastId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (toastId) toast.dismiss(toastId);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [curFileId]);
|
||||
|
||||
/**
|
||||
* Toast notification progress indicator
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!toastId || !curFileId) return;
|
||||
if (!toastId && !curFileId) return;
|
||||
|
||||
let title = "";
|
||||
if (progressPercentage < 33)
|
||||
|
@ -254,8 +317,6 @@ const VideoGifGenerator: React.ComponentType<{}> = props => {
|
|||
if (progressPercentage >= 66) title = `Uploading ${curFileId}.gif...`;
|
||||
|
||||
toast.update(toastId, { progress: progressPercentage, title });
|
||||
|
||||
console.log(`${title} - ${progressPercentage}%`);
|
||||
}, [curFileId, progressPercentage, toastId]);
|
||||
|
||||
return null;
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import EventEmitter from "onfire.js";
|
||||
|
||||
/**
|
||||
* Cross-component event event manager for gif generation
|
||||
*/
|
||||
const gifGenEventEmitter: EventEmitter = new EventEmitter();
|
||||
|
||||
/**
|
||||
* Adds a file to the gif generation queue
|
||||
* @param fileData A valid fileData object
|
||||
*/
|
||||
export const generateGifFromVideo = (fileData: Window["fileData"]) =>
|
||||
gifGenEventEmitter.emit("add", fileData);
|
||||
|
||||
export default gifGenEventEmitter;
|
Loading…
Reference in New Issue