+ GIF Generation button in Inspector

This commit is contained in:
Pogodaanton 2020-06-25 13:07:38 +02:00
parent 426ac760c5
commit 0ea92244b2
7 changed files with 198 additions and 31 deletions

View File

@ -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",

View 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...",

View File

@ -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>
);

View File

@ -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;

View File

@ -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>
);

View File

@ -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;

View File

@ -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;