Feat: use ArtPlayer, support playlist and subtitles

This commit is contained in:
HFO4 2022-04-18 17:50:21 +08:00
parent 84b05ddd66
commit c0de15e49e
13 changed files with 406 additions and 204 deletions

View File

@ -13,9 +13,9 @@
"@types/node": "^14.0.1",
"@types/react": "^16.9.35",
"@types/react-dom": "^16.9.8",
"@types/streamsaver": "^2.0.1",
"@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0",
"@types/streamsaver": "^2.0.1",
"axios": "^0.21.1",
"babel-eslint": "10.0.3",
"babel-jest": "^24.9.0",
@ -93,6 +93,7 @@
"sass-loader": "7.2.0",
"semver": "6.3.0",
"streamsaver": "^2.0.6",
"artplayer": "^4.3.4",
"style-loader": "1.0.0",
"terser-webpack-plugin": "1.4.1",
"timeago-react": "^3.0.0",

View File

@ -4,7 +4,6 @@ import { makeStyles } from "@material-ui/core/styles";
import { useLocation, useParams, useRouteMatch } from "react-router";
import API from "../../middleware/Api";
import { useDispatch } from "react-redux";
import { changeSubTitle } from "../../redux/viewUpdate/action";
import pathHelper from "../../utils/page";
import SaveButton from "../Dial/Save";
import { codePreviewSuffix } from "../../config";
@ -16,6 +15,8 @@ import Switch from "@material-ui/core/Switch";
import MenuItem from "@material-ui/core/MenuItem";
import Divider from "@material-ui/core/Divider";
import { toggleSnackbar } from "../../redux/explorer";
import UseFileSubTitle from "../../hooks/fileSubtitle";
const MonacoEditor = React.lazy(() =>
import(/* webpackChunkName: "codeEditor" */ "react-monaco-editor")
);
@ -65,12 +66,9 @@ export default function CodeViewer() {
const query = useQuery();
const { id } = useParams();
const theme = useTheme();
UseFileSubTitle(query, math, location);
const dispatch = useDispatch();
const SetSubTitle = useCallback(
(title) => dispatch(changeSubTitle(title)),
[dispatch]
);
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
@ -78,18 +76,10 @@ export default function CodeViewer() {
);
useEffect(() => {
if (!pathHelper.isSharePage(location.pathname)) {
const path = query.get("p").split("/");
const extension = query.get("p").split(".");
setSuffix(codePreviewSuffix[extension.pop()]);
SetSubTitle(path[path.length - 1]);
} else {
const extension = query.get("name").split(".");
setSuffix(codePreviewSuffix[extension.pop()]);
SetSubTitle(query.get("name"));
}
const extension = query.get("p").split(".");
setSuffix(codePreviewSuffix[extension.pop()]);
// eslint-disable-next-line
}, [math.params[0], location]);
}, []);
useEffect(() => {
let requestURL = "/file/content/" + query.get("id");
@ -142,11 +132,18 @@ export default function CodeViewer() {
<Paper className={classes.root} elevation={1}>
<div className={classes.toobar}>
<FormControl className={classes.formControl}>
<FormControlLabel control={
<Switch
onChange={(e) => setWordWrap(e.target.checked ? "on" : "off")}
/>
} label="自动换行" />
<FormControlLabel
control={
<Switch
onChange={(e) =>
setWordWrap(
e.target.checked ? "on" : "off"
)
}
/>
}
label="自动换行"
/>
</FormControl>
<FormControl className={classes.formControl}>
<Select

View File

@ -3,9 +3,9 @@ import { makeStyles } from "@material-ui/core/styles";
import { useLocation, useParams, useRouteMatch } from "react-router";
import API from "../../middleware/Api";
import { useDispatch } from "react-redux";
import { changeSubTitle } from "../../redux/viewUpdate/action";
import pathHelper from "../../utils/page";
import { toggleSnackbar } from "../../redux/explorer";
import UseFileSubTitle from "../../hooks/fileSubtitle";
const useStyles = makeStyles(() => ({
layout: {
@ -30,30 +30,16 @@ export default function DocViewer() {
const location = useLocation();
const query = useQuery();
const { id } = useParams();
UseFileSubTitle(query, math, location);
const dispatch = useDispatch();
const SetSubTitle = useCallback(
(title) => dispatch(changeSubTitle(title)),
[dispatch]
);
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
if (!pathHelper.isSharePage(location.pathname)) {
const path = query.get("p").split("/");
SetSubTitle(path[path.length - 1]);
} else {
SetSubTitle(query.get("name"));
}
// eslint-disable-next-line
}, [math.params[0], location]);
useEffect(() => {
let requestURL = "/file/doc/" + query.get("id");
if (pathHelper.isSharePage(location.pathname)) {

View File

@ -1,14 +1,15 @@
import React, { useCallback, useEffect, useState } from "react";
import React, { useCallback, useState } from "react";
import { Document, Page, pdfjs } from "react-pdf";
import { Paper } from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles";
import { useLocation, useParams, useRouteMatch } from "react-router";
import { getBaseURL } from "../../middleware/Api";
import { useDispatch } from "react-redux";
import { changeSubTitle } from "../../redux/viewUpdate/action";
import pathHelper from "../../utils/page";
import TextLoading from "../Placeholder/TextLoading";
import { toggleSnackbar } from "../../redux/explorer";
import UseFileSubTitle from "../../hooks/fileSubtitle";
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
const useStyles = makeStyles((theme) => ({
@ -44,30 +45,16 @@ export default function PDFViewer() {
const location = useLocation();
const query = useQuery();
const { id } = useParams();
UseFileSubTitle(query, math, location);
const [pageNumber, setPageNumber] = useState(1);
const dispatch = useDispatch();
const SetSubTitle = useCallback(
(title) => dispatch(changeSubTitle(title)),
[dispatch]
);
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
if (!pathHelper.isSharePage(location.pathname)) {
const path = query.get("p").split("/");
SetSubTitle(path[path.length - 1]);
} else {
SetSubTitle(query.get("name"));
}
// eslint-disable-next-line
}, [math.params[0], location]);
const removeTextLayerOffset = () => {
const textLayers = document.querySelectorAll(
".react-pdf__Page__textContent"

View File

@ -0,0 +1,54 @@
import React from "react";
import {
Icon,
ListItemIcon,
makeStyles,
Menu,
MenuItem,
} from "@material-ui/core";
import CheckIcon from "@material-ui/icons/Check";
const useStyles = makeStyles((theme) => ({
icon: {
minWidth: 38,
},
}));
export default function SelectMenu({
options,
anchorEl,
handleClose,
callback,
selected,
showIcon = true,
}) {
const classes = useStyles();
return (
<Menu
id="simple-menu"
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleClose}
>
{options.map((item) => (
<>
<MenuItem dense onClick={() => callback(item)}>
{showIcon && (
<ListItemIcon className={classes.icon}>
{item.name !== selected ? (
<Icon />
) : (
<CheckIcon />
)}
</ListItemIcon>
)}
{item.name}
</MenuItem>
</>
))}
</Menu>
);
}

View File

@ -4,12 +4,13 @@ import { makeStyles } from "@material-ui/core/styles";
import { useLocation, useParams, useRouteMatch } from "react-router";
import API from "../../middleware/Api";
import { useDispatch } from "react-redux";
import { changeSubTitle } from "../../redux/viewUpdate/action";
import Editor from "for-editor";
import SaveButton from "../Dial/Save";
import pathHelper from "../../utils/page";
import TextLoading from "../Placeholder/TextLoading";
import { toggleSnackbar } from "../../redux/explorer";
import UseFileSubTitle from "../../hooks/fileSubtitle";
const useStyles = makeStyles((theme) => ({
layout: {
width: "auto",
@ -17,9 +18,8 @@ const useStyles = makeStyles((theme) => ({
marginLeft: theme.spacing(3),
marginRight: theme.spacing(3),
[theme.breakpoints.up(1100 + theme.spacing(3) * 2)]: {
width: 1100,
marginLeft: "auto",
marginRight: "auto",
marginLeft: theme.spacing(12),
marginRight: theme.spacing(12),
},
marginBottom: 50,
},
@ -50,28 +50,15 @@ export default function TextViewer() {
const location = useLocation();
const query = useQuery();
const { id } = useParams();
UseFileSubTitle(query, math, location);
const dispatch = useDispatch();
const SetSubTitle = useCallback(
(title) => dispatch(changeSubTitle(title)),
[dispatch]
);
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
if (!pathHelper.isSharePage(location.pathname)) {
const path = query.get("p").split("/");
SetSubTitle(path[path.length - 1]);
} else {
SetSubTitle(query.get("name"));
}
// eslint-disable-next-line
}, [math.params[0], location]);
useEffect(() => {
let requestURL = "/file/content/" + query.get("id");
if (pathHelper.isSharePage(location.pathname)) {

View File

@ -1,13 +1,27 @@
import React, { useCallback, useEffect } from "react";
import DPlayer from "react-dplayer";
import { Paper } from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles";
import React, { Suspense, useCallback, useEffect, useState } from "react";
import { Button, Paper } from "@material-ui/core";
import { makeStyles, useTheme } from "@material-ui/core/styles";
import { useLocation, useParams, useRouteMatch } from "react-router";
import { getBaseURL } from "../../middleware/Api";
import { useDispatch } from "react-redux";
import { changeSubTitle } from "../../redux/viewUpdate/action";
import pathHelper from "../../utils/page";
import { isMobileSafari } from "../../utils";
import UseFileSubTitle from "../../hooks/fileSubtitle";
import { getPreviewURL } from "../../middleware/Api";
import { useHistory } from "react-router-dom";
import { basename, fileNameNoExt, isMobileSafari } from "../../utils";
import { list } from "../../services/navigate";
import { getViewerURL } from "../../redux/explorer/action";
import { subtitleSuffix, videoPreviewSuffix } from "../../config";
import { toggleSnackbar } from "../../redux/explorer";
import { pathJoin } from "../Uploader/core/utils";
import { Launch, PlaylistPlay, Subtitles } from "@material-ui/icons";
import TextLoading from "../Placeholder/TextLoading";
import SelectMenu from "./SelectMenu";
const Artplayer = React.lazy(() =>
import(
/* webpackChunkName: "artplayer" */ "artplayer/examples/react/Artplayer"
)
);
const useStyles = makeStyles((theme) => ({
layout: {
@ -24,6 +38,14 @@ const useStyles = makeStyles((theme) => ({
},
player: {
borderRadius: "4px",
height: 600,
},
actions: {
marginTop: theme.spacing(2),
},
actionButton: {
marginRight: theme.spacing(1),
marginTop: theme.spacing(1),
},
}));
@ -31,77 +53,216 @@ function useQuery() {
return new URLSearchParams(useLocation().search);
}
let dp = null;
let playing = false;
export default function VideoViewer() {
const math = useRouteMatch();
const location = useLocation();
const query = useQuery();
const { id } = useParams();
const dispatch = useDispatch();
const SetSubTitle = useCallback(
(title) => dispatch(changeSubTitle(title)),
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
if (!pathHelper.isSharePage(location.pathname)) {
const path = query.get("p").split("/");
SetSubTitle(path[path.length - 1]);
} else {
SetSubTitle(query.get("name"));
}
// eslint-disable-next-line
}, [math.params[0], location]);
const { title, path } = UseFileSubTitle(query, math, location);
const theme = useTheme();
const [art, setArt] = useState(null);
const history = useHistory();
const [files, setFiles] = useState([]);
const [subtitles, setSubtitles] = useState([]);
const [playlist, setPlaylist] = useState([]);
const [subtitleOpen, setSubtitleOpen] = useState(null);
const [subtitleSelected, setSubtitleSelected] = useState("");
const [playlistOpen, setPlaylistOpen] = useState(null);
const [externalPlayerOpen, setExternalPlayerOpen] = useState(null);
const isShare = pathHelper.isSharePage(location.pathname);
useEffect(() => {
return () => {
if (
playing &&
art !== null &&
!isMobileSafari() &&
document.pictureInPictureEnabled &&
dp
art.playing
) {
dp.video.requestPictureInPicture();
dp.video.addEventListener(
art.pip = true;
art.query(".art-video").addEventListener(
"leavepictureinpicture",
() => {
dp.video.pause();
art.pause();
},
false
);
}
};
}, []);
}, [art]);
const classes = useStyles();
useEffect(() => {
if (art !== null) {
const newURL = getPreviewURL(
isShare,
id,
query.get("id"),
query.get("share_path")
);
if (newURL !== art.url) {
if (art.subtitle) {
art.subtitle.show = false;
}
art.switchUrl(newURL);
if (path && path !== "") {
list(basename(path), isShare ? { key: id } : null, "").then(
(res) => {
setFiles(
res.data.objects.filter(
(o) => o.type === "file"
)
);
setPlaylist(
res.data.objects.filter(
(o) =>
o.type === "file" &&
videoPreviewSuffix.indexOf(
o.name
.split(".")
.pop()
.toLowerCase()
) !== -1
)
);
}
);
}
}
}
}, [art, id, location, path]);
const switchSubtitle = (f) => {
if (art !== null) {
const fileType = f.name.split(".").pop().toLowerCase();
art.subtitle.switch(
getPreviewURL(
isShare,
id,
f.id,
pathJoin([basename(query.get("share_path")), f.name])
),
{
type: fileType,
}
);
art.subtitle.show = true;
setSubtitleSelected(f.name);
ToggleSnackbar("top", "center", `字幕切换到:${f.name} `, "info");
}
};
useEffect(() => {
if (files.length > 0) {
const options = files.filter((f) => {
const fileType = f.name.split(".").pop().toLowerCase();
if (subtitleSuffix.indexOf(fileType) !== -1) {
if (fileNameNoExt(f.name) === fileNameNoExt(title)) {
switchSubtitle(f);
}
return true;
}
return false;
});
setSubtitles(options);
}
}, [files]);
const switchVideo = (file) => {
if (isShare) {
file.key = id;
}
history.push(getViewerURL("video", file, isShare));
};
const setSubtitle = (sub) => {
setSubtitleOpen(null);
switchSubtitle(sub);
};
const startSelectSubTitle = (e) => {
if (subtitles.length === 0) {
ToggleSnackbar(
"top",
"right",
`视频目录下没有可用字幕文件 (支持ASS/SRT/VTT)`,
"warning"
);
return;
}
setSubtitleOpen(e.currentTarget);
};
return (
<div className={classes.layout}>
<Paper className={classes.root} elevation={1}>
<DPlayer
onLoad={(d) => (dp = d)}
onPlay={() => (playing = true)}
onEnded={() => (playing = false)}
className={classes.player}
options={{
video: {
url:
getBaseURL() +
(pathHelper.isSharePage(location.pathname)
? "/share/preview/" +
id +
(query.get("share_path") !== ""
? "?path=" +
encodeURIComponent(
query.get("share_path")
)
: "")
: "/file/preview/" + query.get("id")),
},
}}
/>
<Suspense fallback={<TextLoading />}>
<Artplayer
option={{
title: title,
theme: theme.palette.secondary.main,
flip: true,
setting: true,
playbackRate: true,
aspectRatio: true,
hotkey: true,
pip: true,
fullscreen: true,
fullscreenWeb: true,
}}
className={classes.player}
getInstance={(a) => setArt(a)}
/>
</Suspense>
</Paper>
<div className={classes.actions}>
<Button
onClick={startSelectSubTitle}
className={classes.actionButton}
startIcon={<Subtitles />}
variant="outlined"
>
选择字幕
</Button>
{playlist.length > 0 && (
<Button
onClick={(e) => setPlaylistOpen(e.currentTarget)}
className={classes.actionButton}
startIcon={<PlaylistPlay />}
variant="outlined"
>
播放列表
</Button>
)}
<Button
className={classes.actionButton}
startIcon={<Launch />}
variant="outlined"
>
用外部播放器打开
</Button>
</div>
<SelectMenu
selected={subtitleSelected}
options={subtitles}
callback={setSubtitle}
anchorEl={subtitleOpen}
handleClose={() => setSubtitleOpen(null)}
/>
<SelectMenu
selected={title}
options={playlist}
callback={switchVideo}
anchorEl={playlistOpen}
handleClose={() => setPlaylistOpen(null)}
/>
</div>
);
}

View File

@ -16,6 +16,7 @@ export const msDocPreviewSuffix = [
"xlsx",
"xls",
];
export const subtitleSuffix = ["ass", "srt", "vrr"];
export const audioPreviewSuffix = ["mp3", "ogg", "flac"];
export const videoPreviewSuffix = ["mp4", "mkv", "webm"];
export const pdfPreviewSuffix = ["pdf"];

30
src/hooks/fileSubtitle.js Normal file
View File

@ -0,0 +1,30 @@
import { useDispatch } from "react-redux";
import { useCallback, useEffect, useState } from "react";
import { changeSubTitle } from "../redux/viewUpdate/action";
import pathHelper from "../utils/page";
export default function UseFileSubTitle(query, math, location) {
const dispatch = useDispatch();
const [title, setTitle] = useState("");
const [path, setPath] = useState("");
const SetSubTitle = useCallback(
(title) => dispatch(changeSubTitle(title)),
[dispatch]
);
useEffect(() => {
if (!pathHelper.isSharePage(location.pathname)) {
const path = query.get("p").split("/");
setPath(query.get("p"));
SetSubTitle(path[path.length - 1]);
setTitle(path[path.length - 1]);
} else {
SetSubTitle(query.get("name"));
setTitle(query.get("name"));
setPath(query.get("share_path"));
}
// eslint-disable-next-line
}, [math.params[0], location]);
return { title, path };
}

View File

@ -7,6 +7,22 @@ export const getBaseURL = () => {
return baseURL;
};
export const getPreviewURL = (
isShare: boolean,
shareID: any,
fileID: any,
path: any
): string => {
return (
getBaseURL() +
(isShare
? "/share/preview/" +
shareID +
(path !== "" ? "?path=" + encodeURIComponent(path) : "")
: "/file/preview/" + fileID)
);
};
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
const instance = axios.create({

View File

@ -360,6 +360,36 @@ export const startBatchDownload = (
};
};
export const getViewerURL = (
viewer: string,
file: any,
isShare: boolean | ""
): string => {
const previewPath = getPreviewPath(file);
if (isShare) {
return (
"/s/" +
file.key +
`/${viewer}?name=` +
encodeURIComponent(file.name) +
"&share_path=" +
previewPath
);
}
return `/${viewer}?p=` + previewPath + "&id=" + file.id;
};
export const openViewer = (
viewer: string,
file: any,
isShare: boolean | ""
) => {
return (dispatch: any, getState: any) => {
dispatch(push(getViewerURL(viewer, file, isShare)));
};
};
export const openPreview = () => {
return (dispatch: any, getState: any) => {
const {
@ -379,102 +409,27 @@ export const openPreview = () => {
}
dispatch(changeContextMenu("file", false));
const previewPath = getPreviewPath(selected[0]);
switch (isPreviewable(selected[0].name)) {
case "img":
dispatch(showImgPreivew(selected[0]));
return;
case "msDoc":
if (isShare) {
dispatch(
push(
selected[0].key +
"/doc?name=" +
encodeURIComponent(selected[0].name) +
"&share_path=" +
previewPath
)
);
return;
}
dispatch(
push("/doc?p=" + previewPath + "&id=" + selected[0].id)
);
dispatch(openViewer("doc", selected[0], isShare));
return;
case "audio":
//if (isShare) {
// dispatch(openMusicDialog());
//}else{
dispatch(showAudioPreview(selected[0]));
//}
return;
case "video":
if (isShare) {
dispatch(
push(
selected[0].key +
"/video?name=" +
encodeURIComponent(selected[0].name) +
"&share_path=" +
previewPath
)
);
return;
}
dispatch(
push("/video?p=" + previewPath + "&id=" + selected[0].id)
);
dispatch(openViewer("video", selected[0], isShare));
return;
case "pdf":
if (isShare) {
dispatch(
push(
selected[0].key +
"/pdf?name=" +
encodeURIComponent(selected[0].name) +
"&share_path=" +
previewPath
)
);
return;
}
dispatch(
push("/pdf?p=" + previewPath + "&id=" + selected[0].id)
);
dispatch(openViewer("pdf", selected[0], isShare));
return;
case "edit":
if (isShare) {
dispatch(
push(
selected[0].key +
"/text?name=" +
encodeURIComponent(selected[0].name) +
"&share_path=" +
previewPath
)
);
return;
}
dispatch(
push("/text?p=" + previewPath + "&id=" + selected[0].id)
);
dispatch(openViewer("text", selected[0], isShare));
return;
case "code":
if (isShare) {
dispatch(
push(
selected[0].key +
"/code?name=" +
encodeURIComponent(selected[0].name) +
"&share_path=" +
previewPath
)
);
return;
}
dispatch(
push("/code?p=" + previewPath + "&id=" + selected[0].id)
);
dispatch(openViewer("code", selected[0], isShare));
return;
default:
dispatch(openLoadingDialog("获取下载地址..."));

View File

@ -149,6 +149,9 @@ export function pathJoin(parts, sep) {
}
export function basename(path) {
if (!path) {
return "";
}
const pathList = path.split("/");
pathList.pop();
return pathList.join("/") === "" ? "/" : pathList.join("/");
@ -159,6 +162,10 @@ export function filename(path) {
return pathList.pop();
}
export function fileNameNoExt(filename) {
return filename.substring(0, filename.lastIndexOf(".")) || filename;
}
export function randomStr(length) {
let result = "";
const characters =

View File

@ -2357,6 +2357,14 @@ arrify@^1.0.1:
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
artplayer@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/artplayer/-/artplayer-4.3.4.tgz#ea578552530258d71f231b207c8e32c1248a3b47"
integrity sha512-Ma60cC1n4G/CFPo8zOGglRU1u/OFnJGp1hJ4fe5rWIFFsCDx3D6OvfBZDMlkRreHQt8DBW0BF/oGGM6Rq0nv1g==
dependencies:
option-validator "^2.0.6"
screenfull "^6.0.1"
asap@^2.0.6, asap@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
@ -7069,7 +7077,7 @@ kind-of@^5.0.0:
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
kind-of@^6.0.0, kind-of@^6.0.2:
kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
@ -8051,6 +8059,13 @@ optimize-css-assets-webpack-plugin@5.0.3:
cssnano "^4.1.10"
last-call-webpack-plugin "^3.0.0"
option-validator@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/option-validator/-/option-validator-2.0.6.tgz#a314dae65e26db5f948ef0ff96fc88f18bb76ed6"
integrity sha512-tmZDan2LRIRQyhUGvkff68/O0R8UmF+Btmiiz0SmSw2ng3CfPZB9wJlIjHpe/MKUZqyIZkVIXCrwr1tIN+0Dzg==
dependencies:
kind-of "^6.0.3"
optionator@^0.8.1, optionator@^0.8.3:
version "0.8.3"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
@ -10228,6 +10243,11 @@ schema-utils@^2.0.0, schema-utils@^2.0.1, schema-utils@^2.2.0:
ajv "^6.12.4"
ajv-keywords "^3.5.2"
screenfull@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-6.0.1.tgz#3b71e6f06b72d817a8d3be73c45ebe71fa8da1ce"
integrity sha512-yzQW+j4zMUBQC51xxWaoDYjxOtl8Kn+xvue3p6v/fv2pIi1jH4AldgVLU8TBfFVgH2x3VXlf3+YiA/AYIPlaew==
seamless-immutable@^7.1.3:
version "7.1.4"
resolved "https://registry.yarnpkg.com/seamless-immutable/-/seamless-immutable-7.1.4.tgz#6e9536def083ddc4dea0207d722e0e80d0f372f8"