gif conversion prototype; Toast directory cleanup
This commit is contained in:
parent
d5e4c2f808
commit
aaf2bc9859
|
@ -29,6 +29,7 @@
|
|||
"copy-webpack-plugin": "^5.1.1",
|
||||
"focus-visible": "^4.1.5",
|
||||
"framer-motion": "2.0.0-beta.75",
|
||||
"gif.js": "^0.2.0",
|
||||
"i18next": "^19.4.4",
|
||||
"i18next-browser-languagedetector": "^4.1.1",
|
||||
"i18next-xhr-backend": "^3.2.2",
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
require_once "../protected/config.php";
|
||||
require_once "../protected/db.inc.php";
|
||||
require_once "../protected/output.inc.php";
|
||||
header("Content-type: application/json");
|
||||
session_start();
|
||||
|
||||
// Request can only be GET
|
||||
|
@ -18,23 +19,93 @@ if (!isset($_SESSION["u_id"])) {
|
|||
$type = $_POST["type"];
|
||||
if (!isset($type)) error("Missing argument \"type\".", 401);
|
||||
|
||||
header("Content-type: application/json");
|
||||
switch ($type) {
|
||||
case "video-thumbnail":
|
||||
$file_id = $_POST["id"];
|
||||
if (!isset($file_id)) error("Missing argument \"id\".", 401);
|
||||
|
||||
if ($type === "video-thumbnail") {
|
||||
$file_id = $_POST["id"];
|
||||
if (!isset($file_id)) error("Missing argument \"id\".", 401);
|
||||
// Uploaded file size must not exceed 2gb
|
||||
$file = $_FILES["data"];
|
||||
if ($file["size"] > 1.342e+8 || !isset($file["size"])) {
|
||||
error("Either no file was provided or the size exceeded the predefined limit of the server.");
|
||||
}
|
||||
|
||||
// Uploaded file size must not exceed 2gb
|
||||
$file = $_FILES["data"];
|
||||
if ($file["size"] > 1.342e+8 || !isset($file["size"])) {
|
||||
error("Either no file was provided or the size exceeded the predefined limit of the server.");
|
||||
}
|
||||
generate_thumbnail($GLOBALS["upload_directory"] . $file_id . ".thumb.jpg", $file["tmp_name"], $file["type"]);
|
||||
|
||||
generate_thumbnail($GLOBALS["upload_directory"] . $file_id . ".thumb.jpg", $file["tmp_name"], $file["type"]);
|
||||
$sql = "UPDATE `" . $GLOBALS["table_prefix"] . "file_tasks` SET thumbnail=0 WHERE id=?";
|
||||
$db->request($sql, "s", $file_id);
|
||||
break;
|
||||
|
||||
$sql = "UPDATE `" . $GLOBALS["table_prefix"] . "file_tasks` SET thumbnail=0 WHERE id=?";
|
||||
$db->request($sql, "s", $file_id);
|
||||
die();
|
||||
case "video-gif-upload":
|
||||
case "video-gif-stitch":
|
||||
$file_id = $_POST["id"];
|
||||
$chunk_number = $_POST["chunkNum"];
|
||||
if (!isset($file_id)) error("Missing argument \"id\".", 401);
|
||||
if (!isset($chunk_number)) error("Missing argument \"chunkNum\".", 401);
|
||||
|
||||
$temp_dir = "../.temp";
|
||||
$chunk_path = $temp_dir . "/" . $file_id . "-";
|
||||
mkdir($temp_dir);
|
||||
|
||||
if ($type === "video-gif-upload") {
|
||||
// Uploaded file size must not exceed 2gb
|
||||
$file = $_FILES["data"];
|
||||
if ($file["size"] > 1.342e+8 || !isset($file["size"])) {
|
||||
error("Either no file was provided or the size exceeded the predefined limit of the server.");
|
||||
}
|
||||
|
||||
// Appending $chunk_number to keep chunk order
|
||||
$chunk_path = $chunk_path . $chunk_number;
|
||||
|
||||
// Check if temporary file exists in order to avoid upload collisions
|
||||
if (file_exists($chunk_path)) {
|
||||
error("File already exists; Uploading is not needed.", 423);
|
||||
}
|
||||
|
||||
if (!move_uploaded_file($file['tmp_name'], $chunk_path)) {
|
||||
error("File is either too big or no file was sent.", 500);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Stitching procedure
|
||||
// We use $chunk_number in this context as the highest chunk number
|
||||
$stitch_path = $chunk_path . "stitched";
|
||||
|
||||
if (file_exists($stitch_path)) {
|
||||
error("Stitched file already exists", 423);
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $chunk_number; $i++) {
|
||||
try {
|
||||
$chunk_i_path = $chunk_path . $i;
|
||||
$file = fopen($chunk_i_path, 'rb');
|
||||
$buff = fread($file, 1024 * 1024);
|
||||
fclose($file);
|
||||
|
||||
$final = fopen($stitch_path, 'ab');
|
||||
$write = fwrite($final, $buff);
|
||||
fclose($final);
|
||||
|
||||
unlink($chunk_i_path);
|
||||
} catch (Exception $e) {
|
||||
error("Error while stitching: " . $e);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate optimized gif from upload and delete temporary image
|
||||
// MySQL update entry happens with the next switch case, since there is no `break`
|
||||
generate_gif($GLOBALS["upload_directory"] . $file_id . ".gif", $stitch_path);
|
||||
unlink($stitch_path);
|
||||
|
||||
case "video-gif-too-big":
|
||||
$file_id = $_POST["id"];
|
||||
$sql = "UPDATE `" . $GLOBALS["table_prefix"] . "file_tasks` SET gif=0 WHERE id=?";
|
||||
$db->request($sql, "s", $file_id);
|
||||
break;
|
||||
|
||||
default:
|
||||
error("Invalid argument value for \"type\".");
|
||||
break;
|
||||
}
|
||||
|
||||
error("Invalid argument value for \"type\".");
|
||||
|
|
|
@ -20,12 +20,28 @@ if (!isset($type)) {
|
|||
error("Missing argument \"type\".", 401);
|
||||
}
|
||||
|
||||
if ($type === "video-thumbnail") {
|
||||
$sql = "SELECT id FROM `" . $GLOBALS["table_prefix"] . "file_tasks` WHERE thumbnail=1";
|
||||
$result = mysqli_query($db->con, $sql);
|
||||
header("Content-type: application/json");
|
||||
echo json_encode($result->fetch_all(MYSQLI_ASSOC));
|
||||
die();
|
||||
// A valid column in the MySQL table
|
||||
$tableColumn = "";
|
||||
|
||||
switch ($type) {
|
||||
case "video-thumbnail":
|
||||
$tableColumn = "thumbnail";
|
||||
break;
|
||||
|
||||
case "video-gif":
|
||||
$tableColumn = "gif";
|
||||
break;
|
||||
|
||||
default:
|
||||
error("Invalid argument value for \"type\".");
|
||||
break;
|
||||
}
|
||||
|
||||
error("Invalid argument value for \"type\".");
|
||||
// Due to switch, this will only be executed if a valid $reqType was assigned.
|
||||
$sql = "SELECT id FROM `" . $GLOBALS["table_prefix"] . "file_tasks` WHERE " . $tableColumn . "=1";
|
||||
$result = mysqli_query($db->con, $sql);
|
||||
|
||||
if ($result === false) error("Unexpected response from server.");
|
||||
|
||||
header("Content-type: application/json");
|
||||
echo json_encode($result->fetch_all(MYSQLI_ASSOC));
|
||||
|
|
|
@ -81,3 +81,30 @@ function generate_thumbnail(string $destination, string $image_path, string $mim
|
|||
// Setting the thumbnail_height according to the newly generated image
|
||||
return exec($GLOBALS["imagick_path"] . " identify -ping -format '%h' " . $destination);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an optimized GIF from an input image.
|
||||
* @param string $destination Destination path of the thumbnail.
|
||||
* @param string $image_path A path to the image in question.
|
||||
* @return string Width and height of the thumbnail; the width is always 200.
|
||||
*/
|
||||
function generate_gif(string $destination, string $image_path)
|
||||
{
|
||||
// Using an array to make this part more readable
|
||||
$exec_array = array(
|
||||
$GLOBALS["imagick_path"],
|
||||
"convert",
|
||||
$image_path,
|
||||
"-colorspace RGB",
|
||||
"-ordered-dither o8x8,8,8,4",
|
||||
"+map",
|
||||
$destination,
|
||||
"2>&1",
|
||||
);
|
||||
|
||||
// Combining arguments into exec string
|
||||
exec(implode(" ", $exec_array));
|
||||
|
||||
// Setting the thumbnail_height according to the newly generated image
|
||||
return exec($GLOBALS["imagick_path"] . " identify -ping -format '%wx%h' " . $destination);
|
||||
}
|
||||
|
|
|
@ -82,14 +82,16 @@ class VideoPreprocessor
|
|||
/**
|
||||
* Since we cannot access any single frame of a video with
|
||||
* plain PHP, we let the client generate the thumbnails for us.
|
||||
*
|
||||
* For that, we compile the ids of each video that needs a proper
|
||||
* thumbnail and let the client handle the rest.
|
||||
*
|
||||
* We also let the client know that the video is not available in
|
||||
* GIF form, as it still needs to be generated.
|
||||
*/
|
||||
private function add_thumbnail_generation_task()
|
||||
private function add_additional_generation_tasks()
|
||||
{
|
||||
$sql = "INSERT INTO `" . $GLOBALS["table_prefix"] . "file_tasks` (id, thumbnail) VALUEs (?,?)";
|
||||
$GLOBALS["db"]->request($sql, "si", $this->file_id, 1);
|
||||
$sql = "INSERT INTO `" . $GLOBALS["table_prefix"] . "file_tasks` (id, thumbnail, gif) VALUEs (?,?,?)";
|
||||
$GLOBALS["db"]->request($sql, "sii", $this->file_id, 1, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -102,7 +104,7 @@ class VideoPreprocessor
|
|||
throw new ErrorException("Generating temporary video thumbnail did not succeed.");
|
||||
}
|
||||
|
||||
$this->add_thumbnail_generation_task();
|
||||
$this->add_additional_generation_tasks();
|
||||
|
||||
return array(
|
||||
"file_width" => $this->file_width,
|
||||
|
|
|
@ -74,7 +74,7 @@ module.exports = {
|
|||
|
||||
/**
|
||||
* Force copy everything to build directory
|
||||
* and include getID3
|
||||
* and include getID3 & gif.js
|
||||
*/
|
||||
try {
|
||||
config.plugins.push(
|
||||
|
@ -140,6 +140,12 @@ module.exports = {
|
|||
to: "protected/getID3",
|
||||
flatten: true,
|
||||
},
|
||||
{
|
||||
from: "submodules/gif.js/",
|
||||
to: "static/js",
|
||||
ignore: ["*.old.*"],
|
||||
force: true,
|
||||
},
|
||||
])
|
||||
);
|
||||
} catch (err) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// We need to declare userData as part of the window object
|
||||
|
||||
interface Window {
|
||||
/**
|
||||
* The backend sets a global "userData" object
|
||||
|
@ -34,3 +35,4 @@ interface Window {
|
|||
}
|
||||
|
||||
declare module "react-resize-aware";
|
||||
declare module "gif.js";
|
||||
|
|
|
@ -14,6 +14,12 @@ const VideoThumbnailGenerator: LoadableComponent<{}> = loadable(() =>
|
|||
)
|
||||
);
|
||||
|
||||
const VideoGifGenerator: LoadableComponent<{}> = loadable(() =>
|
||||
import(
|
||||
/* webpackChunkName: "VideoGifGenerator" */ "../_Workers/VideoGifGenerator/VideoGifGenerator"
|
||||
)
|
||||
);
|
||||
|
||||
const styles: ComponentStyles<AppContainerClassNameContract, DesignSystem> = {
|
||||
container: {
|
||||
display: "flex",
|
||||
|
@ -34,6 +40,7 @@ const AppContainer: React.ComponentType<AppContainerProps> = ({ managedClasses }
|
|||
</Suspense>
|
||||
</Router>
|
||||
{isLoggedIn && <VideoThumbnailGenerator />}
|
||||
{/*isLoggedIn && <VideoGifGenerator />*/}
|
||||
</Background>
|
||||
);
|
||||
|
||||
|
|
|
@ -17,4 +17,5 @@ export interface ToastProps
|
|||
extends ManagedClasses<ToastClassNameContract>,
|
||||
OriginalToastProps {
|
||||
title?: string;
|
||||
progressPercentage?: number;
|
||||
}
|
|
@ -73,7 +73,12 @@ const BaseToast = (props: ToastProps) => {
|
|||
<div className={props.managedClasses.toast_element}>
|
||||
<DefaultToast {...defaultToastProps}>
|
||||
<span className={props.managedClasses.toast_title}>{props.title || title()}</span>
|
||||
<span className={props.managedClasses.toast_content}>{props.children}</span>
|
||||
<span
|
||||
className={props.managedClasses.toast_content}
|
||||
data-content={typeof props.children === "string" ? props.children : ""}
|
||||
>
|
||||
{props.children}
|
||||
</span>
|
||||
</DefaultToast>
|
||||
</div>
|
||||
);
|
|
@ -0,0 +1,267 @@
|
|||
import React, { useEffect, useCallback, useRef, useState } from "react";
|
||||
import { useToasts } from "../../_DesignSystem";
|
||||
import gifJS from "gif.js";
|
||||
import axios from "../../_interceptedAxios";
|
||||
import { Progress } from "@microsoft/fast-components-react-msft";
|
||||
|
||||
/**
|
||||
* gif.js can use a faster quantization through WASM
|
||||
*/
|
||||
const canWasm = window["WebAssembly"] !== null;
|
||||
|
||||
/**
|
||||
* Maximal size of a single GIF chunk when uploading
|
||||
*/
|
||||
const chunkSize = 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Framerate of the output GIF
|
||||
*/
|
||||
const desiredFramerate = 8;
|
||||
|
||||
/**
|
||||
* Percentage to compare current percentage to
|
||||
*/
|
||||
let lastText = "";
|
||||
|
||||
/**
|
||||
* Gif.js instance
|
||||
*/
|
||||
const gif = new gifJS({
|
||||
workerScript:
|
||||
window.location.origin +
|
||||
"/static/js/" +
|
||||
(canWasm ? "gif.worker-wasm.js" : "gif.worker.js"),
|
||||
workers: 4,
|
||||
quality: 8,
|
||||
// globalPalette: true,
|
||||
width: 0,
|
||||
height: 0,
|
||||
// dither: "Atkinson",
|
||||
// dither: false,
|
||||
repeat: 0,
|
||||
});
|
||||
|
||||
const VideoGifGenerator: React.ComponentType<{}> = props => {
|
||||
const { addToast, updateToast, removeToast } = useToasts();
|
||||
|
||||
/**
|
||||
* An array containing video IDs that need GIF equivalents.
|
||||
*/
|
||||
const taskList = useRef<{ id: string }[]>([]);
|
||||
|
||||
/**
|
||||
* Current progress with the conversion of a video
|
||||
*/
|
||||
type progressPercentage = number;
|
||||
const [progressPercentage, setProgress] = useState(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);
|
||||
const [curFileId, setCurFileId] = useState(null);
|
||||
|
||||
/**
|
||||
* Generates a gif from an already uploaded video via gif.js
|
||||
* @param fileID Already existing id for a video file.
|
||||
*/
|
||||
const generateGif = useCallback((fileID: string[8]) => {
|
||||
if (taskList.current.length <= 0) return;
|
||||
setCurFileId(fileID);
|
||||
|
||||
const videoEl = document.createElement("video");
|
||||
videoEl.style.display = "none";
|
||||
videoEl.preload = "metadata";
|
||||
videoEl.autoplay = false;
|
||||
|
||||
/**
|
||||
* Seeks video to given time, adds the frame to gif.js and progresses to
|
||||
* the next GIF frame by calling itself again.
|
||||
*
|
||||
* @param time Video time to seek to in seconds
|
||||
* @param seekSummand Number in seconds by which `time` should progress after successful seeking
|
||||
*/
|
||||
const seekVideo = (time: number, seekSummand: number) => {
|
||||
const seekHandler = () => {
|
||||
console.log("Loaded Data at " + videoEl.currentTime + "!");
|
||||
videoEl.removeEventListener("seeked", seekHandler);
|
||||
|
||||
// Setting progress for toast
|
||||
setProgress((videoEl.currentTime * 33) / videoEl.duration);
|
||||
|
||||
// Start GIF rendering if there is no frame to add anymore
|
||||
if (videoEl.currentTime >= videoEl.duration) {
|
||||
console.log("Finished at " + videoEl.currentTime + ", now rendering!");
|
||||
gif.render();
|
||||
document.body.removeChild(videoEl);
|
||||
return;
|
||||
}
|
||||
|
||||
gif.addFrame(videoEl, { copy: true, delay: seekSummand * 1000 });
|
||||
seekVideo(time + seekSummand, seekSummand);
|
||||
};
|
||||
|
||||
videoEl.addEventListener("seeked", seekHandler);
|
||||
videoEl.currentTime = time;
|
||||
};
|
||||
|
||||
videoEl.addEventListener("loadedmetadata", () => {
|
||||
console.log("Loaded Metadata!", videoEl.duration);
|
||||
const seekSummand = 1 / desiredFramerate;
|
||||
|
||||
gif.abort();
|
||||
gif.frames = [];
|
||||
gif.setOptions({
|
||||
width: videoEl.videoWidth,
|
||||
height: videoEl.videoHeight,
|
||||
});
|
||||
|
||||
seekVideo(0, seekSummand);
|
||||
});
|
||||
|
||||
// NOTE: MP4 is hardcoded!
|
||||
videoEl.src = `${window.location.origin}/${fileID}.mp4`;
|
||||
document.body.appendChild(videoEl);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Removes first object from the task list and
|
||||
* requests a GIF generation for the next item in the list.
|
||||
*/
|
||||
const shiftToNextGif = useCallback(() => {
|
||||
taskList.current.shift();
|
||||
if (taskList.current.length <= 0) return;
|
||||
generateGif(taskList.current[0].id);
|
||||
}, [generateGif]);
|
||||
|
||||
/**
|
||||
* Starts uploading procedure.
|
||||
*
|
||||
* The GIF is split into small chunks and uploaded into a
|
||||
* temporary folder. Finally, a stitching request will tell
|
||||
* the server to combine the chunks and create an optimized
|
||||
* version of the full GIF.
|
||||
*/
|
||||
const uploadGif = useCallback(
|
||||
(gifBlob: Blob) => {
|
||||
const uploadGifChunk = async (chunkNum: number) => {
|
||||
const curID = taskList.current[0].id;
|
||||
const postData = new FormData();
|
||||
|
||||
const offset = chunkNum * chunkSize;
|
||||
const blobChunk = gifBlob.slice(offset, offset + chunkSize);
|
||||
|
||||
/**
|
||||
* After uploading all chunks, we need to tell the server
|
||||
* to start stitching them together.
|
||||
*/
|
||||
const isStitchRequest = blobChunk.size === 0;
|
||||
|
||||
postData.append(
|
||||
"type",
|
||||
!isStitchRequest ? "video-gif-upload" : "video-gif-stitch"
|
||||
);
|
||||
postData.append("id", curID);
|
||||
postData.append("chunkNum", chunkNum.toString());
|
||||
postData.append("data", !isStitchRequest ? blobChunk : null);
|
||||
|
||||
try {
|
||||
await axios.post(window.location.origin + "/api/finishAdminTask.php", postData);
|
||||
|
||||
if (!isStitchRequest) uploadGifChunk(chunkNum + 1);
|
||||
else shiftToNextGif();
|
||||
} catch (err) {
|
||||
// Skip current GIF if another client is already uploading
|
||||
if (typeof err.code !== "undefined" && err.code === 423) {
|
||||
shiftToNextGif();
|
||||
return;
|
||||
}
|
||||
|
||||
addToast("error.gifUpload", { appearance: "error" });
|
||||
console.log("error.gifUpload", "\n", err.message, err, typeof err);
|
||||
}
|
||||
};
|
||||
|
||||
uploadGifChunk(0);
|
||||
},
|
||||
[addToast, shiftToNextGif]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const res = await axios.get(window.location.origin + "/api/getAdminTasks.php", {
|
||||
params: { type: "video-gif" },
|
||||
});
|
||||
|
||||
if (res.data) {
|
||||
taskList.current = res.data;
|
||||
generateGif(taskList.current[0].id);
|
||||
}
|
||||
} catch (err) {
|
||||
addToast("error.requestTaskList", { appearance: "error" });
|
||||
console.log("error.requestTaskList", "\n", err.message);
|
||||
}
|
||||
})();
|
||||
return () => {};
|
||||
}, [addToast, generateGif]);
|
||||
|
||||
useEffect(() => {
|
||||
const gifFinishHandler = uploadGif;
|
||||
const handleGifProgress = (progress: number) => {
|
||||
setProgress((progress * 100 * 33) / 100 + 33);
|
||||
};
|
||||
|
||||
gif.addListener("finished", gifFinishHandler);
|
||||
gif.addListener("progress", handleGifProgress);
|
||||
return () => {
|
||||
gif.removeListener("finished", gifFinishHandler);
|
||||
gif.removeListener("progress", handleGifProgress);
|
||||
};
|
||||
}, [uploadGif]);
|
||||
|
||||
useEffect(() => {
|
||||
let toastId: string = null;
|
||||
addToast(
|
||||
<Progress minValue={0} maxValue={100} />,
|
||||
{
|
||||
title: `Spinning up GIF generator...`,
|
||||
appearance: "info",
|
||||
autoDismiss: false,
|
||||
},
|
||||
(id: string) => {
|
||||
setToastId(id);
|
||||
toastId = id;
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
if (toastId) removeToast(toastId);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [removeToast, addToast]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!toastId || !curFileId) return;
|
||||
|
||||
let title = "";
|
||||
if (progressPercentage < 33)
|
||||
title = `Preparing ${curFileId}.mp4 to convert to GIF...`;
|
||||
if (progressPercentage >= 33) title = `Generating ${curFileId}.gif...`;
|
||||
if (progressPercentage >= 66) title = `Uploading ${curFileId}.gif...`;
|
||||
|
||||
if (title !== lastText) {
|
||||
lastText = title;
|
||||
updateToast(toastId, { title } as any);
|
||||
}
|
||||
|
||||
console.log(`${title} - ${progressPercentage}%`);
|
||||
}, [curFileId, progressPercentage, toastId, updateToast]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default VideoGifGenerator;
|
|
@ -5,14 +5,18 @@ import "./index.scss";
|
|||
import "./i18n";
|
||||
import PogodaDesignToolkitProvider from "./_DesignSystem/Toolkit/DesignSystem";
|
||||
import { ToastProvider } from "react-toast-notifications";
|
||||
import loadable from "@loadable/component";
|
||||
import Toast from "./_DesignSystem/Toast/StaticToast/Toast";
|
||||
import ToastContainer from "./_DesignSystem/Toast/ToastContainer/ToastContainer";
|
||||
// import loadable from "@loadable/component";
|
||||
|
||||
const Toast = loadable(() =>
|
||||
import(/* webpackChunkName: "Toast" */ "./_DesignSystem/Toast/Toast")
|
||||
);
|
||||
const ToastContainer = loadable(() =>
|
||||
import(/* webpackChunkName: "Toast" */ "./_DesignSystem/Toast/ToastContainer")
|
||||
);
|
||||
//const Toast = loadable(() =>
|
||||
// import(/* webpackChunkName: "Toast" */ "./_DesignSystem/Toast/StaticToast/Toast")
|
||||
//);
|
||||
//const ToastContainer = loadable(() =>
|
||||
// import(
|
||||
// /* webpackChunkName: "Toast" */ "./_DesignSystem/Toast/ToastContainer/ToastContainer"
|
||||
// )
|
||||
//);*/
|
||||
|
||||
ReactDOM.render(
|
||||
<PogodaDesignToolkitProvider>
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -5314,6 +5314,11 @@ getpass@^0.1.1:
|
|||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
|
||||
gif.js@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/gif.js/-/gif.js-0.2.0.tgz#615e6e3788850cd3a20c85fe9f09539e784903e8"
|
||||
integrity sha1-YV5uN4iFDNOiDIX+nwlTnnhJA+g=
|
||||
|
||||
glob-parent@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
|
||||
|
|
Loading…
Reference in New Issue