mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-27 09:31:50 +01:00
Merge pull request #185 from frost768/feat/subtitle-rendering
Subtitle rendering feature added
This commit is contained in:
commit
cf2060bd32
@ -9,6 +9,7 @@
|
||||
"@react-spring/web": "^9.7.1",
|
||||
"@use-gesture/react": "^10.2.24",
|
||||
"crypto-js": "^4.1.1",
|
||||
"dompurify": "^3.0.1",
|
||||
"fscreen": "^1.2.0",
|
||||
"fuse.js": "^6.4.6",
|
||||
"hls.js": "^1.0.7",
|
||||
@ -17,6 +18,7 @@
|
||||
"json5": "^2.2.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"nanoid": "^4.0.0",
|
||||
"node-webvtt": "^1.9.4",
|
||||
"ofetch": "^1.0.0",
|
||||
"pako": "^2.1.0",
|
||||
"react": "^17.0.2",
|
||||
@ -56,6 +58,7 @@
|
||||
"@tailwindcss/line-clamp": "^0.4.2",
|
||||
"@types/chromecast-caf-sender": "^1.0.5",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/dompurify": "^2.4.0",
|
||||
"@types/fscreen": "^1.0.1",
|
||||
"@types/lodash.throttle": "^4.1.7",
|
||||
"@types/node": "^17.0.15",
|
||||
|
27
src/@types/node_webtt.d.ts
vendored
Normal file
27
src/@types/node_webtt.d.ts
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
declare module "node-webvtt" {
|
||||
interface Cue {
|
||||
identifier: string;
|
||||
start: number;
|
||||
end: number;
|
||||
text: string;
|
||||
styles: string;
|
||||
}
|
||||
interface Options {
|
||||
meta?: boolean;
|
||||
strict?: boolean;
|
||||
}
|
||||
type ParserError = Error;
|
||||
interface ParseResult {
|
||||
valid: boolean;
|
||||
strict: boolean;
|
||||
cues: Cue[];
|
||||
errors: ParserError[];
|
||||
meta?: Map<string, string>;
|
||||
}
|
||||
interface Segment {
|
||||
duration: number;
|
||||
cues: Cue[];
|
||||
}
|
||||
function parse(text: string, options: Options): ParseResult;
|
||||
function segment(input: string, segmentLength?: number): Segment[];
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
import { mwFetch, proxiedFetch } from "@/backend/helpers/fetch";
|
||||
import { MWCaption, MWCaptionType } from "@/backend/helpers/streams";
|
||||
import toWebVTT from "srt-webvtt";
|
||||
import DOMPurify from "dompurify";
|
||||
|
||||
export const sanitize = DOMPurify.sanitize;
|
||||
export const CUSTOM_CAPTION_ID = "customCaption";
|
||||
export async function getCaptionUrl(caption: MWCaption): Promise<string> {
|
||||
if (caption.type === MWCaptionType.SRT) {
|
||||
|
@ -18,15 +18,14 @@ interface FLIXMediaBase {
|
||||
title: string;
|
||||
url: string;
|
||||
image: string;
|
||||
type: "Movie" | "TV Series";
|
||||
}
|
||||
|
||||
interface FLIXTVSerie extends FLIXMediaBase {
|
||||
type: "TV Series";
|
||||
seasons: number | null;
|
||||
}
|
||||
|
||||
interface FLIXMovie extends FLIXMediaBase {
|
||||
type: "Movie";
|
||||
releaseDate: string;
|
||||
}
|
||||
|
||||
@ -66,22 +65,28 @@ registerProvider({
|
||||
baseURL: flixHqBase,
|
||||
}
|
||||
);
|
||||
|
||||
const foundItem = searchResults.results.find((v: FLIXMediaBase) => {
|
||||
if (media.meta.type === MWMediaType.MOVIE) {
|
||||
if (v.type !== "Movie") return false;
|
||||
const movie = v as FLIXMovie;
|
||||
return (
|
||||
compareTitle(movie.title, media.meta.title) &&
|
||||
movie.releaseDate === media.meta.year
|
||||
);
|
||||
}
|
||||
const serie = v as FLIXTVSerie;
|
||||
if (serie.seasons && media.meta.seasons) {
|
||||
return (
|
||||
compareTitle(serie.title, media.meta.title) &&
|
||||
serie.seasons === media.meta.seasons.length
|
||||
);
|
||||
if (media.meta.type === MWMediaType.SERIES) {
|
||||
if (v.type !== "TV Series") return false;
|
||||
const serie = v as FLIXTVSerie;
|
||||
if (serie.seasons && media.meta.seasons) {
|
||||
return (
|
||||
compareTitle(serie.title, media.meta.title) &&
|
||||
serie.seasons === media.meta.seasons.length
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return compareTitle(serie.title, media.meta.title);
|
||||
return false;
|
||||
});
|
||||
if (!foundItem) throw new Error("No watchable item found");
|
||||
const flixId = foundItem.id;
|
||||
@ -110,6 +115,7 @@ registerProvider({
|
||||
)?.id;
|
||||
}
|
||||
if (!episodeId) throw new Error("No watchable item found");
|
||||
|
||||
const watchInfo = await proxiedFetch<any>("/watch", {
|
||||
baseURL: flixHqBase,
|
||||
params: {
|
||||
|
@ -39,6 +39,7 @@ export enum Icons {
|
||||
GEAR = "gear",
|
||||
WATCH_PARTY = "watch_party",
|
||||
PICTURE_IN_PICTURE = "pictureInPicture",
|
||||
CHECKMARK = "checkmark",
|
||||
}
|
||||
|
||||
export interface IconProps {
|
||||
@ -85,6 +86,7 @@ const iconList: Record<Icons, string> = {
|
||||
gear: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M481.9 166.6c3.2 8.7 .5 18.4-6.4 24.6l-30.9 28.1c-7.7 7.1-11.4 17.5-10.9 27.9c.1 2.9 .2 5.8 .2 8.8s-.1 5.9-.2 8.8c-.5 10.5 3.1 20.9 10.9 27.9l30.9 28.1c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-39.7-12.6c-10-3.2-20.8-1.1-29.7 4.6c-4.9 3.1-9.9 6.1-15.1 8.7c-9.3 4.8-16.5 13.2-18.8 23.4l-8.9 40.7c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-8.9-40.7c-2.2-10.2-9.5-18.6-18.8-23.4c-5.2-2.7-10.2-5.6-15.1-8.7c-8.8-5.7-19.7-7.8-29.7-4.6L69.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l30.9-28.1c7.7-7.1 11.4-17.5 10.9-27.9c-.1-2.9-.2-5.8-.2-8.8s.1-5.9 .2-8.8c.5-10.5-3.1-20.9-10.9-27.9L8.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l39.7 12.6c10 3.2 20.8 1.1 29.7-4.6c4.9-3.1 9.9-6.1 15.1-8.7c9.3-4.8 16.5-13.2 18.8-23.4l8.9-40.7c2-9.1 9-16.3 18.2-17.8C213.3 1.2 227.5 0 242 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l8.9 40.7c2.2 10.2 9.4 18.6 18.8 23.4c5.2 2.7 10.2 5.6 15.1 8.7c8.8 5.7 19.7 7.7 29.7 4.6l39.7-12.6c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM242 336a80 80 0 1 0 0-160 80 80 0 1 0 0 160z"/></svg>`,
|
||||
watch_party: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M319.4 372c48.5-31.3 80.6-85.9 80.6-148c0-97.2-78.8-176-176-176S48 126.8 48 224c0 62.1 32.1 116.6 80.6 148c1.2 17.3 4 38 7.2 57.1l.2 1C56 395.8 0 316.5 0 224C0 100.3 100.3 0 224 0S448 100.3 448 224c0 92.5-56 171.9-136 206.1l.2-1.1c3.1-19.2 6-39.8 7.2-57zm-2.3-38.1c-1.6-5.7-3.9-11.1-7-16.2c-5.8-9.7-13.5-17-21.9-22.4c19.5-17.6 31.8-43 31.8-71.3c0-53-43-96-96-96s-96 43-96 96c0 28.3 12.3 53.8 31.8 71.3c-8.4 5.4-16.1 12.7-21.9 22.4c-3.1 5.1-5.4 10.5-7 16.2C99.8 307.5 80 268 80 224c0-79.5 64.5-144 144-144s144 64.5 144 144c0 44-19.8 83.5-50.9 109.9zM224 312c32.9 0 64 8.6 64 43.8c0 33-12.9 104.1-20.6 132.9c-5.1 19-24.5 23.4-43.4 23.4s-38.2-4.4-43.4-23.4c-7.8-28.5-20.6-99.7-20.6-132.8c0-35.1 31.1-43.8 64-43.8zm0-144a56 56 0 1 1 0 112 56 56 0 1 1 0-112z"/></svg>`,
|
||||
pictureInPicture: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" fill="currentColor" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 1.98 2 1.98h18c1.1 0 2-.88 2-1.98V5c0-1.1-.9-2-2-2zm0 16.01H3V4.98h18v14.03z"/></svg>`,
|
||||
checkmark: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" fill="currentColor" viewBox="0 0 24 24"><path d="M9 22l-10-10.598 2.798-2.859 7.149 7.473 13.144-14.016 2.909 2.806z" /></svg>`,
|
||||
};
|
||||
|
||||
function ChromeCastButton() {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Redirect, Route, Switch } from "react-router-dom";
|
||||
import { BookmarkContextProvider } from "@/state/bookmark";
|
||||
import { WatchedContextProvider } from "@/state/watched";
|
||||
import { SettingsProvider } from "@/state/settings";
|
||||
|
||||
import { NotFoundPage } from "@/views/notfound/NotFoundView";
|
||||
import { MediaView } from "@/views/media/MediaView";
|
||||
@ -17,46 +18,48 @@ import { TestView } from "@/views/developer/TestView";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<WatchedContextProvider>
|
||||
<BookmarkContextProvider>
|
||||
<BannerContextProvider>
|
||||
<Layout>
|
||||
<Switch>
|
||||
{/* functional routes */}
|
||||
<Route exact path="/v2-migration" component={V2MigrationView} />
|
||||
<Route exact path="/">
|
||||
<Redirect to={`/search/${MWMediaType.MOVIE}`} />
|
||||
</Route>
|
||||
<SettingsProvider>
|
||||
<WatchedContextProvider>
|
||||
<BookmarkContextProvider>
|
||||
<BannerContextProvider>
|
||||
<Layout>
|
||||
<Switch>
|
||||
{/* functional routes */}
|
||||
<Route exact path="/v2-migration" component={V2MigrationView} />
|
||||
<Route exact path="/">
|
||||
<Redirect to={`/search/${MWMediaType.MOVIE}`} />
|
||||
</Route>
|
||||
|
||||
{/* pages */}
|
||||
<Route exact path="/media/:media" component={MediaView} />
|
||||
<Route
|
||||
exact
|
||||
path="/media/:media/:season/:episode"
|
||||
component={MediaView}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/search/:type/:query?"
|
||||
component={SearchView}
|
||||
/>
|
||||
{/* pages */}
|
||||
<Route exact path="/media/:media" component={MediaView} />
|
||||
<Route
|
||||
exact
|
||||
path="/media/:media/:season/:episode"
|
||||
component={MediaView}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/search/:type/:query?"
|
||||
component={SearchView}
|
||||
/>
|
||||
|
||||
{/* other */}
|
||||
<Route exact path="/dev" component={DeveloperView} />
|
||||
<Route exact path="/dev/test" component={TestView} />
|
||||
<Route exact path="/dev/video" component={VideoTesterView} />
|
||||
<Route
|
||||
exact
|
||||
path="/dev/providers"
|
||||
component={ProviderTesterView}
|
||||
/>
|
||||
<Route exact path="/dev/embeds" component={EmbedTesterView} />
|
||||
<Route path="*" component={NotFoundPage} />
|
||||
</Switch>
|
||||
</Layout>
|
||||
</BannerContextProvider>
|
||||
</BookmarkContextProvider>
|
||||
</WatchedContextProvider>
|
||||
{/* other */}
|
||||
<Route exact path="/dev" component={DeveloperView} />
|
||||
<Route exact path="/dev/test" component={TestView} />
|
||||
<Route exact path="/dev/video" component={VideoTesterView} />
|
||||
<Route
|
||||
exact
|
||||
path="/dev/providers"
|
||||
component={ProviderTesterView}
|
||||
/>
|
||||
<Route exact path="/dev/embeds" component={EmbedTesterView} />
|
||||
<Route path="*" component={NotFoundPage} />
|
||||
</Switch>
|
||||
</Layout>
|
||||
</BannerContextProvider>
|
||||
</BookmarkContextProvider>
|
||||
</WatchedContextProvider>
|
||||
</SettingsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -54,3 +54,123 @@ body[data-no-select] {
|
||||
.google-cast-button:not(.casting) google-cast-launcher {
|
||||
@apply brightness-[500];
|
||||
}
|
||||
|
||||
/*generated with Input range slider CSS style generator (version 20211225)
|
||||
https://toughengineer.github.io/demo/slider-styler*/
|
||||
:root {
|
||||
--slider-height: 0.25rem;
|
||||
--slider-border-radius: 1em;
|
||||
--slider-progress-background: #8652bb;
|
||||
}
|
||||
input[type=range].styled-slider {
|
||||
height: var(--slider-height);
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
border-radius: var(--slider-border-radius);
|
||||
background: #1C161B;
|
||||
}
|
||||
|
||||
/*progress support*/
|
||||
input[type=range].styled-slider.slider-progress {
|
||||
--range: calc(var(--max) - var(--min));
|
||||
--ratio: calc((var(--value) - var(--min)) / var(--range));
|
||||
--sx: calc(0.5 * 1rem + var(--ratio) * (100% - 1rem));
|
||||
}
|
||||
|
||||
/*webkit*/
|
||||
input[type=range].styled-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: var(--slider-border-radius);
|
||||
background: #FFFFFF;
|
||||
border: none;
|
||||
box-shadow: 0 0 2px #000000;
|
||||
margin-top: calc(0.25em * 0.5 - 1rem * 0.5);
|
||||
}
|
||||
|
||||
input[type=range].styled-slider::-webkit-slider-runnable-track {
|
||||
height: var(--slider-height);
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
border-radius: var(--slider-border-radius);
|
||||
margin-bottom: 1.1em;
|
||||
}
|
||||
|
||||
input[type=range].styled-slider::-webkit-slider-thumb:hover {
|
||||
background: #DCDCDC;
|
||||
}
|
||||
|
||||
input[type=range].styled-slider.slider-progress::-webkit-slider-runnable-track {
|
||||
background: linear-gradient(var(--slider-progress-background),var(--slider-progress-background)) 0/var(--sx) 100% no-repeat, #1C161B;
|
||||
}
|
||||
|
||||
/*mozilla*/
|
||||
input[type=range].styled-slider::-moz-range-thumb {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: var(--slider-border-radius);
|
||||
background: #FFFFFF;
|
||||
border: none;
|
||||
box-shadow: 0 0 2px #000000;
|
||||
}
|
||||
|
||||
input[type=range].styled-slider::-moz-range-track {
|
||||
height: var(--slider-height);
|
||||
border: none;
|
||||
border-radius: var(--slider-border-radius);
|
||||
background: #1C161B;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
input[type=range].styled-slider::-moz-range-thumb:hover {
|
||||
background: #DCDCDC;
|
||||
}
|
||||
|
||||
input[type=range].styled-slider.slider-progress::-moz-range-track {
|
||||
background: linear-gradient(var(--slider-progress-background),var(--slider-progress-background)) 0/var(--sx) 100% no-repeat, #1C161B;
|
||||
}
|
||||
|
||||
/*ms*/
|
||||
input[type=range].styled-slider::-ms-fill-upper {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
input[type=range].styled-slider::-ms-fill-lower {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
input[type=range].styled-slider::-ms-thumb {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: var(--slider-border-radius);
|
||||
background: #FFFFFF;
|
||||
border: none;
|
||||
box-shadow: 0 0 2px #000000;
|
||||
margin-top: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input[type=range].styled-slider::-ms-track {
|
||||
height: var(--slider-height);
|
||||
border-radius: var(--slider-border-radius);
|
||||
background: #1C161B;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input[type=range].styled-slider::-ms-thumb:hover {
|
||||
background: #DCDCDC;
|
||||
}
|
||||
|
||||
input[type=range].styled-slider.slider-progress::-ms-fill-lower {
|
||||
height: var(--slider-height);
|
||||
border-radius: var(--slider-border-radius) 0 0 5px;
|
||||
margin: -undefined 0 -undefined -undefined;
|
||||
background: var(--slider-progress-background);
|
||||
border: none;
|
||||
border-right-width: 0;
|
||||
}
|
||||
|
@ -69,6 +69,13 @@
|
||||
"sources": "Sources",
|
||||
"seasons": "Seasons",
|
||||
"captions": "Captions",
|
||||
"captionPreferences": {
|
||||
"title": "Customize",
|
||||
"delay": "Delay",
|
||||
"fontSize": "Size",
|
||||
"opacity": "Opacity",
|
||||
"color": "Color"
|
||||
},
|
||||
"episode": "E{{index}} - {{title}}",
|
||||
"noCaptions": "No captions",
|
||||
"linkedCaptions": "Linked captions",
|
||||
@ -84,7 +91,8 @@
|
||||
"embeds": "Choose which video to view",
|
||||
"seasons": "Choose which season you want to watch",
|
||||
"episode": "Pick an episode",
|
||||
"captions": "Choose a subtitle language"
|
||||
"captions": "Choose a subtitle language",
|
||||
"captionPreferences": "Make subtitles look how you want it"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
|
78
src/state/settings/context.tsx
Normal file
78
src/state/settings/context.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import { useStore } from "@/utils/storage";
|
||||
import { createContext, ReactNode, useContext, useMemo } from "react";
|
||||
import { SettingsStore } from "./store";
|
||||
import { MWSettingsData } from "./types";
|
||||
|
||||
interface MWSettingsDataSetters {
|
||||
setLanguage(language: string): void;
|
||||
setCaptionDelay(delay: number): void;
|
||||
setCaptionColor(color: string): void;
|
||||
setCaptionFontSize(size: number): void;
|
||||
setCaptionBackgroundColor(backgroundColor: string): void;
|
||||
}
|
||||
type MWSettingsDataWrapper = MWSettingsData & MWSettingsDataSetters;
|
||||
const SettingsContext = createContext<MWSettingsDataWrapper>(null as any);
|
||||
export function SettingsProvider(props: { children: ReactNode }) {
|
||||
function enforceRange(min: number, value: number, max: number) {
|
||||
return Math.max(min, Math.min(value, max));
|
||||
}
|
||||
const [settings, setSettings] = useStore(SettingsStore);
|
||||
|
||||
const context: MWSettingsDataWrapper = useMemo(() => {
|
||||
const settingsContext: MWSettingsDataWrapper = {
|
||||
...settings,
|
||||
setLanguage(language) {
|
||||
setSettings((oldSettings) => {
|
||||
return {
|
||||
...oldSettings,
|
||||
language,
|
||||
};
|
||||
});
|
||||
},
|
||||
setCaptionDelay(delay: number) {
|
||||
setSettings((oldSettings) => {
|
||||
const captionSettings = oldSettings.captionSettings;
|
||||
captionSettings.delay = enforceRange(-10, delay, 10);
|
||||
const newSettings = oldSettings;
|
||||
return newSettings;
|
||||
});
|
||||
},
|
||||
setCaptionColor(color) {
|
||||
setSettings((oldSettings) => {
|
||||
const style = oldSettings.captionSettings.style;
|
||||
style.color = color;
|
||||
const newSettings = oldSettings;
|
||||
return newSettings;
|
||||
});
|
||||
},
|
||||
setCaptionFontSize(size) {
|
||||
setSettings((oldSettings) => {
|
||||
const style = oldSettings.captionSettings.style;
|
||||
style.fontSize = enforceRange(10, size, 60);
|
||||
const newSettings = oldSettings;
|
||||
return newSettings;
|
||||
});
|
||||
},
|
||||
setCaptionBackgroundColor(backgroundColor) {
|
||||
setSettings((oldSettings) => {
|
||||
const style = oldSettings.captionSettings.style;
|
||||
style.backgroundColor = backgroundColor;
|
||||
const newSettings = oldSettings;
|
||||
return newSettings;
|
||||
});
|
||||
},
|
||||
};
|
||||
return settingsContext;
|
||||
}, [settings, setSettings]);
|
||||
return (
|
||||
<SettingsContext.Provider value={context}>
|
||||
{props.children}
|
||||
</SettingsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useSettings() {
|
||||
return useContext(SettingsContext);
|
||||
}
|
||||
|
||||
export default SettingsContext;
|
1
src/state/settings/index.ts
Normal file
1
src/state/settings/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./context";
|
22
src/state/settings/store.ts
Normal file
22
src/state/settings/store.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { createVersionedStore } from "@/utils/storage";
|
||||
import { MWSettingsData } from "./types";
|
||||
|
||||
export const SettingsStore = createVersionedStore<MWSettingsData>()
|
||||
.setKey("mw-settings")
|
||||
.addVersion({
|
||||
version: 0,
|
||||
create(): MWSettingsData {
|
||||
return {
|
||||
language: "en",
|
||||
captionSettings: {
|
||||
delay: 0,
|
||||
style: {
|
||||
color: "#ffffff",
|
||||
fontSize: 25,
|
||||
backgroundColor: "#00000096",
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
})
|
||||
.build();
|
21
src/state/settings/types.ts
Normal file
21
src/state/settings/types.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export interface CaptionStyleSettings {
|
||||
color: string;
|
||||
/**
|
||||
* Range is [10, 30]
|
||||
*/
|
||||
fontSize: number;
|
||||
backgroundColor: string;
|
||||
}
|
||||
|
||||
export interface CaptionSettings {
|
||||
/**
|
||||
* Range is [-10, 10]s
|
||||
*/
|
||||
delay: number;
|
||||
style: CaptionStyleSettings;
|
||||
}
|
||||
|
||||
export interface MWSettingsData {
|
||||
language: string;
|
||||
captionSettings: CaptionSettings;
|
||||
}
|
@ -27,9 +27,10 @@ import { ReactNode, useCallback, useState } from "react";
|
||||
import { PopoutProviderAction } from "@/video/components/popouts/PopoutProviderAction";
|
||||
import { ChromecastAction } from "@/video/components/actions/ChromecastAction";
|
||||
import { CastingTextAction } from "@/video/components/actions/CastingTextAction";
|
||||
import { PictureInPictureAction } from "@/video/components/actions/PictureInPictureAction";
|
||||
import { CaptionRendererAction } from "./actions/CaptionRendererAction";
|
||||
import { SettingsAction } from "./actions/SettingsAction";
|
||||
import { DividerAction } from "./actions/DividerAction";
|
||||
import { PictureInPictureAction } from "./actions/PictureInPictureAction";
|
||||
|
||||
type Props = VideoPlayerBaseProps;
|
||||
|
||||
@ -165,6 +166,7 @@ export function VideoPlayer(props: Props) {
|
||||
</Transition>
|
||||
{show ? <PopoutProviderAction /> : null}
|
||||
</BackdropAction>
|
||||
<CaptionRendererAction isControlsShown={show} />
|
||||
{props.children}
|
||||
</VideoPlayerError>
|
||||
</>
|
||||
|
92
src/video/components/actions/CaptionRendererAction.tsx
Normal file
92
src/video/components/actions/CaptionRendererAction.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import { Transition } from "@/components/Transition";
|
||||
import { useSettings } from "@/state/settings";
|
||||
import { sanitize } from "@/backend/helpers/captions";
|
||||
import { parse, Cue } from "node-webvtt";
|
||||
import { useRef } from "react";
|
||||
import { useAsync } from "react-use";
|
||||
import { useVideoPlayerDescriptor } from "../../state/hooks";
|
||||
import { useProgress } from "../../state/logic/progress";
|
||||
import { useSource } from "../../state/logic/source";
|
||||
|
||||
function CaptionCue({ text }: { text?: string }) {
|
||||
const { captionSettings } = useSettings();
|
||||
const textWithNewlines = (text || "").replaceAll(/\r?\n/g, "<br />");
|
||||
|
||||
// https://www.w3.org/TR/webvtt1/#dom-construction-rules
|
||||
// added a <br /> for newlines
|
||||
const html = sanitize(textWithNewlines, {
|
||||
ALLOWED_TAGS: ["c", "b", "i", "u", "span", "ruby", "rt", "br"],
|
||||
ADD_TAGS: ["v", "lang"],
|
||||
ALLOWED_ATTR: ["title", "lang"],
|
||||
});
|
||||
|
||||
return (
|
||||
<p
|
||||
className="pointer-events-none mb-1 select-none rounded px-4 py-1 text-center [text-shadow:0_2px_4px_rgba(0,0,0,0.5)]"
|
||||
style={{
|
||||
...captionSettings.style,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
// its sanitised a few lines up
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: html,
|
||||
}}
|
||||
dir="auto"
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export function CaptionRendererAction({
|
||||
isControlsShown,
|
||||
}: {
|
||||
isControlsShown: boolean;
|
||||
}) {
|
||||
const descriptor = useVideoPlayerDescriptor();
|
||||
const source = useSource(descriptor).source;
|
||||
const videoTime = useProgress(descriptor).time;
|
||||
const { captionSettings } = useSettings();
|
||||
const captions = useRef<Cue[]>([]);
|
||||
|
||||
useAsync(async () => {
|
||||
const url = source?.caption?.url;
|
||||
if (url) {
|
||||
// Is there a better way?
|
||||
const result = await fetch(url);
|
||||
// Uses UTF-8 by default
|
||||
const text = await result.text();
|
||||
captions.current = parse(text, { strict: false }).cues;
|
||||
} else {
|
||||
captions.current = [];
|
||||
}
|
||||
}, [source?.caption?.url]);
|
||||
|
||||
if (!captions.current.length) return null;
|
||||
const isVisible = (start: number, end: number): boolean => {
|
||||
const delayedStart = start + captionSettings.delay;
|
||||
const delayedEnd = end + captionSettings.delay;
|
||||
return (
|
||||
Math.max(0, delayedStart) <= videoTime &&
|
||||
Math.max(0, delayedEnd) >= videoTime
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Transition
|
||||
className={[
|
||||
"pointer-events-none absolute flex w-full flex-col items-center transition-[bottom]",
|
||||
isControlsShown ? "bottom-24" : "bottom-12",
|
||||
].join(" ")}
|
||||
animation="slide-up"
|
||||
show
|
||||
>
|
||||
{captions.current.map(
|
||||
({ identifier, end, start, text }) =>
|
||||
isVisible(start, end) && (
|
||||
<CaptionCue key={identifier || `${start}-${end}`} text={text} />
|
||||
)
|
||||
)}
|
||||
</Transition>
|
||||
);
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
||||
import { useMisc } from "@/video/state/logic/misc";
|
||||
import { useSource } from "@/video/state/logic/source";
|
||||
import { setProvider, unsetStateProvider } from "@/video/state/providers/utils";
|
||||
import { createVideoStateProvider } from "@/video/state/providers/videoStateProvider";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
@ -13,7 +12,6 @@ interface Props {
|
||||
function VideoElement(props: Props) {
|
||||
const descriptor = useVideoPlayerDescriptor();
|
||||
const mediaPlaying = useMediaPlaying(descriptor);
|
||||
const source = useSource(descriptor);
|
||||
const misc = useMisc(descriptor);
|
||||
const ref = useRef<HTMLVideoElement>(null);
|
||||
|
||||
@ -44,11 +42,7 @@ function VideoElement(props: Props) {
|
||||
muted={mediaPlaying.volume === 0}
|
||||
playsInline
|
||||
className="h-full w-full"
|
||||
>
|
||||
{source.source?.caption ? (
|
||||
<track default kind="captions" src={source.source.caption.url} />
|
||||
) : null}
|
||||
</video>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -70,7 +70,6 @@ export function CaptionSelectionPopout(props: {
|
||||
const captionFile = e.target.files[0];
|
||||
setCustomCaption(captionFile);
|
||||
}
|
||||
|
||||
return (
|
||||
<FloatingView
|
||||
{...props.router.pageProps(props.prefix)}
|
||||
@ -81,6 +80,18 @@ export function CaptionSelectionPopout(props: {
|
||||
title={t("videoPlayer.popouts.captions")}
|
||||
description={t("videoPlayer.popouts.descriptions.captions")}
|
||||
goBack={() => props.router.navigate("/")}
|
||||
action={
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
props.router.navigate(`${props.prefix}/caption-settings`)
|
||||
}
|
||||
className="flex cursor-pointer items-center space-x-2 transition-colors duration-200 hover:text-white"
|
||||
>
|
||||
<span>{t("videoPlayer.popouts.captionPreferences.title")}</span>
|
||||
<Icon icon={Icons.GEAR} />
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<FloatingCardView.Content noSection>
|
||||
<PopoutSection>
|
||||
|
150
src/video/components/popouts/CaptionSettingsPopout.tsx
Normal file
150
src/video/components/popouts/CaptionSettingsPopout.tsx
Normal file
@ -0,0 +1,150 @@
|
||||
import { FloatingCardView } from "@/components/popout/FloatingCard";
|
||||
import { FloatingView } from "@/components/popout/FloatingView";
|
||||
import { useFloatingRouter } from "@/hooks/useFloatingRouter";
|
||||
import { useSettings } from "@/state/settings";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ChangeEventHandler, useEffect, useRef } from "react";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
|
||||
export type SliderProps = {
|
||||
label: string;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
value: number;
|
||||
valueDisplay?: string;
|
||||
onChange: ChangeEventHandler<HTMLInputElement>;
|
||||
};
|
||||
|
||||
export function Slider(props: SliderProps) {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
useEffect(() => {
|
||||
const e = ref.current as HTMLInputElement;
|
||||
e.style.setProperty("--value", e.value);
|
||||
e.style.setProperty("--min", e.min === "" ? "0" : e.min);
|
||||
e.style.setProperty("--max", e.max === "" ? "100" : e.max);
|
||||
e.addEventListener("input", () => e.style.setProperty("--value", e.value));
|
||||
}, [ref]);
|
||||
|
||||
return (
|
||||
<div className="mb-6 flex flex-row gap-4">
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<label className="font-bold">{props.label}</label>
|
||||
<input
|
||||
type="range"
|
||||
ref={ref}
|
||||
className="styled-slider slider-progress"
|
||||
onChange={props.onChange}
|
||||
value={props.value}
|
||||
max={props.max}
|
||||
min={props.min}
|
||||
step={props.step}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1 aspect-[2/1] h-8 rounded-sm bg-[#1C161B] pt-1">
|
||||
<div className="text-center font-bold text-white">
|
||||
{props.valueDisplay ?? props.value}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CaptionSettingsPopout(props: {
|
||||
router: ReturnType<typeof useFloatingRouter>;
|
||||
prefix: string;
|
||||
}) {
|
||||
// For now, won't add label texts to language files since options are prone to change
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
captionSettings,
|
||||
setCaptionBackgroundColor,
|
||||
setCaptionColor,
|
||||
setCaptionDelay,
|
||||
setCaptionFontSize,
|
||||
} = useSettings();
|
||||
const colors = ["#ffffff", "#00ffff", "#ffff00"];
|
||||
return (
|
||||
<FloatingView {...props.router.pageProps(props.prefix)} width={375}>
|
||||
<FloatingCardView.Header
|
||||
title={t("videoPlayer.popouts.captionPreferences.title")}
|
||||
description={t("videoPlayer.popouts.descriptions.captionPreferences")}
|
||||
goBack={() => props.router.navigate("/captions")}
|
||||
/>
|
||||
<FloatingCardView.Content>
|
||||
<Slider
|
||||
label={t("videoPlayer.popouts.captionPreferences.delay")}
|
||||
max={10}
|
||||
min={-10}
|
||||
step={0.1}
|
||||
valueDisplay={`${captionSettings.delay.toFixed(1)}s`}
|
||||
value={captionSettings.delay}
|
||||
onChange={(e) => setCaptionDelay(e.target.valueAsNumber)}
|
||||
/>
|
||||
<Slider
|
||||
label="Size"
|
||||
min={14}
|
||||
step={1}
|
||||
max={60}
|
||||
value={captionSettings.style.fontSize}
|
||||
onChange={(e) => setCaptionFontSize(e.target.valueAsNumber)}
|
||||
/>
|
||||
<Slider
|
||||
label={t("videoPlayer.popouts.captionPreferences.opacity")}
|
||||
step={1}
|
||||
min={0}
|
||||
max={255}
|
||||
valueDisplay={`${(
|
||||
(parseInt(
|
||||
captionSettings.style.backgroundColor.substring(7, 9),
|
||||
16
|
||||
) /
|
||||
255) *
|
||||
100
|
||||
).toFixed(0)}%`}
|
||||
value={parseInt(
|
||||
captionSettings.style.backgroundColor.substring(7, 9),
|
||||
16
|
||||
)}
|
||||
onChange={(e) =>
|
||||
setCaptionBackgroundColor(
|
||||
`${captionSettings.style.backgroundColor.substring(
|
||||
0,
|
||||
7
|
||||
)}${e.target.valueAsNumber.toString(16)}`
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div className="flex flex-row justify-between">
|
||||
<label className="font-bold" htmlFor="color">
|
||||
{t("videoPlayer.popouts.captionPreferences.color")}
|
||||
</label>
|
||||
<div className="flex flex-row gap-2">
|
||||
{colors.map((color) => (
|
||||
<div
|
||||
className={`flex h-8 w-8 items-center justify-center rounded transition-[background-color,transform] duration-100 hover:bg-[#1c161b79] active:scale-110 ${
|
||||
color === captionSettings.style.color ? "bg-[#1C161B]" : ""
|
||||
}`}
|
||||
onClick={() => setCaptionColor(color)}
|
||||
>
|
||||
<div
|
||||
className="h-4 w-4 cursor-pointer appearance-none rounded-full"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
}}
|
||||
/>
|
||||
<Icon
|
||||
className={[
|
||||
"absolute text-xs text-[#1C161B]",
|
||||
color === captionSettings.style.color ? "" : "hidden",
|
||||
].join(" ")}
|
||||
icon={Icons.CHECKMARK}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</FloatingCardView.Content>
|
||||
</FloatingView>
|
||||
);
|
||||
}
|
@ -7,6 +7,7 @@ import { CaptionsSelectionAction } from "@/video/components/actions/list-entries
|
||||
import { SourceSelectionAction } from "@/video/components/actions/list-entries/SourceSelectionAction";
|
||||
import { CaptionSelectionPopout } from "./CaptionSelectionPopout";
|
||||
import { SourceSelectionPopout } from "./SourceSelectionPopout";
|
||||
import { CaptionSettingsPopout } from "./CaptionSettingsPopout";
|
||||
|
||||
export function SettingsPopout() {
|
||||
const floatingRouter = useFloatingRouter();
|
||||
@ -24,6 +25,10 @@ export function SettingsPopout() {
|
||||
</FloatingView>
|
||||
<SourceSelectionPopout router={floatingRouter} prefix="source" />
|
||||
<CaptionSelectionPopout router={floatingRouter} prefix="captions" />
|
||||
<CaptionSettingsPopout
|
||||
router={floatingRouter}
|
||||
prefix="caption-settings"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": "./src",
|
||||
"typeRoots": ["./src/@types"],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
},
|
||||
|
112
yarn.lock
112
yarn.lock
@ -944,7 +944,7 @@
|
||||
|
||||
"@esbuild/darwin-arm64@0.16.5":
|
||||
version "0.16.5"
|
||||
resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.5.tgz"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.16.5.tgz#6e553f4be193d25a5e7cb6a73077d407a64bd6ad"
|
||||
integrity sha512-4HlbUMy50cRaHGVriBjShs46WRPshtnVOqkxEGhEuDuJhgZ3regpWzaQxXOcDXFvVwue8RiqDAAcOi/QlVLE6Q==
|
||||
|
||||
"@esbuild/darwin-x64@0.16.5":
|
||||
@ -1034,7 +1034,7 @@
|
||||
|
||||
"@esbuild/win32-x64@0.16.5":
|
||||
version "0.16.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.16.5.tgz#9398d079a83b309b44021634ae6b4f7bc6a0cad0"
|
||||
resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.5.tgz"
|
||||
integrity sha512-q00Jasz6/wCOD2XxRj4GEwj27u1zfpiBniL1ip3/YGGcYtvOoGKCNSS47sufO/8ixEgrSYDlkglSd6CxcS7m0g==
|
||||
|
||||
"@eslint/eslintrc@^1.3.3":
|
||||
@ -1053,9 +1053,9 @@
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@formkit/auto-animate@^1.0.0-beta.5":
|
||||
version "1.0.0-beta.6"
|
||||
resolved "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-1.0.0-beta.6.tgz"
|
||||
integrity sha512-PVDhLAlr+B4Xb7e+1wozBUWmXa6BFU8xUPR/W/E+TsQhPS1qkAdAsJ25keEnFrcePSnXHrOsh3tiFbEToOzV9w==
|
||||
version "1.0.0-beta.5"
|
||||
resolved "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-1.0.0-beta.5.tgz"
|
||||
integrity sha512-WoSwyhAZPOe6RB/IgicOtCHtrWwEpfKIZ/H/nxpKfnZL9CB6hhhBGU5bCdMRw7YpAUF2CDlQa+WWh+gCqz5lDg==
|
||||
|
||||
"@headlessui/react@^1.5.0":
|
||||
version "1.7.5"
|
||||
@ -1264,7 +1264,7 @@
|
||||
|
||||
"@swc/core-darwin-arm64@1.3.22":
|
||||
version "1.3.22"
|
||||
resolved "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.22.tgz"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.22.tgz#296db00b567d7fab0fc438eccc7334b53d54e2f2"
|
||||
integrity sha512-MMhtPsuXp8gpUgr9bs+RZQ2IyFGiUNDG93usCDAFgAF+6VVp+YaAVjET/3/Bx5Lk2WAt0RxT62C9KTEw1YMo3w==
|
||||
|
||||
"@swc/core-darwin-x64@1.3.22":
|
||||
@ -1309,7 +1309,7 @@
|
||||
|
||||
"@swc/core-win32-x64-msvc@1.3.22":
|
||||
version "1.3.22"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.22.tgz#fb820b1aa03605363d141c9656d966a25000790f"
|
||||
resolved "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.22.tgz"
|
||||
integrity sha512-ESyn4lZXAKEE3mcTaDfXatsolCiEfVGstsXdgBmZYa6o1IE1bDW8FE7Ob/Y+82WTpm9+A9ZYXYjZ62t67POHZg==
|
||||
|
||||
"@swc/core@^1.3.21":
|
||||
@ -1351,9 +1351,9 @@
|
||||
integrity sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==
|
||||
|
||||
"@types/chrome@*":
|
||||
version "0.0.217"
|
||||
resolved "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.217.tgz"
|
||||
integrity sha512-q8fLzCCoHiR9gYRoqvrx12+HaJjRTqUom5Ks/wLSR8Ac83qAqWaA4NgUBUcDjM1O1ACczygxIHCEENXs1zmbqQ==
|
||||
version "0.0.210"
|
||||
resolved "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.210.tgz"
|
||||
integrity sha512-VSjQu1k6a/rAfuqR1Gi/oxHZj4+t6+LG+GobNI3ZWI6DQ+fmphNSF6TrLHG6BYK2bXc9Gb4c1uXFKRRVLaGl5Q==
|
||||
dependencies:
|
||||
"@types/filesystem" "*"
|
||||
"@types/har-format" "*"
|
||||
@ -1370,6 +1370,13 @@
|
||||
resolved "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.1.tgz"
|
||||
integrity sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==
|
||||
|
||||
"@types/dompurify@^2.4.0":
|
||||
version "2.4.0"
|
||||
resolved "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.4.0.tgz"
|
||||
integrity sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg==
|
||||
dependencies:
|
||||
"@types/trusted-types" "*"
|
||||
|
||||
"@types/estree@0.0.39":
|
||||
version "0.0.39"
|
||||
resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz"
|
||||
@ -1520,7 +1527,7 @@
|
||||
resolved "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz"
|
||||
integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==
|
||||
|
||||
"@types/trusted-types@^2.0.2":
|
||||
"@types/trusted-types@*", "@types/trusted-types@^2.0.2":
|
||||
version "2.0.3"
|
||||
resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz"
|
||||
integrity sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==
|
||||
@ -1707,7 +1714,7 @@ acorn-walk@^8.0.2, acorn-walk@^8.2.0:
|
||||
|
||||
acorn@^7.0.0:
|
||||
version "7.4.1"
|
||||
resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
|
||||
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
|
||||
|
||||
acorn@^8.1.0, acorn@^8.5.0, acorn@^8.8.0, acorn@^8.8.1, acorn@^8.8.2:
|
||||
@ -1992,9 +1999,9 @@ camelcase-css@^2.0.1:
|
||||
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
|
||||
|
||||
caniuse-lite@^1.0.30001426, caniuse-lite@^1.0.30001449:
|
||||
version "1.0.30001458"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001458.tgz"
|
||||
integrity sha512-lQ1VlUUq5q9ro9X+5gOEyH7i3vm+AYVT1WDCVB69XOZ17KZRhnZ9J0Sqz7wTHQaLBJccNCHq8/Ww5LlOIZbB0w==
|
||||
version "1.0.30001457"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001457.tgz"
|
||||
integrity sha512-SDIV6bgE1aVbK6XyxdURbUE89zY7+k1BBBaOwYwkNCglXlel/E7mELiHC64HQ+W0xSKlqWhV9Wh7iHxUjMs4fA==
|
||||
|
||||
chai@^4.3.7:
|
||||
version "4.3.7"
|
||||
@ -2100,6 +2107,11 @@ commander@^2.20.0:
|
||||
resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz"
|
||||
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
|
||||
|
||||
commander@^7.1.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz"
|
||||
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
|
||||
|
||||
commander@^8.0.0:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz"
|
||||
@ -2133,9 +2145,9 @@ copy-to-clipboard@^3.3.1:
|
||||
toggle-selection "^1.0.6"
|
||||
|
||||
core-js-compat@^3.25.1:
|
||||
version "3.29.0"
|
||||
resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.29.0.tgz"
|
||||
integrity sha512-ScMn3uZNAFhK2DGoEfErguoiAHhV2Ju+oJo/jK08p7B3f3UhocUrCCkTvnZaiS+edl5nlIoiBXKcwMc6elv4KQ==
|
||||
version "3.28.0"
|
||||
resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.28.0.tgz"
|
||||
integrity sha512-myzPgE7QodMg4nnd3K1TDoES/nADRStM8Gpz0D6nhkwbmwEnE0ZGJgoWsvQ722FR8D7xS0n0LV556RcEicjTyg==
|
||||
dependencies:
|
||||
browserslist "^4.21.5"
|
||||
|
||||
@ -2145,9 +2157,9 @@ core-js-pure@^3.25.1:
|
||||
integrity sha512-VVXcDpp/xJ21KdULRq/lXdLzQAtX7+37LzpyfFM973il0tWSsDEoyzG38G14AjTpK9VTfiNM9jnFauq/CpaWGQ==
|
||||
|
||||
core-js@^3.6.5:
|
||||
version "3.28.0"
|
||||
resolved "https://registry.npmjs.org/core-js/-/core-js-3.28.0.tgz"
|
||||
integrity sha512-GiZn9D4Z/rSYvTeg1ljAIsEqFm0LaN9gVtwDCrKL80zHtS31p9BAjmTxVqTQDMpwlMolJZOFntUG2uwyj7DAqw==
|
||||
version "3.27.1"
|
||||
resolved "https://registry.npmjs.org/core-js/-/core-js-3.27.1.tgz"
|
||||
integrity sha512-GutwJLBChfGCpwwhbYoqfv03LAfmiz7e7D/BNxzeMxwQf10GRSzqiOjx7AmtEk+heiD/JWmBuyBPgFtx0Sg1ww==
|
||||
|
||||
cross-spawn@^7.0.2:
|
||||
version "7.0.3"
|
||||
@ -2285,7 +2297,7 @@ delayed-stream@~1.0.0:
|
||||
resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
|
||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||
|
||||
destr@^1.2.2:
|
||||
destr@^1.2.1:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmjs.org/destr/-/destr-1.2.2.tgz"
|
||||
integrity sha512-lrbCJwD9saUQrqUfXvl6qoM+QN3W7tLV5pAOs+OqOmopCCz/JkE05MHedJR1xfk4IAnZuJXPVuN5+7jNA2ZCiA==
|
||||
@ -2350,6 +2362,11 @@ domexception@^4.0.0:
|
||||
dependencies:
|
||||
webidl-conversions "^7.0.0"
|
||||
|
||||
dompurify@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/dompurify/-/dompurify-3.0.1.tgz"
|
||||
integrity sha512-60tsgvPKwItxZZdfLmamp0MTcecCta3avOhsLgPZ0qcWt96OasFfhkeIRbJ6br5i0fQawT1/RBGB5L58/Jpwuw==
|
||||
|
||||
eastasianwidth@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz"
|
||||
@ -2872,7 +2889,7 @@ fs-extra@^9.0.1:
|
||||
|
||||
fs.realpath@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
|
||||
|
||||
fscreen@^1.2.0:
|
||||
@ -2882,7 +2899,7 @@ fscreen@^1.2.0:
|
||||
|
||||
fsevents@~2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
||||
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
||||
|
||||
function-bind@^1.1.1:
|
||||
@ -2970,13 +2987,13 @@ glob@^7.1.3, glob@^7.1.6, glob@^7.2.0:
|
||||
|
||||
globals@^11.1.0:
|
||||
version "11.12.0"
|
||||
resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
|
||||
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
|
||||
|
||||
globals@^13.15.0:
|
||||
version "13.19.0"
|
||||
resolved "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz"
|
||||
integrity sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==
|
||||
version "13.20.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-13.20.0.tgz#ea276a1e508ffd4f1612888f9d1bad1e2717bf82"
|
||||
integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==
|
||||
dependencies:
|
||||
type-fest "^0.20.2"
|
||||
|
||||
@ -3156,7 +3173,7 @@ imurmurhash@^0.1.4:
|
||||
|
||||
inflight@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz"
|
||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||
integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
|
||||
dependencies:
|
||||
once "^1.3.0"
|
||||
@ -3164,7 +3181,7 @@ inflight@^1.0.4:
|
||||
|
||||
inherits@2:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
||||
inline-style-prefixer@^6.0.0:
|
||||
@ -3707,16 +3724,23 @@ natural-compare@^1.4.0:
|
||||
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
|
||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||
|
||||
node-fetch-native@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.0.2.tgz"
|
||||
integrity sha512-KIkvH1jl6b3O7es/0ShyCgWLcfXxlBrLBbP3rOr23WArC66IMcU4DeZEeYEOwnopYhawLTn7/y+YtmASe8DFVQ==
|
||||
node-fetch-native@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.0.1.tgz"
|
||||
integrity sha512-VzW+TAk2wE4X9maiKMlT+GsPU4OMmR1U9CrHSmd3DFLn2IcZ9VJ6M6BBugGfYUnPCLSYxXdZy17M0BEJyhUTwg==
|
||||
|
||||
node-releases@^2.0.8:
|
||||
version "2.0.10"
|
||||
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz"
|
||||
integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==
|
||||
|
||||
node-webvtt@^1.9.4:
|
||||
version "1.9.4"
|
||||
resolved "https://registry.npmjs.org/node-webvtt/-/node-webvtt-1.9.4.tgz"
|
||||
integrity sha512-EjrJdKdxSyd8j4LMLW6s2Ah4yNoeVXp18Ob04CQl1In18xcUmKzEE8pcsxxnFVqanTyjbGYph2VnvtwIXR4EjA==
|
||||
dependencies:
|
||||
commander "^7.1.0"
|
||||
|
||||
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz"
|
||||
@ -3805,17 +3829,17 @@ object.values@^1.1.5:
|
||||
es-abstract "^1.20.4"
|
||||
|
||||
ofetch@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/ofetch/-/ofetch-1.0.1.tgz"
|
||||
integrity sha512-icBz2JYfEpt+wZz1FRoGcrMigjNKjzvufE26m9+yUiacRQRHwnNlGRPiDnW4op7WX/MR6aniwS8xw8jyVelF2g==
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/ofetch/-/ofetch-1.0.0.tgz"
|
||||
integrity sha512-d40aof8czZFSQKJa4+F7Ch3UC5D631cK1TTUoK+iNEut9NoiCL+u0vykl/puYVUS2df4tIQl5upQcolIcEzQjQ==
|
||||
dependencies:
|
||||
destr "^1.2.2"
|
||||
node-fetch-native "^1.0.2"
|
||||
ufo "^1.1.0"
|
||||
destr "^1.2.1"
|
||||
node-fetch-native "^1.0.1"
|
||||
ufo "^1.0.0"
|
||||
|
||||
once@^1.3.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
@ -3891,7 +3915,7 @@ path-exists@^4.0.0:
|
||||
|
||||
path-is-absolute@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz"
|
||||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||
integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
|
||||
|
||||
path-key@^3.0.0, path-key@^3.1.0:
|
||||
@ -4916,7 +4940,7 @@ typescript@^4.6.4:
|
||||
resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz"
|
||||
integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==
|
||||
|
||||
ufo@^1.1.0:
|
||||
ufo@^1.0.0, ufo@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/ufo/-/ufo-1.1.0.tgz"
|
||||
integrity sha512-LQc2s/ZDMaCN3QLpa+uzHUOQ7SdV0qgv3VBXOolQGXTaaZpIur6PwUclF5nN2hNkiTRcUugXd1zFOW3FLJ135Q==
|
||||
@ -5396,7 +5420,7 @@ workbox-window@6.5.4, workbox-window@^6.5.4:
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
||||
|
||||
ws@^8.11.0:
|
||||
|
Loading…
Reference in New Issue
Block a user