Add subtitles to the player

This commit is contained in:
Zoe Roux 2022-10-06 01:00:54 +09:00
parent 9b62cb8a93
commit 6eccb2cede
10 changed files with 460 additions and 79 deletions

View File

@ -20,7 +20,7 @@ services:
- postgres
volumes:
- ./back:/app
- /app/out
- /app/out/
- kyoo:/var/lib/kyoo
- ./video:/video
front:
@ -29,7 +29,8 @@ services:
dockerfile: Dockerfile.dev
volumes:
- ./front:/app
- /app/nodes_modules
- /app/node_modules/
- /app/.next/
ports:
- "3000:3000"
restart: on-failure
@ -55,6 +56,7 @@ services:
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
volumes:
- db:/var/lib/postgresql/data

View File

@ -7,5 +7,6 @@
"mute": "Toggle mute",
"volume": "Volume",
"subtitles": "Subtitles",
"subtitle-none": "None",
"fullscreen": "Fullscreen"
}

View File

@ -7,5 +7,6 @@
"mute": "Muet",
"volume": "Volume",
"subtitles": "Sous titres",
"subtitle-none": "Aucun",
"fullscreen": "Plein-écran"
}

View File

@ -18,6 +18,8 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
const CopyPlugin = require("copy-webpack-plugin");
/**
* @type {import("next").NextConfig}
*/
@ -25,6 +27,21 @@ const nextConfig = {
reactStrictMode: true,
swcMinify: true,
output: "standalone",
webpack: (config) => {
config.plugins = [
...config.plugins,
new CopyPlugin({
patterns: [
{
context: "node_modules/@jellyfin/libass-wasm/dist/js/",
from: "*",
to: "static/chunks/",
},
],
}),
];
return config;
},
async redirects() {
return [
{

View File

@ -23,6 +23,7 @@
"dependencies": {
"@emotion/react": "^11.9.3",
"@emotion/styled": "^11.9.3",
"@jellyfin/libass-wasm": "^4.1.1",
"@mui/icons-material": "^5.8.4",
"@mui/material": "^5.8.7",
"next": "12.2.2",
@ -38,6 +39,7 @@
"@types/node": "18.0.3",
"@types/react": "18.0.15",
"@types/react-dom": "18.0.6",
"copy-webpack-plugin": "^11.0.0",
"eslint": "8.19.0",
"eslint-config-next": "12.2.2",
"eslint-config-prettier": "^8.5.0",

View File

@ -25,7 +25,11 @@ import { Link } from "~/utils/link";
import { Image } from "./poster";
export const episodeDisplayNumber = (
episode: { seasonNumber?: number; episodeNumber?: number; absoluteNumber?: number },
episode: {
seasonNumber?: number | null;
episodeNumber?: number | null;
absoluteNumber?: number | null;
},
def?: string,
) => {
if (typeof episode.seasonNumber === "number" && typeof episode.episodeNumber === "number")

View File

@ -60,6 +60,7 @@ export const TrackP = ResourceP.extend({
*/
displayName: z.string(),
});
export type Track = z.infer<typeof TrackP>;
export const ChapterP = z.object({
/**

View File

@ -228,6 +228,78 @@ const Item = ({ item, layout }: { item?: LibraryItem; layout: Layout }) => {
}
};
const SortByMenu = ({
sortKey,
setSort,
sortOrd,
setSortOrd,
anchor,
onClose,
}: {
sortKey: SortBy;
setSort: (sort: SortBy) => void;
sortOrd: SortOrd;
setSortOrd: (sort: SortOrd) => void;
anchor: HTMLElement;
onClose: () => void;
}) => {
const router = useRouter();
const { t } = useTranslation("browse");
return (
<Menu
id="sortby-menu"
MenuListProps={{
"aria-labelledby": "sortby",
}}
anchorEl={anchor}
open={!!anchor}
onClose={onClose}
>
{Object.values(SortBy).map((x) => (
<MenuItem
key={x}
selected={sortKey === x}
onClick={() => setSort(x)}
component={Link}
to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }}
shallow
replace
>
<ListItemText>{t(`browse.sortkey.${x}`)}</ListItemText>
</MenuItem>
))}
<Divider />
<MenuItem
selected={sortOrd === SortOrd.Asc}
onClick={() => setSortOrd(SortOrd.Asc)}
component={Link}
to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }}
shallow
replace
>
<ListItemIcon>
<South fontSize="small" />
</ListItemIcon>
<ListItemText>{t("browse.sortord.asc")}</ListItemText>
</MenuItem>
<MenuItem
selected={sortOrd === SortOrd.Desc}
onClick={() => setSortOrd(SortOrd.Desc)}
component={Link}
to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }}
shallow
replace
>
<ListItemIcon>
<North fontSize="small" />
</ListItemIcon>
<ListItemText>{t("browse.sortord.desc")}</ListItemText>
</MenuItem>
</Menu>
);
};
const BrowseSettings = ({
sortKey,
setSort,
@ -244,7 +316,6 @@ const BrowseSettings = ({
setLayout: (layout: Layout) => void;
}) => {
const [sortAnchor, setSortAnchor] = useState<HTMLElement | null>(null);
const router = useRouter();
const { t } = useTranslation("browse");
const switchViewTitle = layout === Layout.Grid
@ -265,7 +336,7 @@ const BrowseSettings = ({
aria-controls={sortAnchor ? "sorby-menu" : undefined}
aria-haspopup="true"
aria-expanded={sortAnchor ? "true" : undefined}
onClick={(event: MouseEvent<HTMLElement>) => setSortAnchor(event.currentTarget)}
onClick={(event) => setSortAnchor(event.currentTarget)}
>
<Sort />
{t("browse.sortby", { key: t(`browse.sortkey.${sortKey}`) })}
@ -282,56 +353,16 @@ const BrowseSettings = ({
</Tooltip>
</ButtonGroup>
</Box>
<Menu
id="sortby-menu"
MenuListProps={{
"aria-labelledby": "sortby",
}}
anchorEl={sortAnchor}
open={!!sortAnchor}
onClose={() => setSortAnchor(null)}
>
{Object.values(SortBy).map((x) => (
<MenuItem
key={x}
selected={sortKey === x}
onClick={() => setSort(x)}
component={Link}
to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }}
shallow
replace
>
<ListItemText>{t(`browse.sortkey.${x}`)}</ListItemText>
</MenuItem>
))}
<Divider />
<MenuItem
selected={sortOrd === SortOrd.Asc}
onClick={() => setSortOrd(SortOrd.Asc)}
component={Link}
to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }}
shallow
replace
>
<ListItemIcon>
<South fontSize="small" />
</ListItemIcon>
<ListItemText>{t("browse.sortord.asc")}</ListItemText>
</MenuItem>
<MenuItem
selected={sortOrd === SortOrd.Desc}
onClick={() => setSortOrd(SortOrd.Desc)}
component={Link}
to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }}
shallow
replace
>
<ListItemIcon>
<North fontSize="small" />
</ListItemIcon>
<ListItemText>{t("browse.sortord.desc")}</ListItemText>
</MenuItem>
</Menu>
{sortAnchor && (
<SortByMenu
sortKey={sortKey}
sortOrd={sortOrd}
setSort={setSort}
setSortOrd={setSortOrd}
anchor={sortAnchor}
onClose={() => setSortAnchor(null)}
/>
)}
</>
);
};

View File

@ -20,10 +20,18 @@
import { QueryIdentifier, QueryPage } from "~/utils/query";
import { withRoute } from "~/utils/router";
import { WatchItem, WatchItemP, Chapter } from "~/models/resources/watch-item";
import { WatchItem, WatchItemP, Chapter, Track } from "~/models/resources/watch-item";
import { useFetch } from "~/utils/query";
import { ErrorPage } from "~/components/errors";
import { useState, useRef, useEffect, HTMLProps, memo, useMemo, useCallback } from "react";
import {
useState,
useRef,
useEffect,
memo,
useMemo,
useCallback,
RefObject,
} from "react";
import {
Box,
CircularProgress,
@ -32,6 +40,10 @@ import {
Typography,
Skeleton,
Slider,
Menu,
MenuItem,
ListItemText,
BoxProps,
} from "@mui/material";
import useTranslation from "next-translate/useTranslation";
import {
@ -50,7 +62,11 @@ import {
} from "@mui/icons-material";
import { Poster } from "~/components/poster";
import { episodeDisplayNumber } from "~/components/episode";
import { Link } from "~/utils/link";
import NextLink from "next/link";
import { useRouter } from "next/router";
// @ts-ignore
import SubtitleOctopus from "@jellyfin/libass-wasm"
const toTimerString = (timer: number, duration?: number) => {
if (!duration) duration = timer;
@ -58,6 +74,74 @@ const toTimerString = (timer: number, duration?: number) => {
return new Date(timer * 1000).toISOString().substring(14, 19);
};
const SubtitleMenu = ({
subtitles,
setSubtitle,
selectedID,
anchor,
onClose,
}: {
subtitles: Track[];
setSubtitle: (subtitle: Track | null) => void;
selectedID?: number;
anchor: HTMLElement;
onClose: () => void;
}) => {
const router = useRouter();
const { t } = useTranslation("player");
const { subtitle, ...queryWithoutSubs } = router.query;
return (
<Menu
id="subtitle-menu"
MenuListProps={{
"aria-labelledby": "subtitle",
}}
anchorEl={anchor}
open={!!anchor}
onClose={onClose}
anchorOrigin={{
vertical: "top",
horizontal: "center",
}}
transformOrigin={{
vertical: "bottom",
horizontal: "center",
}}
>
<MenuItem
selected={!selectedID}
onClick={() => {
setSubtitle(null);
onClose();
}}
component={Link}
to={{ query: queryWithoutSubs }}
shallow
replace
>
<ListItemText>{t("subtitle-none")}</ListItemText>
</MenuItem>
{subtitles.map((sub) => (
<MenuItem
key={sub.id}
selected={selectedID == sub.id}
onClick={() => {
setSubtitle(sub);
onClose();
}}
component={Link}
to={{ query: { ...router.query, subtitle: sub.language ?? sub.id } }}
shallow
replace
>
<ListItemText>{sub.displayName}</ListItemText>
</MenuItem>
))}
</Menu>
);
};
const LoadingIndicator = () => {
return (
<Box
@ -304,24 +388,50 @@ const LeftButtons = memo(function LeftButtons({
const RightButtons = memo(function RightButton({
isFullscreen,
toggleFullscreen,
subtitles,
selectedSubtitle,
selectSubtitle,
}: {
isFullscreen: boolean;
toggleFullscreen: () => void;
subtitles?: Track[];
selectedSubtitle: Track | null;
selectSubtitle: (track: Track | null) => void;
}) {
const { t } = useTranslation("player");
const [subtitleAnchor, setSubtitleAnchor] = useState<HTMLButtonElement | null>(null);
return (
<Box sx={{ "> *": { mx: "8px !important" } }}>
<Tooltip title={t("subtitles")}>
<IconButton aria-label={t("subtitles")} sx={{ color: "white" }}>
<ClosedCaption />
</IconButton>
</Tooltip>
<Box sx={{ "> *": { m: "8px !important" } }}>
{subtitles && (
<Tooltip title={t("subtitles")}>
<IconButton
id="sortby"
aria-label={t("subtitles")}
aria-controls={subtitleAnchor ? "subtitle-menu" : undefined}
aria-haspopup="true"
aria-expanded={subtitleAnchor ? "true" : undefined}
onClick={(event) => setSubtitleAnchor(event.currentTarget)}
sx={{ color: "white" }}
>
<ClosedCaption />
</IconButton>
</Tooltip>
)}
<Tooltip title={t("fullscreen")}>
<IconButton onClick={toggleFullscreen} aria-label={t("fullscreen")} sx={{ color: "white" }}>
{isFullscreen ? <FullscreenExit /> : <Fullscreen />}
</IconButton>
</Tooltip>
{subtitleAnchor && (
<SubtitleMenu
subtitles={subtitles!}
anchor={subtitleAnchor}
setSubtitle={selectSubtitle}
selectedID={selectedSubtitle?.id}
onClose={() => setSubtitleAnchor(null)}
/>
)}
</Box>
);
});
@ -356,6 +466,67 @@ const Back = memo(function Back({ name, href }: { name?: string; href: string })
);
});
const useSubtitleController = (player: RefObject<HTMLVideoElement>): [Track | null, (value: Track | null) => void] => {
const [selectedSubtitle, setSubtitle] = useState<Track | null>(null);
const [htmlTrack, setHtmlTrack] = useState<HTMLTrackElement | null>(null);
const [subocto, setSubOcto] = useState<SubtitleOctopus | null>(null);
return [
selectedSubtitle,
useCallback(
(value: Track | null) => {
const removeHtmlSubtitle = () => {
if (htmlTrack) htmlTrack.remove();
setHtmlTrack(null);
};
const removeOctoSub = () => {
if (subocto) {
subocto.freeTrack();
subocto.dispose();
}
setSubOcto(null);
};
if (!player.current) return;
setSubtitle(value);
if (!value) {
removeHtmlSubtitle();
removeOctoSub();
} else if (value.codec === "vtt" || value.codec === "srt") {
removeOctoSub();
const track: HTMLTrackElement = htmlTrack ?? document.createElement("track");
track.kind = "subtitles";
track.label = value.displayName;
if (value.language) track.srclang = value.language;
track.src = `subtitle/${value.slug}.vtt`;
track.className = "subtitle_container";
track.default = true;
track.onload = () => {
if (player.current) player.current.textTracks[0].mode = "showing";
};
player.current.appendChild(track);
setHtmlTrack(track);
} else if (value.codec === "ass") {
removeHtmlSubtitle();
removeOctoSub();
setSubOcto(
new SubtitleOctopus({
video: player.current,
subUrl: `/api/subtitle/${value.slug}`,
workerUrl: "/_next/static/chunks/subtitles-octopus-worker.js",
legacyWorkerUrl: "/_next/static/chunks/subtitles-octopus-worker-legacy.js",
/* fonts: */
renderMode: "wasm-blend",
}),
);
}
},
[htmlTrack, subocto, player],
),
];
};
const useVideoController = () => {
const player = useRef<HTMLVideoElement>(null);
const [isPlaying, setPlay] = useState(true);
@ -366,6 +537,7 @@ const useVideoController = () => {
const [volume, setVolume] = useState(100);
const [isMuted, setMute] = useState(false);
const [isFullscreen, setFullscreen] = useState(false);
const [selectedSubtitle, selectSubtitle] = useSubtitleController(player);
useEffect(() => {
if (!player?.current?.duration) return;
@ -373,7 +545,7 @@ const useVideoController = () => {
}, [player]);
const togglePlay = useCallback(() => {
if (!player?.current) return;
if (!player.current) return;
if (!isPlaying) {
player.current.play();
} else {
@ -390,7 +562,7 @@ const useVideoController = () => {
}
}, [isFullscreen]);
const videoProps: HTMLProps<HTMLVideoElement> = useMemo(
const videoProps: BoxProps<"video"> = useMemo(
() => ({
ref: player,
onClick: togglePlay,
@ -418,7 +590,17 @@ const useVideoController = () => {
[player, togglePlay, toggleFullscreen],
);
return {
state: { isPlaying, isLoading, progress, duration, buffered, volume, isMuted, isFullscreen },
state: {
isPlaying,
isLoading,
progress,
duration,
buffered,
volume,
isMuted,
isFullscreen,
selectedSubtitle,
},
videoProps,
togglePlay,
toggleMute: useCallback(() => {
@ -439,6 +621,7 @@ const useVideoController = () => {
},
[player],
),
selectSubtitle,
};
};
@ -447,19 +630,31 @@ const query = (slug: string): QueryIdentifier<WatchItem> => ({
parser: WatchItemP,
});
//
// Callback used to hide the controls when the mouse goes iddle. This is stored globally to clear the old timeout
// if the mouse moves again
let mouseCallback: NodeJS.Timeout;
const Player: QueryPage<{ slug: string }> = ({ slug }) => {
const { data, error } = useFetch(query(slug));
const {
state: { isPlaying, isLoading, progress, duration, buffered, volume, isMuted, isFullscreen },
state: {
isPlaying,
isLoading,
progress,
duration,
buffered,
volume,
isMuted,
isFullscreen,
selectedSubtitle,
},
videoProps,
togglePlay,
toggleMute,
toggleFullscreen,
setProgress,
setVolume,
selectSubtitle,
} = useVideoController();
const [showHover, setHover] = useState(false);
const [mouseMoved, setMouseMoved] = useState(false);
@ -494,7 +689,7 @@ const Player: QueryPage<{ slug: string }> = ({ slug }) => {
<Box
component="video"
src={data?.link.direct}
{...(videoProps as any)}
{...videoProps}
sx={{
position: "absolute",
top: 0,
@ -511,15 +706,19 @@ const Player: QueryPage<{ slug: string }> = ({ slug }) => {
<Box
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
sx={ displayControls ? {
visibility: "visible",
opacity: 1,
transition: "opacity .2s ease-in",
} : {
visibility: "hidden",
opacity: 0,
transition: "opacity .4s ease-out, visibility 0s .4s",
}}
sx={
displayControls
? {
visibility: "visible",
opacity: 1,
transition: "opacity .2s ease-in",
}
: {
visibility: "hidden",
opacity: 0,
transition: "opacity .4s ease-out, visibility 0s .4s",
}
}
>
<Back
name={data?.name}
@ -566,7 +765,13 @@ const Player: QueryPage<{ slug: string }> = ({ slug }) => {
{toTimerString(progress, duration)} : {toTimerString(duration)}
</Typography>
</Box>
<RightButtons isFullscreen={isFullscreen} toggleFullscreen={toggleFullscreen} />
<RightButtons
isFullscreen={isFullscreen}
toggleFullscreen={toggleFullscreen}
subtitles={data?.subtitles}
selectedSubtitle={selectedSubtitle}
selectSubtitle={selectSubtitle}
/>
</Box>
</Box>
</Box>

View File

@ -195,6 +195,11 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
"@jellyfin/libass-wasm@^4.1.1":
version "4.1.1"
resolved "https://registry.yarnpkg.com/@jellyfin/libass-wasm/-/libass-wasm-4.1.1.tgz#d1c0e789844e1ad5d3b36acaeb7351e59f5b7d9a"
integrity sha512-xQVJw+lZUg4U1TmLS80reBECfPtpCgRF8hhUSvUUQM9g68OvINyUU3K2yqRH+8tomGpghiRaIcr/bUJ83e0veA==
"@mui/base@5.0.0-alpha.88":
version "5.0.0-alpha.88"
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.88.tgz#0930d1849c74ba62a28ab2d8533de88764173ba4"
@ -404,6 +409,11 @@
dependencies:
"@types/ms" "*"
"@types/json-schema@^7.0.9":
version "7.0.11"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
"@types/json5@^0.0.29":
version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
@ -535,6 +545,20 @@ acorn@^8.7.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30"
integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==
ajv-formats@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520"
integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==
dependencies:
ajv "^8.0.0"
ajv-keywords@^5.0.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16"
integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==
dependencies:
fast-deep-equal "^3.1.3"
ajv@^6.10.0, ajv@^6.12.4:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
@ -545,6 +569,16 @@ ajv@^6.10.0, ajv@^6.12.4:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ajv@^8.0.0, ajv@^8.8.0:
version "8.11.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f"
integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==
dependencies:
fast-deep-equal "^3.1.1"
json-schema-traverse "^1.0.0"
require-from-string "^2.0.2"
uri-js "^4.2.2"
ansi-regex@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
@ -774,6 +808,18 @@ copy-anything@^3.0.2:
dependencies:
is-what "^4.1.6"
copy-webpack-plugin@^11.0.0:
version "11.0.0"
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz#96d4dbdb5f73d02dd72d0528d1958721ab72e04a"
integrity sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==
dependencies:
fast-glob "^3.2.11"
glob-parent "^6.0.1"
globby "^13.1.1"
normalize-path "^3.0.0"
schema-utils "^4.0.0"
serialize-javascript "^6.0.0"
core-js-pure@^3.20.2:
version "3.23.4"
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.23.4.tgz#aba5c7fb297063444f6bf93afb0362151679a012"
@ -1180,6 +1226,17 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-glob@^3.2.11:
version "3.2.12"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
glob-parent "^5.1.2"
merge2 "^1.3.0"
micromatch "^4.0.4"
fast-glob@^3.2.9:
version "3.2.11"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9"
@ -1351,6 +1408,17 @@ globby@^11.1.0:
merge2 "^1.4.1"
slash "^3.0.0"
globby@^13.1.1:
version "13.1.2"
resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.2.tgz#29047105582427ab6eca4f905200667b056da515"
integrity sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==
dependencies:
dir-glob "^3.0.1"
fast-glob "^3.2.11"
ignore "^5.2.0"
merge2 "^1.4.1"
slash "^4.0.0"
has-bigints@^1.0.1, has-bigints@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
@ -1580,6 +1648,11 @@ json-schema-traverse@^0.4.1:
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
json-schema-traverse@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
json-stable-stringify-without-jsonify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
@ -1981,6 +2054,11 @@ next@12.2.2:
"@next/swc-win32-ia32-msvc" "12.2.2"
"@next/swc-win32-x64-msvc" "12.2.2"
normalize-path@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@ -2183,6 +2261,13 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
dependencies:
safe-buffer "^5.1.0"
react-dom@18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
@ -2260,6 +2345,11 @@ remove-accents@0.4.2:
resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5"
integrity sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==
require-from-string@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
resolve-from@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
@ -2309,6 +2399,11 @@ sade@^1.7.3:
dependencies:
mri "^1.1.0"
safe-buffer@^5.1.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
@ -2321,6 +2416,16 @@ scheduler@^0.23.0:
dependencies:
loose-envify "^1.1.0"
schema-utils@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.0.0.tgz#60331e9e3ae78ec5d16353c467c34b3a0a1d3df7"
integrity sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==
dependencies:
"@types/json-schema" "^7.0.9"
ajv "^8.8.0"
ajv-formats "^2.1.1"
ajv-keywords "^5.0.0"
semver@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
@ -2333,6 +2438,13 @@ semver@^7.3.7:
dependencies:
lru-cache "^6.0.0"
serialize-javascript@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8"
integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==
dependencies:
randombytes "^2.1.0"
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@ -2359,6 +2471,11 @@ slash@^3.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
slash@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7"
integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==
source-map-js@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"