From 42402eb5c73c7dfa286a3e31ad8ae08f660f80ad Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sat, 7 Jan 2023 21:36:18 +0100 Subject: [PATCH 001/135] new search view --- package.json | 7 +- src/components/SearchBar.tsx | 66 +- src/components/buttons/DropdownButton.tsx | 72 +- src/components/layout/Backdrop.tsx | 49 +- src/components/layout/ErrorBoundary.tsx | 2 +- src/components/layout/Navigation.tsx | 16 +- src/components/layout/SectionHeading.tsx | 10 - src/components/layout/ThinContainer.tsx | 4 +- src/components/text/Title.tsx | 6 +- src/constants.ts | 3 - src/index.tsx | 9 +- src/providers/list/flixhq/index.ts | 2 +- src/providers/list/gdriveplayer/index.ts | 2 +- src/providers/list/gomostream/index.ts | 2 +- src/providers/list/superstream/index.ts | 2 +- src/providers/list/theflix/index.ts | 2 +- src/providers/list/theflix/portableToMedia.ts | 2 +- src/providers/list/theflix/search.ts | 2 +- src/providers/list/xemovie/index.ts | 2 +- src/{ => setup}/App.tsx | 6 +- src/{ => setup}/config.ts | 2 +- src/{mw_constants.ts => setup/constants.ts} | 0 src/{ => setup}/i18n.ts | 0 src/{ => setup}/index.css | 6 +- src/utils/cache.ts | 60 +- src/utils/storage.ts | 20 +- src/views/MediaView.tsx | 4 +- src/views/SearchView.tsx | 224 --- src/views/notfound/NotFoundView.tsx | 22 +- src/views/search/HomeView.tsx | 65 + src/views/search/SearchLoadingView.tsx | 12 + src/views/search/SearchResultsPartial.tsx | 32 + src/views/search/SearchResultsView.tsx | 102 ++ src/views/search/SearchView.tsx | 54 + vite.config.ts | 3 +- yarn.lock | 1234 ++++++++++++++++- 36 files changed, 1714 insertions(+), 392 deletions(-) delete mode 100644 src/constants.ts rename src/{ => setup}/App.tsx (88%) rename src/{ => setup}/config.ts (95%) rename src/{mw_constants.ts => setup/constants.ts} (100%) rename src/{ => setup}/i18n.ts (100%) rename src/{ => setup}/index.css (70%) delete mode 100644 src/views/SearchView.tsx create mode 100644 src/views/search/HomeView.tsx create mode 100644 src/views/search/SearchLoadingView.tsx create mode 100644 src/views/search/SearchResultsPartial.tsx create mode 100644 src/views/search/SearchResultsView.tsx create mode 100644 src/views/search/SearchView.tsx diff --git a/package.json b/package.json index f3cfd55c..405a6ca5 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "react-dom": "^17.0.2", "react-i18next": "^12.1.1", "react-router-dom": "^5.2.0", + "react-stickynode": "^4.1.0", "srt-webvtt": "^2.0.0", "unpacker": "^1.0.1" }, @@ -47,6 +48,7 @@ "@types/react-dom": "^17.0.11", "@types/react-router": "^5.1.18", "@types/react-router-dom": "^5.3.3", + "@types/react-stickynode": "^4.0.0", "@typescript-eslint/eslint-plugin": "^5.13.0", "@typescript-eslint/parser": "^5.13.0", "@vitejs/plugin-react-swc": "^3.0.0", @@ -59,12 +61,15 @@ "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-react": "7.29.4", "eslint-plugin-react-hooks": "4.3.0", + "i": "^0.3.7", + "npm": "^9.2.0", "postcss": "^8.4.20", "prettier": "^2.5.1", "prettier-plugin-tailwindcss": "^0.1.7", "tailwind-scrollbar": "^2.0.1", "tailwindcss": "^3.2.4", "typescript": "^4.6.4", - "vite": "^4.0.1" + "vite": "^4.0.1", + "vite-plugin-package-version": "^1.0.2" } } diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index 6a5c3ad4..eea26423 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { MWMediaType, MWQuery } from "@/providers"; import { useTranslation } from "react-i18next"; import { DropdownButton } from "./buttons/DropdownButton"; -import { Icons } from "./Icon"; +import { Icon, Icons } from "./Icon"; import { TextInputControl } from "./text-inputs/TextInputControl"; export interface SearchBarProps { @@ -37,42 +37,48 @@ export function SearchBarInput(props: SearchBarProps) { } return ( -
+
+
+ +
+ setSearch(val)} value={props.value.searchQuery} - className="w-full flex-1 bg-transparent text-white placeholder-denim-700 focus:outline-none" + className="w-full flex-1 bg-transparent px-4 py-4 pl-12 text-white placeholder-denim-700 focus:outline-none sm:py-4 sm:pr-2" placeholder={props.placeholder} /> - setDropdownOpen(val)} - selectedItem={props.value.type} - setSelectedItem={(val) => setType(val)} - options={[ - { - id: MWMediaType.MOVIE, - name: t('searchBar.movie'), - icon: Icons.FILM, - }, - { - id: MWMediaType.SERIES, - name: t('searchBar.series'), - icon: Icons.CLAPPER_BOARD, - }, - // { - // id: MWMediaType.ANIME, - // name: "Anime", - // icon: Icons.DRAGON, - // }, - ]} - onClick={() => setDropdownOpen((old) => !old)} - > - {props.buttonText || t('searchBar.search')} - +
+ setDropdownOpen(val)} + selectedItem={props.value.type} + setSelectedItem={(val) => setType(val)} + options={[ + { + id: MWMediaType.MOVIE, + name: t("searchBar.movie"), + icon: Icons.FILM, + }, + { + id: MWMediaType.SERIES, + name: t("searchBar.series"), + icon: Icons.CLAPPER_BOARD, + }, + // { + // id: MWMediaType.ANIME, + // name: "Anime", + // icon: Icons.DRAGON, + // }, + ]} + onClick={() => setDropdownOpen((old) => !old)} + > + {props.buttonText || t("searchBar.search")} + +
); } diff --git a/src/components/buttons/DropdownButton.tsx b/src/components/buttons/DropdownButton.tsx index 5c1a12c5..f32517ba 100644 --- a/src/components/buttons/DropdownButton.tsx +++ b/src/components/buttons/DropdownButton.tsx @@ -6,7 +6,11 @@ import React, { } from "react"; import { Icon, Icons } from "@/components/Icon"; -import { Backdrop, useBackdrop } from "@/components/layout/Backdrop"; +import { + Backdrop, + BackdropContainer, + useBackdrop, +} from "@/components/layout/Backdrop"; import { ButtonControlProps, ButtonControl } from "./ButtonControl"; export interface OptionItem { @@ -56,7 +60,7 @@ export const DropdownButton = React.forwardRef< ); useEffect(() => { - let id: NodeJS.Timeout; + let id: ReturnType; if (props.open) { setDelayedSelectedId(props.selectedItem); @@ -93,37 +97,43 @@ export const DropdownButton = React.forwardRef< className="relative w-full sm:w-auto" {...highlightedProps} > - props.setOpen(false)} + {...backdropProps} > - - {selectedItem.name} - - -
- {props.options - .filter((opt) => opt.id !== delayedSelectedId) - .map((opt) => ( -
+ + + {selectedItem.name} + + +
+ {props.options + .filter((opt) => opt.id !== delayedSelectedId) + .map((opt) => ( +
+
- props.setOpen(false)} {...backdropProps} /> ); }); diff --git a/src/components/layout/Backdrop.tsx b/src/components/layout/Backdrop.tsx index 65d3a81d..3daac079 100644 --- a/src/components/layout/Backdrop.tsx +++ b/src/components/layout/Backdrop.tsx @@ -1,5 +1,6 @@ -import { useEffect, useState } from "react"; +import React, { createRef, useEffect, useState } from "react"; import { useFade } from "@/hooks/useFade"; +import { createPortal } from "react-dom"; interface BackdropProps { onClick?: (e: MouseEvent) => void; @@ -58,7 +59,7 @@ export function Backdrop(props: BackdropProps) { return (
); } + +export function BackdropContainer( + props: { + children: React.ReactNode; + } & BackdropProps +) { + const root = createRef(); + const copy = createRef(); + + useEffect(() => { + let frame = -1; + function poll() { + if (root.current && copy.current) { + const rect = root.current.getBoundingClientRect(); + copy.current.style.top = `${rect.top}px`; + copy.current.style.left = `${rect.left}px`; + copy.current.style.width = `${rect.width}px`; + copy.current.style.height = `${rect.height}px`; + } + frame = window.requestAnimationFrame(poll); + } + poll(); + return () => { + window.cancelAnimationFrame(frame); + }; + // we dont want this to run only on mount, dont care about ref updates + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [root, copy]); + + return ( +
+ {createPortal( +
+ +
+ {props.children} +
+
, + document.body + )} +
{props.children}
+
+ ); +} diff --git a/src/components/layout/ErrorBoundary.tsx b/src/components/layout/ErrorBoundary.tsx index b1803226..061ff5df 100644 --- a/src/components/layout/ErrorBoundary.tsx +++ b/src/components/layout/ErrorBoundary.tsx @@ -3,7 +3,7 @@ import { IconPatch } from "@/components/buttons/IconPatch"; import { Icons } from "@/components/Icon"; import { Link } from "@/components/text/Link"; import { Title } from "@/components/text/Title"; -import { conf } from "@/config"; +import { conf } from "@/setup/config"; interface ErrorBoundaryState { hasError: boolean; diff --git a/src/components/layout/Navigation.tsx b/src/components/layout/Navigation.tsx index 00fd2eb0..71d40f76 100644 --- a/src/components/layout/Navigation.tsx +++ b/src/components/layout/Navigation.tsx @@ -2,17 +2,25 @@ import { ReactNode } from "react"; import { Link } from "react-router-dom"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icons } from "@/components/Icon"; -import { conf } from "@/config"; +import { conf } from "@/setup/config"; import { BrandPill } from "./BrandPill"; export interface NavigationProps { children?: ReactNode; + bg?: boolean; } export function Navigation(props: NavigationProps) { return ( -
-
+
+
+
+
+
diff --git a/src/components/layout/ThinContainer.tsx b/src/components/layout/ThinContainer.tsx index c1866956..e1672f63 100644 --- a/src/components/layout/ThinContainer.tsx +++ b/src/components/layout/ThinContainer.tsx @@ -8,7 +8,9 @@ interface ThinContainerProps { export function ThinContainer(props: ThinContainerProps) { return (
{props.children}
diff --git a/src/components/text/Title.tsx b/src/components/text/Title.tsx index 436a2663..f6771c6f 100644 --- a/src/components/text/Title.tsx +++ b/src/components/text/Title.tsx @@ -3,5 +3,9 @@ export interface TitleProps { } export function Title(props: TitleProps) { - return

{props.children}

; + return ( +

+ {props.children} +

+ ); } diff --git a/src/constants.ts b/src/constants.ts deleted file mode 100644 index f9ac5da1..00000000 --- a/src/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const DISCORD_LINK = "https://discord.gg/Jhqt4Xzpfb"; -export const GITHUB_LINK = "https://github.com/JamesHawkinss/movie-web"; -export const APP_VERSION = "2.1.0"; diff --git a/src/index.tsx b/src/index.tsx index d0a08f46..d6b93ba0 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,11 +1,12 @@ import React, { Suspense } from "react"; import ReactDOM from "react-dom"; import { HashRouter } from "react-router-dom"; -import "./index.css"; import { ErrorBoundary } from "@/components/layout/ErrorBoundary"; -import App from "./App"; -import "./i18n"; -import { conf } from "./config"; +import { conf } from "@/setup/config"; + +import App from "@/setup/App"; +import "@/setup/i18n"; +import "@/setup/index.css"; // initialize const key = diff --git a/src/providers/list/flixhq/index.ts b/src/providers/list/flixhq/index.ts index 8fe6564d..304f0157 100644 --- a/src/providers/list/flixhq/index.ts +++ b/src/providers/list/flixhq/index.ts @@ -7,7 +7,7 @@ import { MWProviderMediaResult, } from "@/providers/types"; -import { conf } from "@/config"; +import { conf } from "@/setup/config"; export const flixhqProvider: MWMediaProvider = { id: "flixhq", diff --git a/src/providers/list/gdriveplayer/index.ts b/src/providers/list/gdriveplayer/index.ts index d13d2414..b1ed69d3 100644 --- a/src/providers/list/gdriveplayer/index.ts +++ b/src/providers/list/gdriveplayer/index.ts @@ -9,7 +9,7 @@ import { MWProviderMediaResult, } from "@/providers/types"; -import { conf } from "@/config"; +import { conf } from "@/setup/config"; const format = { stringify: (cipher: any) => { diff --git a/src/providers/list/gomostream/index.ts b/src/providers/list/gomostream/index.ts index 092645d4..e9d65d88 100644 --- a/src/providers/list/gomostream/index.ts +++ b/src/providers/list/gomostream/index.ts @@ -9,7 +9,7 @@ import { MWProviderMediaResult, } from "@/providers/types"; -import { conf } from "@/config"; +import { conf } from "@/setup/config"; export const gomostreamScraper: MWMediaProvider = { id: "gomostream", diff --git a/src/providers/list/superstream/index.ts b/src/providers/list/superstream/index.ts index 3dc26e7b..832e141a 100644 --- a/src/providers/list/superstream/index.ts +++ b/src/providers/list/superstream/index.ts @@ -4,7 +4,7 @@ import { customAlphabet } from "nanoid"; import toWebVTT from "srt-webvtt"; import CryptoJS from "crypto-js"; -import { conf } from "@/config"; +import { conf } from "@/setup/config"; import { MWMediaProvider, MWMediaType, diff --git a/src/providers/list/theflix/index.ts b/src/providers/list/theflix/index.ts index cdfe8e66..01ac7092 100644 --- a/src/providers/list/theflix/index.ts +++ b/src/providers/list/theflix/index.ts @@ -15,7 +15,7 @@ import { } from "@/providers/list/theflix/search"; import { getDataFromPortableSearch } from "@/providers/list/theflix/portableToMedia"; -import { conf } from "@/config"; +import { conf } from "@/setup/config"; export const theFlixScraper: MWMediaProvider = { id: "theflix", diff --git a/src/providers/list/theflix/portableToMedia.ts b/src/providers/list/theflix/portableToMedia.ts index 191e828c..4f42dd47 100644 --- a/src/providers/list/theflix/portableToMedia.ts +++ b/src/providers/list/theflix/portableToMedia.ts @@ -1,4 +1,4 @@ -import { conf } from "@/config"; +import { conf } from "@/setup/config"; import { MWMediaType, MWPortableMedia } from "@/providers/types"; const getTheFlixUrl = (media: MWPortableMedia, params?: URLSearchParams) => { diff --git a/src/providers/list/theflix/search.ts b/src/providers/list/theflix/search.ts index c0dded24..aa575194 100644 --- a/src/providers/list/theflix/search.ts +++ b/src/providers/list/theflix/search.ts @@ -1,4 +1,4 @@ -import { conf } from "@/config"; +import { conf } from "@/setup/config"; import { MWMediaType, MWProviderMediaResult, MWQuery } from "@/providers"; const getTheFlixUrl = (type: "tv-shows" | "movies", params: URLSearchParams) => diff --git a/src/providers/list/xemovie/index.ts b/src/providers/list/xemovie/index.ts index 2d14e73b..82df6848 100644 --- a/src/providers/list/xemovie/index.ts +++ b/src/providers/list/xemovie/index.ts @@ -8,7 +8,7 @@ import { MWMediaCaption, } from "@/providers/types"; -import { conf } from "@/config"; +import { conf } from "@/setup/config"; export const xemovieScraper: MWMediaProvider = { id: "xemovie", diff --git a/src/App.tsx b/src/setup/App.tsx similarity index 88% rename from src/App.tsx rename to src/setup/App.tsx index b427c62e..b23cf7ee 100644 --- a/src/App.tsx +++ b/src/setup/App.tsx @@ -2,10 +2,10 @@ import { Redirect, Route, Switch } from "react-router-dom"; import { MWMediaType } from "@/providers"; import { BookmarkContextProvider } from "@/state/bookmark"; import { WatchedContextProvider } from "@/state/watched"; + import { NotFoundPage } from "@/views/notfound/NotFoundView"; -import "./index.css"; -import { MediaView } from "./views/MediaView"; -import { SearchView } from "./views/SearchView"; +import { MediaView } from "@/views/MediaView"; +import { SearchView } from "@/views/search/SearchView"; function App() { return ( diff --git a/src/config.ts b/src/setup/config.ts similarity index 95% rename from src/config.ts rename to src/setup/config.ts index 6e4fb805..951be497 100644 --- a/src/config.ts +++ b/src/setup/config.ts @@ -1,4 +1,4 @@ -import { APP_VERSION, GITHUB_LINK, DISCORD_LINK } from "@/constants"; +import { APP_VERSION, GITHUB_LINK, DISCORD_LINK } from "./constants"; interface Config { APP_VERSION: string; diff --git a/src/mw_constants.ts b/src/setup/constants.ts similarity index 100% rename from src/mw_constants.ts rename to src/setup/constants.ts diff --git a/src/i18n.ts b/src/setup/i18n.ts similarity index 100% rename from src/i18n.ts rename to src/setup/i18n.ts diff --git a/src/index.css b/src/setup/index.css similarity index 70% rename from src/index.css rename to src/setup/index.css index eadd4e89..efc192db 100644 --- a/src/index.css +++ b/src/setup/index.css @@ -4,13 +4,11 @@ html, body { - @apply bg-denim-100 text-denim-700 font-open-sans min-h-screen; + @apply bg-denim-100 text-denim-700 font-open-sans min-h-screen overflow-x-hidden; } #root { - display: flex; - justify-content: flex-start; - align-items: flex-start; + padding: 0.05px; min-height: 100vh; width: 100%; } diff --git a/src/utils/cache.ts b/src/utils/cache.ts index 383a660e..f5c3be3b 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -1,15 +1,15 @@ export class SimpleCache { protected readonly INTERVAL_MS = 2 * 60 * 1000; // 2 minutes - protected _interval: NodeJS.Timer | null = null; + protected _interval: ReturnType | null = null; protected _compare: ((a: Key, b: Key) => boolean) | null = null; protected _storage: { key: Key; value: Value; expiry: Date }[] = []; /* - ** initialize store, will start the interval - */ + ** initialize store, will start the interval + */ public initialize(): void { if (this._interval) throw new Error("cache is already initialized"); this._interval = setInterval(() => { @@ -22,46 +22,48 @@ export class SimpleCache { } /* - ** destroy cache instance, its not safe to use the instance after calling this - */ + ** destroy cache instance, its not safe to use the instance after calling this + */ public destroy(): void { - if (this._interval) - clearInterval(this._interval); + if (this._interval) clearInterval(this._interval); this.clear(); } - + /* - ** Set compare function, function must return true if A & B are equal - */ + ** Set compare function, function must return true if A & B are equal + */ public setCompare(compare: (a: Key, b: Key) => boolean): void { this._compare = compare; } /* - ** check if cache contains the item - */ + ** check if cache contains the item + */ public has(key: Key): boolean { return !!this.get(key); } - + /* - ** get item from cache - */ + ** get item from cache + */ public get(key: Key): Value | undefined { if (!this._compare) throw new Error("Compare function not set"); - const foundValue = this._storage.find(item => this._compare && this._compare(item.key, key)); - if (!foundValue) - return undefined; + const foundValue = this._storage.find( + (item) => this._compare && this._compare(item.key, key) + ); + if (!foundValue) return undefined; return foundValue.value; } - + /* - ** set item from cache, if it already exists, it will overwrite - */ + ** set item from cache, if it already exists, it will overwrite + */ public set(key: Key, value: Value, expirySeconds: number): void { if (!this._compare) throw new Error("Compare function not set"); - const foundValue = this._storage.find(item => this._compare && this._compare(item.key, key)); - const expiry = new Date((new Date().getTime()) + (expirySeconds * 1000)); + const foundValue = this._storage.find( + (item) => this._compare && this._compare(item.key, key) + ); + const expiry = new Date(new Date().getTime() + expirySeconds * 1000); // overwrite old value if (foundValue) { @@ -76,12 +78,12 @@ export class SimpleCache { key, value, expiry, - }) + }); } /* - ** remove item from cache - */ + ** remove item from cache + */ public remove(key: Key): void { if (!this._compare) throw new Error("Compare function not set"); this._storage.filter((val) => { @@ -89,10 +91,10 @@ export class SimpleCache { return true; }); } - + /* - ** clear entire cache storage - */ + ** clear entire cache storage + */ public clear(): void { this._storage = []; } diff --git a/src/utils/storage.ts b/src/utils/storage.ts index 51befa7a..9e56e651 100644 --- a/src/utils/storage.ts +++ b/src/utils/storage.ts @@ -157,19 +157,19 @@ export function versionedStoreBuilder(): any { version, // version number update: migrate ? (data: any) => { - // update function, and increment version - const newData = migrate(data); - newData["--version"] = version; // eslint-disable-line no-param-reassign - return newData; - } + // update function, and increment version + const newData = migrate(data); + newData["--version"] = version; // eslint-disable-line no-param-reassign + return newData; + } : undefined, init: create ? () => { - // return an initial object - const data = create(); - data["--version"] = version; - return data; - } + // return an initial object + const data = create(); + data["--version"] = version; + return data; + } : undefined, }; return this; diff --git a/src/views/MediaView.tsx b/src/views/MediaView.tsx index 1c121cd2..02ea86e0 100644 --- a/src/views/MediaView.tsx +++ b/src/views/MediaView.tsx @@ -120,7 +120,7 @@ function LoadingMediaFooter(props: { error?: boolean }) { {props.error ? (
-

{t('media.invalidUrl')}

+

{t("media.invalidUrl")}

) : ( @@ -200,7 +200,7 @@ export function MediaView() { : reactHistory.push("/") } direction="left" - linkText={t('media.arrowText')} + linkText={t("media.arrowText")} /> diff --git a/src/views/SearchView.tsx b/src/views/SearchView.tsx deleted file mode 100644 index 5d7fa97b..00000000 --- a/src/views/SearchView.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; -import { SearchBarInput } from "@/components/SearchBar"; -import { MWMassProviderOutput, MWQuery, SearchProviders } from "@/providers"; -import { ThinContainer } from "@/components/layout/ThinContainer"; -import { SectionHeading } from "@/components/layout/SectionHeading"; -import { Icons } from "@/components/Icon"; -import { Loading } from "@/components/layout/Loading"; -import { Tagline } from "@/components/text/Tagline"; -import { Title } from "@/components/text/Title"; -import { useDebounce } from "@/hooks/useDebounce"; -import { useLoading } from "@/hooks/useLoading"; -import { IconPatch } from "@/components/buttons/IconPatch"; -import { Navigation } from "@/components/layout/Navigation"; -import { useSearchQuery } from "@/hooks/useSearchQuery"; -import { useWatchedContext } from "@/state/watched/context"; -import { - getIfBookmarkedFromPortable, - useBookmarkContext, -} from "@/state/bookmark/context"; -import { useTranslation } from "react-i18next"; - -function SearchLoading() { - const { t } = useTranslation(); - return ; -} - -function SearchSuffix(props: { - fails: number; - total: number; - resultsSize: number; -}) { - const { t } = useTranslation(); - - const allFailed: boolean = props.fails === props.total; - const icon: Icons = allFailed ? Icons.WARNING : Icons.EYE_SLASH; - - return ( -
- - - {/* standard suffix */} - {!allFailed ? ( -
- {props.fails > 0 ? ( -

- {t('search.providersFailed', { fails: props.fails, total: props.total })} -

- ) : null} - {props.resultsSize > 0 ? ( -

{t('search.allResults')}

- ) : ( -

{t('search.noResults')}

- )} -
- ) : null} - - {/* Error result */} - {allFailed ? ( -
-

{t('search.allFailed')}

-
- ) : null} -
- ); -} - -function SearchResultsView({ - searchQuery, - clear, -}: { - searchQuery: MWQuery; - clear: () => void; -}) { - const { t } = useTranslation(); - - const [results, setResults] = useState(); - const [runSearchQuery, loading, error, success] = useLoading( - (query: MWQuery) => SearchProviders(query) - ); - - useEffect(() => { - async function runSearch(query: MWQuery) { - const searchResults = await runSearchQuery(query); - if (!searchResults) return; - setResults(searchResults); - } - - if (searchQuery.searchQuery !== "") runSearch(searchQuery); - }, [searchQuery, runSearchQuery]); - - return ( -
- {/* results */} - {success && results?.results.length ? ( - clear()} - > - {results.results.map((v) => ( - - ))} - - ) : null} - - {/* search suffix */} - {success && results ? ( - - ) : null} - - {/* error */} - {error ? : null} - - {/* Loading icon */} - {loading ? : null} -
- ); -} - -function ExtraItems() { - const { t } = useTranslation(); - - const { getFilteredBookmarks } = useBookmarkContext(); - const { getFilteredWatched } = useWatchedContext(); - - const bookmarks = getFilteredBookmarks(); - - const watchedItems = getFilteredWatched().filter( - (v) => !getIfBookmarkedFromPortable(bookmarks, v) - ); - - if (watchedItems.length === 0 && bookmarks.length === 0) return null; - - return ( -
- {bookmarks.length > 0 ? ( - - {bookmarks.map((v) => ( - - ))} - - ) : null} - {watchedItems.length > 0 ? ( - - {watchedItems.map((v) => ( - - ))} - - ) : null} -
- ); -} - -export function SearchView() { - const { t } = useTranslation(); - - const [searching, setSearching] = useState(false); - const [loading, setLoading] = useState(false); - const [search, setSearch, setSearchUnFocus] = useSearchQuery(); - - const debouncedSearch = useDebounce(search, 2000); - useEffect(() => { - setSearching(search.searchQuery !== ""); - setLoading(search.searchQuery !== ""); - }, [search]); - useEffect(() => { - setLoading(false); - }, [debouncedSearch]); - - const resultView = useMemo(() => { - if (loading) return ; - if (searching) - return ( - setSearch({ searchQuery: "" }, true)} - /> - ); - return ; - }, [loading, searching, debouncedSearch, setSearch]); - - return ( - <> - - - {/* input section */} -
-
- {t('search.tagline')} - {t('search.title')} -
- -
- - {/* results view */} - {resultView} -
- - ); -} diff --git a/src/views/notfound/NotFoundView.tsx b/src/views/notfound/NotFoundView.tsx index c797e281..6ba492ac 100644 --- a/src/views/notfound/NotFoundView.tsx +++ b/src/views/notfound/NotFoundView.tsx @@ -26,11 +26,9 @@ export function NotFoundMedia() { icon={Icons.EYE_SLASH} className="mb-6 text-xl text-bink-600" /> - {t('notFound.media.title')} -

- {t('notFound.media.description')} -

- + {t("notFound.media.title")} +

{t("notFound.media.description")}

+
); } @@ -44,11 +42,11 @@ export function NotFoundProvider() { icon={Icons.EYE_SLASH} className="mb-6 text-xl text-bink-600" /> - {t('notFound.provider.title')} + {t("notFound.provider.title")}

- {t('notFound.provider.description')} + {t("notFound.provider.description")}

- +
); } @@ -62,11 +60,9 @@ export function NotFoundPage() { icon={Icons.EYE_SLASH} className="mb-6 text-xl text-bink-600" /> - {t('notFound.page.title')} -

- {t('notFound.page.description')} -

- + {t("notFound.page.title")} +

{t("notFound.page.description")}

+ ); } diff --git a/src/views/search/HomeView.tsx b/src/views/search/HomeView.tsx new file mode 100644 index 00000000..d646646f --- /dev/null +++ b/src/views/search/HomeView.tsx @@ -0,0 +1,65 @@ +import { Icons } from "@/components/Icon"; +import { SectionHeading } from "@/components/layout/SectionHeading"; +import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; +import { + getIfBookmarkedFromPortable, + useBookmarkContext, +} from "@/state/bookmark"; +import { useWatchedContext } from "@/state/watched"; +import { useTranslation } from "react-i18next"; + +function Bookmarks() { + const { t } = useTranslation(); + const { getFilteredBookmarks } = useBookmarkContext(); + const bookmarks = getFilteredBookmarks(); + + if (bookmarks.length === 0) return null; + + return ( + + {bookmarks.map((v) => ( + + ))} + + ); +} + +function Watched() { + const { t } = useTranslation(); + const { getFilteredBookmarks } = useBookmarkContext(); + const { getFilteredWatched } = useWatchedContext(); + + const bookmarks = getFilteredBookmarks(); + const watchedItems = getFilteredWatched().filter( + (v) => !getIfBookmarkedFromPortable(bookmarks, v) + ); + + if (watchedItems.length === 0) return null; + + return ( + + {watchedItems.map((v) => ( + + ))} + + ); +} + +export function HomeView() { + return ( +
+ + +
+ ); +} diff --git a/src/views/search/SearchLoadingView.tsx b/src/views/search/SearchLoadingView.tsx new file mode 100644 index 00000000..e8ce46d2 --- /dev/null +++ b/src/views/search/SearchLoadingView.tsx @@ -0,0 +1,12 @@ +import { Loading } from "@/components/layout/Loading"; +import { useTranslation } from "react-i18next"; + +export function SearchLoadingView() { + const { t } = useTranslation(); + return ( + + ); +} diff --git a/src/views/search/SearchResultsPartial.tsx b/src/views/search/SearchResultsPartial.tsx new file mode 100644 index 00000000..96934fa8 --- /dev/null +++ b/src/views/search/SearchResultsPartial.tsx @@ -0,0 +1,32 @@ +import { useDebounce } from "@/hooks/useDebounce"; +import { MWQuery } from "@/providers"; +import { useEffect, useMemo, useState } from "react"; +import { HomeView } from "./HomeView"; +import { SearchLoadingView } from "./SearchLoadingView"; +import { SearchResultsView } from "./SearchResultsView"; + +interface SearchResultsPartialProps { + search: MWQuery; +} + +export function SearchResultsPartial({ search }: SearchResultsPartialProps) { + const [searching, setSearching] = useState(false); + const [loading, setLoading] = useState(false); + + const debouncedSearch = useDebounce(search, 2000); + useEffect(() => { + setSearching(search.searchQuery !== ""); + setLoading(search.searchQuery !== ""); + }, [search]); + useEffect(() => { + setLoading(false); + }, [debouncedSearch]); + + const resultView = useMemo(() => { + if (loading) return ; + if (searching) return ; + return ; + }, [loading, searching, debouncedSearch]); + + return resultView; +} diff --git a/src/views/search/SearchResultsView.tsx b/src/views/search/SearchResultsView.tsx new file mode 100644 index 00000000..a948553c --- /dev/null +++ b/src/views/search/SearchResultsView.tsx @@ -0,0 +1,102 @@ +import { IconPatch } from "@/components/buttons/IconPatch"; +import { Icons } from "@/components/Icon"; +import { SectionHeading } from "@/components/layout/SectionHeading"; +import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; +import { useLoading } from "@/hooks/useLoading"; +import { MWMassProviderOutput, MWQuery, SearchProviders } from "@/providers"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { SearchLoadingView } from "./SearchLoadingView"; + +function SearchSuffix(props: { + fails: number; + total: number; + resultsSize: number; +}) { + const { t } = useTranslation(); + + const allFailed: boolean = props.fails === props.total; + const icon: Icons = allFailed ? Icons.WARNING : Icons.EYE_SLASH; + + return ( +
+ + + {/* standard suffix */} + {!allFailed ? ( +
+ {props.fails > 0 ? ( +

+ {t("search.providersFailed", { + fails: props.fails, + total: props.total, + })} +

+ ) : null} + {props.resultsSize > 0 ? ( +

{t("search.allResults")}

+ ) : ( +

{t("search.noResults")}

+ )} +
+ ) : null} + + {/* Error result */} + {allFailed ? ( +
+

{t("search.allFailed")}

+
+ ) : null} +
+ ); +} + +export function SearchResultsView({ searchQuery }: { searchQuery: MWQuery }) { + const { t } = useTranslation(); + + const [results, setResults] = useState(); + const [runSearchQuery, loading, error] = useLoading((query: MWQuery) => + SearchProviders(query) + ); + + useEffect(() => { + async function runSearch(query: MWQuery) { + const searchResults = await runSearchQuery(query); + if (!searchResults) return; + setResults(searchResults); + } + + if (searchQuery.searchQuery !== "") runSearch(searchQuery); + }, [searchQuery, runSearchQuery]); + + if (loading) return ; + if (error) return ; + if (!results) return null; + + return ( +
+ {results?.results.length > 0 ? ( + + {results.results.map((v) => ( + + ))} + + ) : null} + + +
+ ); +} diff --git a/src/views/search/SearchView.tsx b/src/views/search/SearchView.tsx new file mode 100644 index 00000000..d4f5e1d9 --- /dev/null +++ b/src/views/search/SearchView.tsx @@ -0,0 +1,54 @@ +import { useCallback, useState } from "react"; +import { Navigation } from "@/components/layout/Navigation"; +import { ThinContainer } from "@/components/layout/ThinContainer"; +import { SearchBarInput } from "@/components/SearchBar"; +import Sticky from "react-stickynode"; +import { Title } from "@/components/text/Title"; +import { useSearchQuery } from "@/hooks/useSearchQuery"; +import { useTranslation } from "react-i18next"; + +import { SearchResultsPartial } from "./SearchResultsPartial"; + +export function SearchView() { + const { t } = useTranslation(); + const [search, setSearch, setSearchUnFocus] = useSearchQuery(); + const [showBg, setShowBg] = useState(false); + + const stickStateChanged = useCallback( + ({ status }: Sticky.Status) => setShowBg(status === Sticky.STATUS_FIXED), + [setShowBg] + ); + + return ( + <> +
+ + +
+
+
+
+
+
+ {t("search.title")} +
+ + + +
+
+ +
+ + + + + ); +} diff --git a/vite.config.ts b/vite.config.ts index d02fa01f..c9788164 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,9 +1,10 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react-swc"; +import loadVersion from "vite-plugin-package-version"; import path from "path"; export default defineConfig({ - plugins: [react()], + plugins: [react(), loadVersion()], resolve: { alias: { "@": path.resolve(__dirname, "./src"), diff --git a/yarn.lock b/yarn.lock index 1e35f4e6..b454d8fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17,6 +17,9 @@ dependencies: "regenerator-runtime" "^0.13.11" +"@colors/colors@1.5.0": + "version" "1.5.0" + "@esbuild/linux-x64@0.16.5": "integrity" "sha512-vsOwzKN+4NenUTyuoWLmg5dAuO8JKuLD9MXSeENA385XucuOZbblmOMwwgPlHsgVRtSjz38riqPJU2ALI/CWYQ==" "resolved" "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.5.tgz" @@ -37,6 +40,9 @@ "minimatch" "^3.1.2" "strip-json-comments" "^3.1.1" +"@gar/promisify@^1.1.3": + "version" "1.1.3" + "@headlessui/react@^1.5.0": "integrity" "sha512-UZSxOfA0CYKO7QDT5OGlFvesvlR1SKkawwSjwQJwt7XQItpzRKdE3ZUQxHcg4LEz3C0Wler2s9psdb872ynwrQ==" "resolved" "https://registry.npmjs.org/@headlessui/react/-/react-1.7.5.tgz" @@ -63,6 +69,9 @@ "resolved" "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" "version" "1.2.1" +"@isaacs/string-locale-compare@^1.1.0": + "version" "1.1.0" + "@nodelib/fs.scandir@2.1.5": "integrity" "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==" "resolved" "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" @@ -84,6 +93,141 @@ "@nodelib/fs.scandir" "2.1.5" "fastq" "^1.6.0" +"@npmcli/arborist@^6.1.5": + "version" "6.1.5" + dependencies: + "@isaacs/string-locale-compare" "^1.1.0" + "@npmcli/fs" "^3.1.0" + "@npmcli/installed-package-contents" "^2.0.0" + "@npmcli/map-workspaces" "^3.0.0" + "@npmcli/metavuln-calculator" "^5.0.0" + "@npmcli/name-from-folder" "^1.0.1" + "@npmcli/node-gyp" "^3.0.0" + "@npmcli/package-json" "^3.0.0" + "@npmcli/query" "^3.0.0" + "@npmcli/run-script" "^6.0.0" + "bin-links" "^4.0.1" + "cacache" "^17.0.3" + "common-ancestor-path" "^1.0.1" + "hosted-git-info" "^6.1.1" + "json-parse-even-better-errors" "^3.0.0" + "json-stringify-nice" "^1.1.4" + "minimatch" "^5.1.1" + "nopt" "^7.0.0" + "npm-install-checks" "^6.0.0" + "npm-package-arg" "^10.1.0" + "npm-pick-manifest" "^8.0.1" + "npm-registry-fetch" "^14.0.3" + "npmlog" "^7.0.1" + "pacote" "^15.0.7" + "parse-conflict-json" "^3.0.0" + "proc-log" "^3.0.0" + "promise-all-reject-late" "^1.0.0" + "promise-call-limit" "^1.0.1" + "read-package-json-fast" "^3.0.1" + "semver" "^7.3.7" + "ssri" "^10.0.1" + "treeverse" "^3.0.0" + "walk-up-path" "^1.0.0" + +"@npmcli/config@^6.1.0": + "version" "6.1.0" + dependencies: + "@npmcli/map-workspaces" "^3.0.0" + "ini" "^3.0.0" + "nopt" "^7.0.0" + "proc-log" "^3.0.0" + "read-package-json-fast" "^3.0.0" + "semver" "^7.3.5" + "walk-up-path" "^1.0.0" + +"@npmcli/disparity-colors@^3.0.0": + "version" "3.0.0" + dependencies: + "ansi-styles" "^4.3.0" + +"@npmcli/fs@^2.1.0": + "version" "2.1.2" + dependencies: + "@gar/promisify" "^1.1.3" + "semver" "^7.3.5" + +"@npmcli/fs@^3.1.0": + "version" "3.1.0" + dependencies: + "semver" "^7.3.5" + +"@npmcli/git@^4.0.0", "@npmcli/git@^4.0.1": + "version" "4.0.3" + dependencies: + "@npmcli/promise-spawn" "^6.0.0" + "lru-cache" "^7.4.4" + "mkdirp" "^1.0.4" + "npm-pick-manifest" "^8.0.0" + "proc-log" "^3.0.0" + "promise-inflight" "^1.0.1" + "promise-retry" "^2.0.1" + "semver" "^7.3.5" + "which" "^3.0.0" + +"@npmcli/installed-package-contents@^2.0.0", "@npmcli/installed-package-contents@^2.0.1": + "version" "2.0.1" + dependencies: + "npm-bundled" "^3.0.0" + "npm-normalize-package-bin" "^3.0.0" + +"@npmcli/map-workspaces@^3.0.0": + "version" "3.0.0" + dependencies: + "@npmcli/name-from-folder" "^1.0.1" + "glob" "^8.0.1" + "minimatch" "^5.0.1" + "read-package-json-fast" "^3.0.0" + +"@npmcli/metavuln-calculator@^5.0.0": + "version" "5.0.0" + dependencies: + "cacache" "^17.0.0" + "json-parse-even-better-errors" "^3.0.0" + "pacote" "^15.0.0" + "semver" "^7.3.5" + +"@npmcli/move-file@^2.0.0": + "version" "2.0.1" + dependencies: + "mkdirp" "^1.0.4" + "rimraf" "^3.0.2" + +"@npmcli/name-from-folder@^1.0.1": + "version" "1.0.1" + +"@npmcli/node-gyp@^3.0.0": + "version" "3.0.0" + +"@npmcli/package-json@^3.0.0": + "version" "3.0.0" + dependencies: + "json-parse-even-better-errors" "^3.0.0" + +"@npmcli/promise-spawn@^6.0.0", "@npmcli/promise-spawn@^6.0.1": + "version" "6.0.1" + dependencies: + "which" "^3.0.0" + +"@npmcli/query@^3.0.0": + "version" "3.0.0" + dependencies: + "postcss-selector-parser" "^6.0.10" + +"@npmcli/run-script@^6.0.0": + "version" "6.0.0" + dependencies: + "@npmcli/node-gyp" "^3.0.0" + "@npmcli/promise-spawn" "^6.0.0" + "node-gyp" "^9.0.0" + "read-package-json-fast" "^3.0.0" + "which" "^3.0.0" + "@swc/core-linux-x64-gnu@1.3.22": "integrity" "sha512-FLkbiqsdXsVIFZi6iedx4rSBGX8x0vo/5aDlklSxJAAYOcQpO0QADKP5Yr65iMT1d6ABCt2d+/StpGLF7GWOcA==" "resolved" "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.22.tgz" @@ -110,6 +254,9 @@ "@swc/core-win32-ia32-msvc" "1.3.22" "@swc/core-win32-x64-msvc" "1.3.22" +"@tootallnate/once@2": + "version" "2.0.0" + "@types/crypto-js@^4.1.1": "integrity" "sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==" "resolved" "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.1.tgz" @@ -164,6 +311,13 @@ "@types/history" "^4.7.11" "@types/react" "*" +"@types/react-stickynode@^4.0.0": + "integrity" "sha512-PKkmOzF6WCNuyIKrvhidGeUPLfe8htPwfEljKnQBF4bA5v74ADvXtwkjavOH8i6aCSw9J14AyDDl1Ul0VNQJUg==" + "resolved" "https://registry.npmjs.org/@types/react-stickynode/-/react-stickynode-4.0.0.tgz" + "version" "4.0.0" + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@^17", "@types/react@^17.0.39": "integrity" "sha512-vwk8QqVODi0VaZZpDXQCmEmiOuyjEFPY7Ttaw5vjM112LOq37yz1CDJGrRJwA1fYEq4Iitd5rnjd1yWAc/bT+A==" "resolved" "https://registry.npmjs.org/@types/react/-/react-17.0.52.tgz" @@ -273,6 +427,17 @@ dependencies: "@swc/core" "^1.3.21" +"abbrev@^1.0.0": + "version" "1.1.1" + +"abbrev@^2.0.0": + "version" "2.0.0" + +"abort-controller@^3.0.0": + "version" "3.0.0" + dependencies: + "event-target-shim" "^5.0.0" + "acorn-jsx@^5.3.2": "integrity" "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==" "resolved" "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" @@ -302,6 +467,24 @@ "resolved" "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" "version" "7.4.1" +"agent-base@^6.0.2", "agent-base@6": + "version" "6.0.2" + dependencies: + "debug" "4" + +"agentkeepalive@^4.2.1": + "version" "4.2.1" + dependencies: + "debug" "^4.1.0" + "depd" "^1.1.2" + "humanize-ms" "^1.2.1" + +"aggregate-error@^3.0.0": + "version" "3.1.0" + dependencies: + "clean-stack" "^2.0.0" + "indent-string" "^4.0.0" + "ajv@^6.10.0", "ajv@^6.12.4": "integrity" "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==" "resolved" "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" @@ -317,7 +500,7 @@ "resolved" "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" "version" "5.0.1" -"ansi-styles@^4.1.0": +"ansi-styles@^4.1.0", "ansi-styles@^4.3.0": "integrity" "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==" "resolved" "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" "version" "4.3.0" @@ -332,6 +515,24 @@ "normalize-path" "^3.0.0" "picomatch" "^2.0.4" +"aproba@^1.0.3 || ^2.0.0", "aproba@^2.0.0": + "version" "2.0.0" + +"archy@~1.0.0": + "version" "1.0.0" + +"are-we-there-yet@^3.0.0": + "version" "3.0.1" + dependencies: + "delegates" "^1.0.0" + "readable-stream" "^3.6.0" + +"are-we-there-yet@^4.0.0": + "version" "4.0.0" + dependencies: + "delegates" "^1.0.0" + "readable-stream" "^4.1.0" + "arg@^5.0.2": "integrity" "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" "resolved" "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz" @@ -418,11 +619,25 @@ "resolved" "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" "version" "1.0.2" +"base64-js@^1.3.1": + "version" "1.5.1" + +"bin-links@^4.0.1": + "version" "4.0.1" + dependencies: + "cmd-shim" "^6.0.0" + "npm-normalize-package-bin" "^3.0.0" + "read-cmd-shim" "^4.0.0" + "write-file-atomic" "^5.0.0" + "binary-extensions@^2.0.0": "integrity" "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" "resolved" "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" "version" "2.2.0" +"binary-extensions@^2.2.0": + "version" "2.2.0" + "brace-expansion@^1.1.7": "integrity" "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==" "resolved" "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" @@ -431,6 +646,11 @@ "balanced-match" "^1.0.0" "concat-map" "0.0.1" +"brace-expansion@^2.0.1": + "version" "2.0.1" + dependencies: + "balanced-match" "^1.0.0" + "braces@^3.0.2", "braces@~3.0.2": "integrity" "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==" "resolved" "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" @@ -448,6 +668,56 @@ "node-releases" "^2.0.6" "update-browserslist-db" "^1.0.9" +"buffer@^6.0.3": + "version" "6.0.3" + dependencies: + "base64-js" "^1.3.1" + "ieee754" "^1.2.1" + +"builtins@^5.0.0": + "version" "5.0.1" + dependencies: + "semver" "^7.0.0" + +"cacache@^16.1.0": + "version" "16.1.3" + dependencies: + "@npmcli/fs" "^2.1.0" + "@npmcli/move-file" "^2.0.0" + "chownr" "^2.0.0" + "fs-minipass" "^2.1.0" + "glob" "^8.0.1" + "infer-owner" "^1.0.4" + "lru-cache" "^7.7.1" + "minipass" "^3.1.6" + "minipass-collect" "^1.0.2" + "minipass-flush" "^1.0.5" + "minipass-pipeline" "^1.2.4" + "mkdirp" "^1.0.4" + "p-map" "^4.0.0" + "promise-inflight" "^1.0.1" + "rimraf" "^3.0.2" + "ssri" "^9.0.0" + "tar" "^6.1.11" + "unique-filename" "^2.0.0" + +"cacache@^17.0.0", "cacache@^17.0.3": + "version" "17.0.3" + dependencies: + "@npmcli/fs" "^3.1.0" + "fs-minipass" "^2.1.0" + "glob" "^8.0.1" + "lru-cache" "^7.7.1" + "minipass" "^4.0.0" + "minipass-collect" "^1.0.2" + "minipass-flush" "^1.0.5" + "minipass-pipeline" "^1.2.4" + "p-map" "^4.0.0" + "promise-inflight" "^1.0.1" + "ssri" "^10.0.0" + "tar" "^6.1.11" + "unique-filename" "^3.0.0" + "call-bind@^1.0.0", "call-bind@^1.0.2": "integrity" "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==" "resolved" "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" @@ -471,7 +741,7 @@ "resolved" "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001439.tgz" "version" "1.0.30001439" -"chalk@^4.0.0": +"chalk@^4.0.0", "chalk@^4.1.0", "chalk@^4.1.2": "integrity" "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==" "resolved" "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" "version" "4.1.2" @@ -494,11 +764,49 @@ optionalDependencies: "fsevents" "~2.3.2" +"chownr@^2.0.0": + "version" "2.0.0" + +"ci-info@^3.7.0": + "version" "3.7.0" + +"cidr-regex@^3.1.1": + "version" "3.1.1" + dependencies: + "ip-regex" "^4.1.0" + +"classnames@^2.0.0": + "integrity" "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + "resolved" "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz" + "version" "2.3.2" + +"clean-stack@^2.0.0": + "version" "2.2.0" + +"cli-columns@^4.0.0": + "version" "4.0.0" + dependencies: + "string-width" "^4.2.3" + "strip-ansi" "^6.0.1" + +"cli-table3@^0.6.3": + "version" "0.6.3" + dependencies: + "string-width" "^4.2.0" + optionalDependencies: + "@colors/colors" "1.5.0" + "client-only@^0.0.1": "integrity" "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" "resolved" "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz" "version" "0.0.1" +"clone@^1.0.2": + "version" "1.0.4" + +"cmd-shim@^6.0.0": + "version" "6.0.0" + "color-convert@^2.0.1": "integrity" "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==" "resolved" "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" @@ -511,6 +819,18 @@ "resolved" "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" "version" "1.1.4" +"color-support@^1.1.3": + "version" "1.1.3" + +"columnify@^1.6.0": + "version" "1.6.0" + dependencies: + "strip-ansi" "^6.0.1" + "wcwidth" "^1.0.0" + +"common-ancestor-path@^1.0.1": + "version" "1.0.1" + "concat-map@0.0.1": "integrity" "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" "resolved" "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" @@ -521,11 +841,19 @@ "resolved" "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz" "version" "1.0.11" +"console-control-strings@^1.1.0": + "version" "1.1.0" + "core-js-pure@^3.25.1": "integrity" "sha512-VVXcDpp/xJ21KdULRq/lXdLzQAtX7+37LzpyfFM973il0tWSsDEoyzG38G14AjTpK9VTfiNM9jnFauq/CpaWGQ==" "resolved" "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.26.1.tgz" "version" "3.26.1" +"core-js@^3.6.5": + "integrity" "sha512-GutwJLBChfGCpwwhbYoqfv03LAfmiz7e7D/BNxzeMxwQf10GRSzqiOjx7AmtEk+heiD/JWmBuyBPgFtx0Sg1ww==" + "resolved" "https://registry.npmjs.org/core-js/-/core-js-3.27.1.tgz" + "version" "3.27.1" + "cross-fetch@3.1.5": "integrity" "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==" "resolved" "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz" @@ -576,6 +904,11 @@ dependencies: "ms" "^2.1.1" +"debug@^4.1.0", "debug@^4.3.3", "debug@4": + "version" "4.3.4" + dependencies: + "ms" "2.1.2" + "debug@^4.1.1", "debug@^4.3.2", "debug@^4.3.4": "integrity" "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==" "resolved" "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" @@ -588,6 +921,11 @@ "resolved" "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" "version" "0.1.4" +"defaults@^1.0.3": + "version" "1.0.3" + dependencies: + "clone" "^1.0.2" + "define-properties@^1.1.3", "define-properties@^1.1.4": "integrity" "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==" "resolved" "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz" @@ -601,6 +939,12 @@ "resolved" "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz" "version" "1.0.1" +"delegates@^1.0.0": + "version" "1.0.0" + +"depd@^1.1.2": + "version" "1.1.2" + "detective@^5.2.1": "integrity" "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==" "resolved" "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz" @@ -615,6 +959,9 @@ "resolved" "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz" "version" "1.2.2" +"diff@^5.1.0": + "version" "5.1.0" + "dir-glob@^3.0.1": "integrity" "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==" "resolved" "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" @@ -646,11 +993,25 @@ "resolved" "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz" "version" "1.4.284" +"emoji-regex@^8.0.0": + "version" "8.0.0" + "emoji-regex@^9.2.2": "integrity" "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" "resolved" "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" "version" "9.2.2" +"encoding@^0.1.13": + "version" "0.1.13" + dependencies: + "iconv-lite" "^0.6.2" + +"env-paths@^2.2.0": + "version" "2.2.1" + +"err-code@^2.0.2": + "version" "2.0.3" + "es-abstract@^1.19.0", "es-abstract@^1.20.4": "integrity" "sha512-7h8MM2EQhsCA7pU/Nv78qOXFpD8Rhqd12gYiSJVkrH9+e8VuA8JlPJK/hQjjlLv6pJvx/z1iRFKzYb0XT/RuAQ==" "resolved" "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.5.tgz" @@ -965,6 +1326,17 @@ "resolved" "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" "version" "2.0.3" +"event-target-shim@^5.0.0": + "version" "5.0.1" + +"eventemitter3@^3.0.0": + "integrity" "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" + "resolved" "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz" + "version" "3.1.2" + +"events@^3.3.0": + "version" "3.3.0" + "fast-deep-equal@^3.1.1", "fast-deep-equal@^3.1.3": "integrity" "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" "resolved" "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" @@ -991,6 +1363,9 @@ "resolved" "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" "version" "2.0.6" +"fastest-levenshtein@^1.0.16": + "version" "1.0.16" + "fastq@^1.6.0": "integrity" "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==" "resolved" "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz" @@ -1038,6 +1413,11 @@ "resolved" "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz" "version" "4.2.0" +"fs-minipass@^2.0.0", "fs-minipass@^2.1.0": + "version" "2.1.0" + dependencies: + "minipass" "^3.0.0" + "fs.realpath@^1.0.0": "integrity" "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" "resolved" "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" @@ -1068,6 +1448,30 @@ "resolved" "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz" "version" "6.6.2" +"gauge@^4.0.3": + "version" "4.0.4" + dependencies: + "aproba" "^1.0.3 || ^2.0.0" + "color-support" "^1.1.3" + "console-control-strings" "^1.1.0" + "has-unicode" "^2.0.1" + "signal-exit" "^3.0.7" + "string-width" "^4.2.3" + "strip-ansi" "^6.0.1" + "wide-align" "^1.1.5" + +"gauge@^5.0.0": + "version" "5.0.0" + dependencies: + "aproba" "^1.0.3 || ^2.0.0" + "color-support" "^1.1.3" + "console-control-strings" "^1.1.0" + "has-unicode" "^2.0.1" + "signal-exit" "^3.0.7" + "string-width" "^4.2.3" + "strip-ansi" "^6.0.1" + "wide-align" "^1.1.5" + "get-intrinsic@^1.0.2", "get-intrinsic@^1.1.1", "get-intrinsic@^1.1.3": "integrity" "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==" "resolved" "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz" @@ -1118,6 +1522,25 @@ "once" "^1.3.0" "path-is-absolute" "^1.0.0" +"glob@^7.1.4": + "version" "7.2.3" + dependencies: + "fs.realpath" "^1.0.0" + "inflight" "^1.0.4" + "inherits" "2" + "minimatch" "^3.1.1" + "once" "^1.3.0" + "path-is-absolute" "^1.0.0" + +"glob@^8.0.1": + "version" "8.0.3" + dependencies: + "fs.realpath" "^1.0.0" + "inflight" "^1.0.4" + "inherits" "2" + "minimatch" "^5.0.1" + "once" "^1.3.0" + "globals@^13.15.0": "integrity" "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==" "resolved" "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz" @@ -1144,6 +1567,9 @@ dependencies: "get-intrinsic" "^1.1.3" +"graceful-fs@^4.2.10", "graceful-fs@^4.2.6": + "version" "4.2.10" + "grapheme-splitter@^1.0.4": "integrity" "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" "resolved" "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz" @@ -1178,6 +1604,9 @@ dependencies: "has-symbols" "^1.0.2" +"has-unicode@^2.0.1": + "version" "2.0.1" + "has@^1.0.3": "integrity" "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==" "resolved" "https://registry.npmjs.org/has/-/has-1.0.3.tgz" @@ -1209,6 +1638,11 @@ dependencies: "react-is" "^16.7.0" +"hosted-git-info@^6.0.0", "hosted-git-info@^6.1.1": + "version" "6.1.1" + dependencies: + "lru-cache" "^7.5.1" + "html-parse-stringify@^3.0.1": "integrity" "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==" "resolved" "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz" @@ -1216,6 +1650,32 @@ dependencies: "void-elements" "3.1.0" +"http-cache-semantics@^4.1.0": + "version" "4.1.0" + +"http-proxy-agent@^5.0.0": + "version" "5.0.0" + dependencies: + "@tootallnate/once" "2" + "agent-base" "6" + "debug" "4" + +"https-proxy-agent@^5.0.0": + "version" "5.0.1" + dependencies: + "agent-base" "6" + "debug" "4" + +"humanize-ms@^1.2.1": + "version" "1.2.1" + dependencies: + "ms" "^2.0.0" + +"i@^0.3.7": + "integrity" "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==" + "resolved" "https://registry.npmjs.org/i/-/i-0.3.7.tgz" + "version" "0.3.7" + "i18next-browser-languagedetector@^7.0.1": "integrity" "sha512-Pa5kFwaczXJAeHE56CHG2aWzFBMJNUNghf0Pm4SwSrEMps/PTKqW90EYWlIvhuYStf3Sn1K0vw+gH3+TLdkH1g==" "resolved" "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.0.1.tgz" @@ -1237,6 +1697,19 @@ dependencies: "@babel/runtime" "^7.20.6" +"iconv-lite@^0.6.2": + "version" "0.6.3" + dependencies: + "safer-buffer" ">= 2.1.2 < 3.0.0" + +"ieee754@^1.2.1": + "version" "1.2.1" + +"ignore-walk@^6.0.0": + "version" "6.0.0" + dependencies: + "minimatch" "^5.0.1" + "ignore@^5.2.0": "integrity" "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==" "resolved" "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz" @@ -1255,6 +1728,12 @@ "resolved" "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" "version" "0.1.4" +"indent-string@^4.0.0": + "version" "4.0.0" + +"infer-owner@^1.0.4": + "version" "1.0.4" + "inflight@^1.0.4": "integrity" "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==" "resolved" "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" @@ -1263,11 +1742,25 @@ "once" "^1.3.0" "wrappy" "1" -"inherits@2": +"inherits@^2.0.3", "inherits@2": "integrity" "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "resolved" "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" "version" "2.0.4" +"ini@^3.0.0", "ini@^3.0.1": + "version" "3.0.1" + +"init-package-json@^4.0.1": + "version" "4.0.1" + dependencies: + "npm-package-arg" "^10.0.0" + "promzard" "^0.3.0" + "read" "^1.0.7" + "read-package-json" "^6.0.0" + "semver" "^7.3.5" + "validate-npm-package-license" "^3.0.4" + "validate-npm-package-name" "^5.0.0" + "internal-slot@^1.0.3": "integrity" "sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==" "resolved" "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.4.tgz" @@ -1277,6 +1770,12 @@ "has" "^1.0.3" "side-channel" "^1.0.4" +"ip-regex@^4.1.0": + "version" "4.3.0" + +"ip@^2.0.0": + "version" "2.0.0" + "is-bigint@^1.0.1": "integrity" "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==" "resolved" "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz" @@ -1304,6 +1803,11 @@ "resolved" "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" "version" "1.2.7" +"is-cidr@^4.0.2": + "version" "4.0.2" + dependencies: + "cidr-regex" "^3.1.1" + "is-core-module@^2.8.1", "is-core-module@^2.9.0": "integrity" "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==" "resolved" "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz" @@ -1323,6 +1827,9 @@ "resolved" "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" "version" "2.1.1" +"is-fullwidth-code-point@^3.0.0": + "version" "3.0.0" + "is-glob@^4.0.0", "is-glob@^4.0.1", "is-glob@^4.0.3", "is-glob@~4.0.1": "integrity" "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==" "resolved" "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" @@ -1330,6 +1837,9 @@ dependencies: "is-extglob" "^2.1.1" +"is-lambda@^1.0.1": + "version" "1.0.1" + "is-negative-zero@^2.0.2": "integrity" "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==" "resolved" "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz" @@ -1415,6 +1925,9 @@ dependencies: "argparse" "^2.0.1" +"json-parse-even-better-errors@^3.0.0": + "version" "3.0.0" + "json-schema-traverse@^0.4.1": "integrity" "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" "resolved" "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" @@ -1425,6 +1938,9 @@ "resolved" "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" "version" "1.0.1" +"json-stringify-nice@^1.1.4": + "version" "1.1.4" + "json5@^1.0.1": "integrity" "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==" "resolved" "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz" @@ -1437,6 +1953,9 @@ "resolved" "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz" "version" "2.2.1" +"jsonparse@^1.3.1": + "version" "1.3.1" + "jsx-ast-utils@^2.4.1 || ^3.0.0", "jsx-ast-utils@^3.3.2": "integrity" "sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==" "resolved" "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz" @@ -1445,6 +1964,12 @@ "array-includes" "^3.1.5" "object.assign" "^4.1.3" +"just-diff-apply@^5.2.0": + "version" "5.4.1" + +"just-diff@^5.0.1": + "version" "5.1.1" + "language-subtag-registry@^0.3.20": "integrity" "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==" "resolved" "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz" @@ -1465,6 +1990,95 @@ "prelude-ls" "^1.2.1" "type-check" "~0.4.0" +"libnpmaccess@^7.0.1": + "version" "7.0.1" + dependencies: + "npm-package-arg" "^10.1.0" + "npm-registry-fetch" "^14.0.3" + +"libnpmdiff@^5.0.6": + "version" "5.0.6" + dependencies: + "@npmcli/arborist" "^6.1.5" + "@npmcli/disparity-colors" "^3.0.0" + "@npmcli/installed-package-contents" "^2.0.0" + "binary-extensions" "^2.2.0" + "diff" "^5.1.0" + "minimatch" "^5.1.1" + "npm-package-arg" "^10.1.0" + "pacote" "^15.0.7" + "tar" "^6.1.13" + +"libnpmexec@^5.0.6": + "version" "5.0.6" + dependencies: + "@npmcli/arborist" "^6.1.5" + "@npmcli/run-script" "^6.0.0" + "chalk" "^4.1.0" + "ci-info" "^3.7.0" + "npm-package-arg" "^10.1.0" + "npmlog" "^7.0.1" + "pacote" "^15.0.7" + "proc-log" "^3.0.0" + "read" "^1.0.7" + "read-package-json-fast" "^3.0.1" + "semver" "^7.3.7" + "walk-up-path" "^1.0.0" + +"libnpmfund@^4.0.6": + "version" "4.0.6" + dependencies: + "@npmcli/arborist" "^6.1.5" + +"libnpmhook@^9.0.1": + "version" "9.0.1" + dependencies: + "aproba" "^2.0.0" + "npm-registry-fetch" "^14.0.3" + +"libnpmorg@^5.0.1": + "version" "5.0.1" + dependencies: + "aproba" "^2.0.0" + "npm-registry-fetch" "^14.0.3" + +"libnpmpack@^5.0.6": + "version" "5.0.6" + dependencies: + "@npmcli/arborist" "^6.1.5" + "@npmcli/run-script" "^6.0.0" + "npm-package-arg" "^10.1.0" + "pacote" "^15.0.7" + +"libnpmpublish@^7.0.6": + "version" "7.0.6" + dependencies: + "normalize-package-data" "^5.0.0" + "npm-package-arg" "^10.1.0" + "npm-registry-fetch" "^14.0.3" + "semver" "^7.3.7" + "ssri" "^10.0.1" + +"libnpmsearch@^6.0.1": + "version" "6.0.1" + dependencies: + "npm-registry-fetch" "^14.0.3" + +"libnpmteam@^5.0.1": + "version" "5.0.1" + dependencies: + "aproba" "^2.0.0" + "npm-registry-fetch" "^14.0.3" + +"libnpmversion@^4.0.1": + "version" "4.0.1" + dependencies: + "@npmcli/git" "^4.0.1" + "@npmcli/run-script" "^6.0.0" + "json-parse-even-better-errors" "^3.0.0" + "proc-log" "^3.0.0" + "semver" "^7.3.7" + "lilconfig@^2.0.5", "lilconfig@^2.0.6": "integrity" "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==" "resolved" "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz" @@ -1482,6 +2096,11 @@ "resolved" "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" "version" "4.6.2" +"lodash@^4.17.15": + "integrity" "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "resolved" "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + "version" "4.17.21" + "loose-envify@^1.1.0", "loose-envify@^1.2.0", "loose-envify@^1.3.1", "loose-envify@^1.4.0": "integrity" "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==" "resolved" "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" @@ -1496,6 +2115,49 @@ dependencies: "yallist" "^4.0.0" +"lru-cache@^7.4.4", "lru-cache@^7.5.1", "lru-cache@^7.7.1": + "version" "7.13.2" + +"make-fetch-happen@^10.0.3": + "version" "10.2.1" + dependencies: + "agentkeepalive" "^4.2.1" + "cacache" "^16.1.0" + "http-cache-semantics" "^4.1.0" + "http-proxy-agent" "^5.0.0" + "https-proxy-agent" "^5.0.0" + "is-lambda" "^1.0.1" + "lru-cache" "^7.7.1" + "minipass" "^3.1.6" + "minipass-collect" "^1.0.2" + "minipass-fetch" "^2.0.3" + "minipass-flush" "^1.0.5" + "minipass-pipeline" "^1.2.4" + "negotiator" "^0.6.3" + "promise-retry" "^2.0.1" + "socks-proxy-agent" "^7.0.0" + "ssri" "^9.0.0" + +"make-fetch-happen@^11.0.0", "make-fetch-happen@^11.0.2": + "version" "11.0.2" + dependencies: + "agentkeepalive" "^4.2.1" + "cacache" "^17.0.0" + "http-cache-semantics" "^4.1.0" + "http-proxy-agent" "^5.0.0" + "https-proxy-agent" "^5.0.0" + "is-lambda" "^1.0.1" + "lru-cache" "^7.7.1" + "minipass" "^4.0.0" + "minipass-collect" "^1.0.2" + "minipass-fetch" "^3.0.0" + "minipass-flush" "^1.0.5" + "minipass-pipeline" "^1.2.4" + "negotiator" "^0.6.3" + "promise-retry" "^2.0.1" + "socks-proxy-agent" "^7.0.0" + "ssri" "^10.0.0" + "merge2@^1.3.0", "merge2@^1.4.1": "integrity" "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" "resolved" "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" @@ -1516,11 +2178,87 @@ dependencies: "brace-expansion" "^1.1.7" +"minimatch@^5.0.1", "minimatch@^5.1.1": + "version" "5.1.1" + dependencies: + "brace-expansion" "^2.0.1" + "minimist@^1.2.0", "minimist@^1.2.6": "integrity" "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" "resolved" "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz" "version" "1.2.7" +"minipass-collect@^1.0.2": + "version" "1.0.2" + dependencies: + "minipass" "^3.0.0" + +"minipass-fetch@^2.0.3": + "version" "2.1.2" + dependencies: + "minipass" "^3.1.6" + "minipass-sized" "^1.0.3" + "minizlib" "^2.1.2" + optionalDependencies: + "encoding" "^0.1.13" + +"minipass-fetch@^3.0.0": + "version" "3.0.0" + dependencies: + "minipass" "^3.1.6" + "minipass-sized" "^1.0.3" + "minizlib" "^2.1.2" + optionalDependencies: + "encoding" "^0.1.13" + +"minipass-flush@^1.0.5": + "version" "1.0.5" + dependencies: + "minipass" "^3.0.0" + +"minipass-json-stream@^1.0.1": + "version" "1.0.1" + dependencies: + "jsonparse" "^1.3.1" + "minipass" "^3.0.0" + +"minipass-pipeline@^1.2.4": + "version" "1.2.4" + dependencies: + "minipass" "^3.0.0" + +"minipass-sized@^1.0.3": + "version" "1.0.3" + dependencies: + "minipass" "^3.0.0" + +"minipass@^3.0.0": + "version" "3.3.6" + dependencies: + "yallist" "^4.0.0" + +"minipass@^3.1.1", "minipass@^3.1.6": + "version" "3.3.6" + dependencies: + "yallist" "^4.0.0" + +"minipass@^4.0.0": + "version" "4.0.0" + dependencies: + "yallist" "^4.0.0" + +"minizlib@^2.1.1", "minizlib@^2.1.2": + "version" "2.1.2" + dependencies: + "minipass" "^3.0.0" + "yallist" "^4.0.0" + +"mkdirp@^1.0.3", "mkdirp@^1.0.4": + "version" "1.0.4" + +"ms@^2.0.0", "ms@^2.1.2": + "version" "2.1.3" + "ms@^2.1.1", "ms@2.1.2": "integrity" "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "resolved" "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" @@ -1531,6 +2269,9 @@ "resolved" "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" "version" "2.0.0" +"mute-stream@~0.0.4": + "version" "0.0.8" + "nanoid@^3.3.4": "integrity" "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" "resolved" "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz" @@ -1551,6 +2292,9 @@ "resolved" "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" "version" "1.4.0" +"negotiator@^0.6.3": + "version" "0.6.3" + "node-fetch@2.6.7": "integrity" "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==" "resolved" "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz" @@ -1558,11 +2302,43 @@ dependencies: "whatwg-url" "^5.0.0" +"node-gyp@^9.0.0", "node-gyp@^9.3.0": + "version" "9.3.0" + dependencies: + "env-paths" "^2.2.0" + "glob" "^7.1.4" + "graceful-fs" "^4.2.6" + "make-fetch-happen" "^10.0.3" + "nopt" "^6.0.0" + "npmlog" "^6.0.0" + "rimraf" "^3.0.2" + "semver" "^7.3.5" + "tar" "^6.1.2" + "which" "^2.0.2" + "node-releases@^2.0.6": "integrity" "sha512-EJ3rzxL9pTWPjk5arA0s0dgXpnyiAbJDE6wHT62g7VsgrgQgmmZ+Ru++M1BFofncWja+Pnn3rEr3fieRySAdKQ==" "resolved" "https://registry.npmjs.org/node-releases/-/node-releases-2.0.7.tgz" "version" "2.0.7" +"nopt@^6.0.0": + "version" "6.0.0" + dependencies: + "abbrev" "^1.0.0" + +"nopt@^7.0.0": + "version" "7.0.0" + dependencies: + "abbrev" "^2.0.0" + +"normalize-package-data@^5.0.0": + "version" "5.0.0" + dependencies: + "hosted-git-info" "^6.0.0" + "is-core-module" "^2.8.1" + "semver" "^7.3.5" + "validate-npm-package-license" "^3.0.4" + "normalize-path@^3.0.0", "normalize-path@~3.0.0": "integrity" "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" "resolved" "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" @@ -1573,6 +2349,155 @@ "resolved" "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" "version" "0.1.2" +"npm-audit-report@^4.0.0": + "version" "4.0.0" + dependencies: + "chalk" "^4.0.0" + +"npm-bundled@^3.0.0": + "version" "3.0.0" + dependencies: + "npm-normalize-package-bin" "^3.0.0" + +"npm-install-checks@^6.0.0": + "version" "6.0.0" + dependencies: + "semver" "^7.1.1" + +"npm-normalize-package-bin@^3.0.0": + "version" "3.0.0" + +"npm-package-arg@^10.0.0", "npm-package-arg@^10.1.0": + "version" "10.1.0" + dependencies: + "hosted-git-info" "^6.0.0" + "proc-log" "^3.0.0" + "semver" "^7.3.5" + "validate-npm-package-name" "^5.0.0" + +"npm-packlist@^7.0.0": + "version" "7.0.4" + dependencies: + "ignore-walk" "^6.0.0" + +"npm-pick-manifest@^8.0.0", "npm-pick-manifest@^8.0.1": + "version" "8.0.1" + dependencies: + "npm-install-checks" "^6.0.0" + "npm-normalize-package-bin" "^3.0.0" + "npm-package-arg" "^10.0.0" + "semver" "^7.3.5" + +"npm-profile@^7.0.1": + "version" "7.0.1" + dependencies: + "npm-registry-fetch" "^14.0.0" + "proc-log" "^3.0.0" + +"npm-registry-fetch@^14.0.0", "npm-registry-fetch@^14.0.3": + "version" "14.0.3" + dependencies: + "make-fetch-happen" "^11.0.0" + "minipass" "^4.0.0" + "minipass-fetch" "^3.0.0" + "minipass-json-stream" "^1.0.1" + "minizlib" "^2.1.2" + "npm-package-arg" "^10.0.0" + "proc-log" "^3.0.0" + +"npm-user-validate@^1.0.1": + "version" "1.0.1" + +"npm@^9.2.0": + "integrity" "sha512-oypVdaWGHDuV79RXLvp+B9gh6gDyAmoHKrQ0/JBYTWWx5D8/+AAxFdZC84fSIiyDdyW4qfrSyYGKhekxDOaMXQ==" + "resolved" "https://registry.npmjs.org/npm/-/npm-9.2.0.tgz" + "version" "9.2.0" + dependencies: + "@isaacs/string-locale-compare" "^1.1.0" + "@npmcli/arborist" "^6.1.5" + "@npmcli/config" "^6.1.0" + "@npmcli/map-workspaces" "^3.0.0" + "@npmcli/package-json" "^3.0.0" + "@npmcli/run-script" "^6.0.0" + "abbrev" "^2.0.0" + "archy" "~1.0.0" + "cacache" "^17.0.3" + "chalk" "^4.1.2" + "ci-info" "^3.7.0" + "cli-columns" "^4.0.0" + "cli-table3" "^0.6.3" + "columnify" "^1.6.0" + "fastest-levenshtein" "^1.0.16" + "fs-minipass" "^2.1.0" + "glob" "^8.0.1" + "graceful-fs" "^4.2.10" + "hosted-git-info" "^6.1.1" + "ini" "^3.0.1" + "init-package-json" "^4.0.1" + "is-cidr" "^4.0.2" + "json-parse-even-better-errors" "^3.0.0" + "libnpmaccess" "^7.0.1" + "libnpmdiff" "^5.0.6" + "libnpmexec" "^5.0.6" + "libnpmfund" "^4.0.6" + "libnpmhook" "^9.0.1" + "libnpmorg" "^5.0.1" + "libnpmpack" "^5.0.6" + "libnpmpublish" "^7.0.6" + "libnpmsearch" "^6.0.1" + "libnpmteam" "^5.0.1" + "libnpmversion" "^4.0.1" + "make-fetch-happen" "^11.0.2" + "minimatch" "^5.1.1" + "minipass" "^4.0.0" + "minipass-pipeline" "^1.2.4" + "mkdirp" "^1.0.4" + "ms" "^2.1.2" + "node-gyp" "^9.3.0" + "nopt" "^7.0.0" + "npm-audit-report" "^4.0.0" + "npm-install-checks" "^6.0.0" + "npm-package-arg" "^10.1.0" + "npm-pick-manifest" "^8.0.1" + "npm-profile" "^7.0.1" + "npm-registry-fetch" "^14.0.3" + "npm-user-validate" "^1.0.1" + "npmlog" "^7.0.1" + "p-map" "^4.0.0" + "pacote" "^15.0.7" + "parse-conflict-json" "^3.0.0" + "proc-log" "^3.0.0" + "qrcode-terminal" "^0.12.0" + "read" "~1.0.7" + "read-package-json" "^6.0.0" + "read-package-json-fast" "^3.0.1" + "rimraf" "^3.0.2" + "semver" "^7.3.8" + "ssri" "^10.0.1" + "tar" "^6.1.13" + "text-table" "~0.2.0" + "tiny-relative-date" "^1.3.0" + "treeverse" "^3.0.0" + "validate-npm-package-name" "^5.0.0" + "which" "^3.0.0" + "write-file-atomic" "^5.0.0" + +"npmlog@^6.0.0": + "version" "6.0.2" + dependencies: + "are-we-there-yet" "^3.0.0" + "console-control-strings" "^1.1.0" + "gauge" "^4.0.3" + "set-blocking" "^2.0.0" + +"npmlog@^7.0.1": + "version" "7.0.1" + dependencies: + "are-we-there-yet" "^4.0.0" + "console-control-strings" "^1.1.0" + "gauge" "^5.0.0" + "set-blocking" "^2.0.0" + "object-assign@^4.1.1": "integrity" "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" "resolved" "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" @@ -1671,6 +2596,32 @@ dependencies: "p-limit" "^3.0.2" +"p-map@^4.0.0": + "version" "4.0.0" + dependencies: + "aggregate-error" "^3.0.0" + +"pacote@^15.0.0", "pacote@^15.0.7": + "version" "15.0.7" + dependencies: + "@npmcli/git" "^4.0.0" + "@npmcli/installed-package-contents" "^2.0.1" + "@npmcli/promise-spawn" "^6.0.1" + "@npmcli/run-script" "^6.0.0" + "cacache" "^17.0.0" + "fs-minipass" "^2.1.0" + "minipass" "^4.0.0" + "npm-package-arg" "^10.0.0" + "npm-packlist" "^7.0.0" + "npm-pick-manifest" "^8.0.0" + "npm-registry-fetch" "^14.0.0" + "proc-log" "^3.0.0" + "promise-retry" "^2.0.1" + "read-package-json" "^6.0.0" + "read-package-json-fast" "^3.0.0" + "ssri" "^10.0.0" + "tar" "^6.1.11" + "parent-module@^1.0.0": "integrity" "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==" "resolved" "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" @@ -1678,6 +2629,13 @@ dependencies: "callsites" "^3.0.0" +"parse-conflict-json@^3.0.0": + "version" "3.0.0" + dependencies: + "json-parse-even-better-errors" "^3.0.0" + "just-diff" "^5.0.1" + "just-diff-apply" "^5.2.0" + "path-exists@^4.0.0": "integrity" "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" "resolved" "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" @@ -1710,6 +2668,11 @@ "resolved" "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" "version" "4.0.0" +"performance-now@^2.1.0": + "integrity" "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + "resolved" "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" + "version" "2.1.0" + "picocolors@^1.0.0": "integrity" "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" "resolved" "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" @@ -1793,7 +2756,33 @@ "resolved" "https://registry.npmjs.org/prettier/-/prettier-2.8.1.tgz" "version" "2.8.1" -"prop-types@^15.6.2", "prop-types@^15.8.1": +"proc-log@^3.0.0": + "version" "3.0.0" + +"process@^0.11.10": + "version" "0.11.10" + +"promise-all-reject-late@^1.0.0": + "version" "1.0.1" + +"promise-call-limit@^1.0.1": + "version" "1.0.1" + +"promise-inflight@^1.0.1": + "version" "1.0.1" + +"promise-retry@^2.0.1": + "version" "2.0.1" + dependencies: + "err-code" "^2.0.2" + "retry" "^0.12.0" + +"promzard@^0.3.0": + "version" "0.3.0" + dependencies: + "read" "1" + +"prop-types@^15.6.0", "prop-types@^15.6.2", "prop-types@^15.8.1": "integrity" "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==" "resolved" "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" "version" "15.8.1" @@ -1807,6 +2796,9 @@ "resolved" "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" "version" "2.1.1" +"qrcode-terminal@^0.12.0": + "version" "0.12.0" + "queue-microtask@^1.2.2": "integrity" "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" "resolved" "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" @@ -1817,7 +2809,14 @@ "resolved" "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz" "version" "5.1.1" -"react-dom@^16 || ^17 || ^18", "react-dom@^17.0.2": +"raf@^3.0.0": + "integrity" "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==" + "resolved" "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz" + "version" "3.4.1" + dependencies: + "performance-now" "^2.1.0" + +"react-dom@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^16 || ^17 || ^18", "react-dom@^17.0.2": "integrity" "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==" "resolved" "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz" "version" "17.0.2" @@ -1867,7 +2866,18 @@ "tiny-invariant" "^1.0.2" "tiny-warning" "^1.0.0" -"react@^16 || ^17 || ^18", "react@^17.0.2", "react@>= 16.8.0", "react@>=15", "react@17.0.2": +"react-stickynode@^4.1.0": + "integrity" "sha512-zylWgfad75jLfh/gYIayDcDWIDwO4weZrsZqDpjZ/axhF06zRjdCWFBgUr33Pvv2+htKWqPSFksWTyB6aMQ1ZQ==" + "resolved" "https://registry.npmjs.org/react-stickynode/-/react-stickynode-4.1.0.tgz" + "version" "4.1.0" + dependencies: + "classnames" "^2.0.0" + "core-js" "^3.6.5" + "prop-types" "^15.6.0" + "shallowequal" "^1.0.0" + "subscribe-ui-event" "^2.0.6" + +"react@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16 || ^17 || ^18", "react@^17.0.2", "react@>= 16.8.0", "react@>=15", "react@17.0.2": "integrity" "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==" "resolved" "https://registry.npmjs.org/react/-/react-17.0.2.tgz" "version" "17.0.2" @@ -1882,6 +2892,43 @@ dependencies: "pify" "^2.3.0" +"read-cmd-shim@^4.0.0": + "version" "4.0.0" + +"read-package-json-fast@^3.0.0", "read-package-json-fast@^3.0.1": + "version" "3.0.1" + dependencies: + "json-parse-even-better-errors" "^3.0.0" + "npm-normalize-package-bin" "^3.0.0" + +"read-package-json@^6.0.0": + "version" "6.0.0" + dependencies: + "glob" "^8.0.1" + "json-parse-even-better-errors" "^3.0.0" + "normalize-package-data" "^5.0.0" + "npm-normalize-package-bin" "^3.0.0" + +"read@^1.0.7", "read@~1.0.7", "read@1": + "version" "1.0.7" + dependencies: + "mute-stream" "~0.0.4" + +"readable-stream@^3.6.0": + "version" "3.6.0" + dependencies: + "inherits" "^2.0.3" + "string_decoder" "^1.1.1" + "util-deprecate" "^1.0.1" + +"readable-stream@^4.1.0": + "version" "4.2.0" + dependencies: + "abort-controller" "^3.0.0" + "buffer" "^6.0.3" + "events" "^3.3.0" + "process" "^0.11.10" + "readdirp@~3.6.0": "integrity" "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==" "resolved" "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" @@ -1936,6 +2983,9 @@ "path-parse" "^1.0.7" "supports-preserve-symlinks-flag" "^1.0.0" +"retry@^0.12.0": + "version" "0.12.0" + "reusify@^1.0.4": "integrity" "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" "resolved" "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" @@ -1962,6 +3012,9 @@ dependencies: "queue-microtask" "^1.2.2" +"safe-buffer@~5.2.0": + "version" "5.2.1" + "safe-regex-test@^1.0.0": "integrity" "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==" "resolved" "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz" @@ -1971,6 +3024,9 @@ "get-intrinsic" "^1.1.3" "is-regex" "^1.1.4" +"safer-buffer@>= 2.1.2 < 3.0.0": + "version" "2.1.2" + "scheduler@^0.20.2": "integrity" "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==" "resolved" "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz" @@ -1984,13 +3040,21 @@ "resolved" "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" "version" "6.3.0" -"semver@^7.3.7": +"semver@^7.0.0", "semver@^7.1.1", "semver@^7.3.5", "semver@^7.3.7", "semver@^7.3.8": "integrity" "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==" "resolved" "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz" "version" "7.3.8" dependencies: "lru-cache" "^6.0.0" +"set-blocking@^2.0.0": + "version" "2.0.0" + +"shallowequal@^1.0.0": + "integrity" "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + "resolved" "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz" + "version" "1.1.0" + "shebang-command@^2.0.0": "integrity" "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==" "resolved" "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" @@ -2012,21 +3076,80 @@ "get-intrinsic" "^1.0.2" "object-inspect" "^1.9.0" +"signal-exit@^3.0.7": + "version" "3.0.7" + "slash@^3.0.0": "integrity" "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" "resolved" "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" "version" "3.0.0" +"smart-buffer@^4.2.0": + "version" "4.2.0" + +"socks-proxy-agent@^7.0.0": + "version" "7.0.0" + dependencies: + "agent-base" "^6.0.2" + "debug" "^4.3.3" + "socks" "^2.6.2" + +"socks@^2.6.2": + "version" "2.7.0" + dependencies: + "ip" "^2.0.0" + "smart-buffer" "^4.2.0" + "source-map-js@^1.0.2": "integrity" "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" "resolved" "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz" "version" "1.0.2" +"spdx-correct@^3.0.0": + "version" "3.1.1" + dependencies: + "spdx-expression-parse" "^3.0.0" + "spdx-license-ids" "^3.0.0" + +"spdx-exceptions@^2.1.0": + "version" "2.3.0" + +"spdx-expression-parse@^3.0.0": + "version" "3.0.1" + dependencies: + "spdx-exceptions" "^2.1.0" + "spdx-license-ids" "^3.0.0" + +"spdx-license-ids@^3.0.0": + "version" "3.0.11" + "srt-webvtt@^2.0.0": "integrity" "sha512-G2Z7/Jf2NRKrmLYNSIhSYZZYE6OFlKXFp9Au2/zJBKgrioUzmrAys1x7GT01dwl6d2sEnqr5uahEIOd0JW/Rbw==" "resolved" "https://registry.npmjs.org/srt-webvtt/-/srt-webvtt-2.0.0.tgz" "version" "2.0.0" +"ssri@^10.0.0", "ssri@^10.0.1": + "version" "10.0.1" + dependencies: + "minipass" "^4.0.0" + +"ssri@^9.0.0": + "version" "9.0.1" + dependencies: + "minipass" "^3.1.1" + +"string_decoder@^1.1.1": + "version" "1.3.0" + dependencies: + "safe-buffer" "~5.2.0" + +"string-width@^1.0.2 || 2 || 3 || 4", "string-width@^4.2.0", "string-width@^4.2.3": + "version" "4.2.3" + dependencies: + "emoji-regex" "^8.0.0" + "is-fullwidth-code-point" "^3.0.0" + "strip-ansi" "^6.0.1" + "string.prototype.matchall@^4.0.6": "integrity" "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==" "resolved" "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz" @@ -2076,6 +3199,15 @@ "resolved" "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" "version" "3.1.1" +"subscribe-ui-event@^2.0.6": + "integrity" "sha512-Acrtf9XXl6lpyHAWYeRD1xTPUQHDERfL4GHeNuYAtZMc4Z8Us2iDBP0Fn3xiRvkQ1FO+hx+qRLmPEwiZxp7FDQ==" + "resolved" "https://registry.npmjs.org/subscribe-ui-event/-/subscribe-ui-event-2.0.7.tgz" + "version" "2.0.7" + dependencies: + "eventemitter3" "^3.0.0" + "lodash" "^4.17.15" + "raf" "^3.0.0" + "supports-color@^7.1.0": "integrity" "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==" "resolved" "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" @@ -2122,16 +3254,32 @@ "quick-lru" "^5.1.1" "resolve" "^1.22.1" +"tar@^6.1.11", "tar@^6.1.13", "tar@^6.1.2": + "version" "6.1.13" + dependencies: + "chownr" "^2.0.0" + "fs-minipass" "^2.0.0" + "minipass" "^4.0.0" + "minizlib" "^2.1.1" + "mkdirp" "^1.0.3" + "yallist" "^4.0.0" + "text-table@^0.2.0": "integrity" "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" "resolved" "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" "version" "0.2.0" +"text-table@~0.2.0": + "version" "0.2.0" + "tiny-invariant@^1.0.2": "integrity" "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==" "resolved" "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz" "version" "1.3.1" +"tiny-relative-date@^1.3.0": + "version" "1.3.0" + "tiny-warning@^1.0.0": "integrity" "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" "resolved" "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz" @@ -2149,6 +3297,9 @@ "resolved" "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" "version" "0.0.3" +"treeverse@^3.0.0": + "version" "3.0.0" + "tsconfig-paths@^3.14.1": "integrity" "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==" "resolved" "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz" @@ -2198,6 +3349,26 @@ "has-symbols" "^1.0.3" "which-boxed-primitive" "^1.0.2" +"unique-filename@^2.0.0": + "version" "2.0.1" + dependencies: + "unique-slug" "^3.0.0" + +"unique-filename@^3.0.0": + "version" "3.0.0" + dependencies: + "unique-slug" "^4.0.0" + +"unique-slug@^3.0.0": + "version" "3.0.0" + dependencies: + "imurmurhash" "^0.1.4" + +"unique-slug@^4.0.0": + "version" "4.0.0" + dependencies: + "imurmurhash" "^0.1.4" + "unpacker@^1.0.1": "integrity" "sha512-0HTljwp8+JBdITpoHcK1LWi7X9U2BspUmWv78UWZh7NshYhbh1nec8baY/iSbe2OQTZ2bhAtVdnr6/BTD0DKVg==" "resolved" "https://registry.npmjs.org/unpacker/-/unpacker-1.0.1.tgz" @@ -2218,17 +3389,33 @@ dependencies: "punycode" "^2.1.0" -"util-deprecate@^1.0.2": +"util-deprecate@^1.0.1", "util-deprecate@^1.0.2": "integrity" "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" "resolved" "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" "version" "1.0.2" +"validate-npm-package-license@^3.0.4": + "version" "3.0.4" + dependencies: + "spdx-correct" "^3.0.0" + "spdx-expression-parse" "^3.0.0" + +"validate-npm-package-name@^5.0.0": + "version" "5.0.0" + dependencies: + "builtins" "^5.0.0" + "value-equal@^1.0.1": "integrity" "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" "resolved" "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz" "version" "1.0.1" -"vite@^4.0.0", "vite@^4.0.1": +"vite-plugin-package-version@^1.0.2": + "integrity" "sha512-xCJMR0KD4rqSUwINyHJlLizio2VzYzaMrRkqC9xWaVGXgw1lIrzdD+wBUf1XDM8EhL1JoQ7aykLOfKrlZd1SoQ==" + "resolved" "https://registry.npmjs.org/vite-plugin-package-version/-/vite-plugin-package-version-1.0.2.tgz" + "version" "1.0.2" + +"vite@^4.0.0", "vite@^4.0.1", "vite@>=2.0.0-beta.69": "integrity" "sha512-kZQPzbDau35iWOhy3CpkrRC7It+HIHtulAzBhMqzGHKRf/4+vmh8rPDDdv98SWQrFWo6//3ozwsRmwQIPZsK9g==" "resolved" "https://registry.npmjs.org/vite/-/vite-4.0.1.tgz" "version" "4.0.1" @@ -2245,6 +3432,14 @@ "resolved" "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz" "version" "3.1.0" +"walk-up-path@^1.0.0": + "version" "1.0.0" + +"wcwidth@^1.0.0": + "version" "1.0.1" + dependencies: + "defaults" "^1.0.3" + "webidl-conversions@^3.0.0": "integrity" "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" "resolved" "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" @@ -2276,6 +3471,21 @@ dependencies: "isexe" "^2.0.0" +"which@^2.0.2": + "version" "2.0.2" + dependencies: + "isexe" "^2.0.0" + +"which@^3.0.0": + "version" "3.0.0" + dependencies: + "isexe" "^2.0.0" + +"wide-align@^1.1.5": + "version" "1.1.5" + dependencies: + "string-width" "^1.0.2 || 2 || 3 || 4" + "word-wrap@^1.2.3": "integrity" "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" "resolved" "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" @@ -2286,6 +3496,12 @@ "resolved" "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" "version" "1.0.2" +"write-file-atomic@^5.0.0": + "version" "5.0.0" + dependencies: + "imurmurhash" "^0.1.4" + "signal-exit" "^3.0.7" + "xtend@^4.0.2": "integrity" "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" "resolved" "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz" From e7981539e65f689cc0db87b19e3191bb8712d366 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sat, 7 Jan 2023 23:44:46 +0100 Subject: [PATCH 002/135] media grids --- package.json | 1 + src/components/SearchBar.tsx | 2 +- src/components/buttons/DropdownButton.tsx | 6 +- src/components/buttons/IconPatch.tsx | 4 +- src/components/layout/Backdrop.tsx | 8 +-- src/components/layout/BrandPill.tsx | 9 +-- src/components/layout/WideContainer.tsx | 18 ++++++ src/components/media/MediaCard.tsx | 78 ++++++++--------------- src/components/media/MediaGrid.tsx | 11 ++++ src/components/text/Title.tsx | 7 +- src/views/search/HomeView.tsx | 28 +++++--- src/views/search/SearchLoadingView.tsx | 2 +- src/views/search/SearchResultsView.tsx | 17 +++-- src/views/search/SearchView.tsx | 12 ++-- tailwind.config.js | 26 ++++---- yarn.lock | 19 ++++-- 16 files changed, 136 insertions(+), 112 deletions(-) create mode 100644 src/components/layout/WideContainer.tsx create mode 100644 src/components/media/MediaGrid.tsx diff --git a/package.json b/package.json index 405a6ca5..49f803f6 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ ] }, "devDependencies": { + "@tailwindcss/line-clamp": "^0.4.2", "@types/crypto-js": "^4.1.1", "@types/node": "^17.0.15", "@types/react": "^17.0.39", diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index eea26423..3be9d0b5 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -37,7 +37,7 @@ export function SearchBarInput(props: SearchBarProps) { } return ( -
+
diff --git a/src/components/buttons/DropdownButton.tsx b/src/components/buttons/DropdownButton.tsx index f32517ba..a49403e8 100644 --- a/src/components/buttons/DropdownButton.tsx +++ b/src/components/buttons/DropdownButton.tsx @@ -6,11 +6,7 @@ import React, { } from "react"; import { Icon, Icons } from "@/components/Icon"; -import { - Backdrop, - BackdropContainer, - useBackdrop, -} from "@/components/layout/Backdrop"; +import { BackdropContainer, useBackdrop } from "@/components/layout/Backdrop"; import { ButtonControlProps, ButtonControl } from "./ButtonControl"; export interface OptionItem { diff --git a/src/components/buttons/IconPatch.tsx b/src/components/buttons/IconPatch.tsx index f14a3f56..53980322 100644 --- a/src/components/buttons/IconPatch.tsx +++ b/src/components/buttons/IconPatch.tsx @@ -12,9 +12,9 @@ export function IconPatch(props: IconPatchProps) { return (
diff --git a/src/components/layout/Backdrop.tsx b/src/components/layout/Backdrop.tsx index 3daac079..3ded3afc 100644 --- a/src/components/layout/Backdrop.tsx +++ b/src/components/layout/Backdrop.tsx @@ -40,7 +40,7 @@ export function useBackdrop(): [ return [setBackdrop, backdropProps, highlightedProps]; } -export function Backdrop(props: BackdropProps) { +function Backdrop(props: BackdropProps) { const clickEvent = props.onClick || (() => {}); const animationEvent = props.onBackdropHide || (() => {}); const [isVisible, setVisible, fadeProps] = useFade(); @@ -59,7 +59,7 @@ export function Backdrop(props: BackdropProps) { return (
{createPortal( -
+
-
+
{props.children}
, diff --git a/src/components/layout/BrandPill.tsx b/src/components/layout/BrandPill.tsx index 3df0be76..82a037cb 100644 --- a/src/components/layout/BrandPill.tsx +++ b/src/components/layout/BrandPill.tsx @@ -6,13 +6,14 @@ export function BrandPill(props: { clickable?: boolean }) { return (
- {t('global.name')} + {t("global.name")}
); } diff --git a/src/components/layout/WideContainer.tsx b/src/components/layout/WideContainer.tsx new file mode 100644 index 00000000..f7d745fe --- /dev/null +++ b/src/components/layout/WideContainer.tsx @@ -0,0 +1,18 @@ +import { ReactNode } from "react"; + +interface WideContainerProps { + classNames?: string; + children?: ReactNode; +} + +export function WideContainer(props: WideContainerProps) { + return ( +
+ {props.children} +
+ ); +} diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 2171f7cb..8174f2c8 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -5,23 +5,20 @@ import { MWMediaMeta, MWMediaType, } from "@/providers"; -import { Icon, Icons } from "@/components/Icon"; import { serializePortableMedia } from "@/hooks/usePortableMedia"; import { DotList } from "@/components/text/DotList"; export interface MediaCardProps { media: MWMediaMeta; + // eslint-disable-next-line react/no-unused-prop-types watchedPercentage: number; linkable?: boolean; series?: boolean; } -function MediaCardContent({ - media, - linkable, - watchedPercentage, - series, -}: MediaCardProps) { +// TODO add progress back + +function MediaCardContent({ media, series, linkable }: MediaCardProps) { const provider = getProviderFromId(media.providerId); if (!provider) { @@ -29,52 +26,31 @@ function MediaCardContent({ } return ( -
- {/* progress background */} - {watchedPercentage > 0 ? ( -
-
-
-
-
- ) : null} - -
- {/* card content */} -
-

- {media.title} - {series && media.seasonId && media.episodeId ? ( - - S{media.seasonId} E{media.episodeId} - - ) : null} -

- -
- - {/* hoverable chevron */} -
- -
-
-
+
+
+

+ {media.title} + {series && media.seasonId && media.episodeId ? ( + + S{media.seasonId} E{media.episodeId} + + ) : null} +

+ +
+
); } diff --git a/src/components/media/MediaGrid.tsx b/src/components/media/MediaGrid.tsx new file mode 100644 index 00000000..59a3e39e --- /dev/null +++ b/src/components/media/MediaGrid.tsx @@ -0,0 +1,11 @@ +interface MediaGridProps { + children?: React.ReactNode; +} + +export function MediaGrid(props: MediaGridProps) { + return ( +
+ {props.children} +
+ ); +} diff --git a/src/components/text/Title.tsx b/src/components/text/Title.tsx index f6771c6f..f30d6f8a 100644 --- a/src/components/text/Title.tsx +++ b/src/components/text/Title.tsx @@ -1,10 +1,15 @@ export interface TitleProps { children?: React.ReactNode; + className?: string; } export function Title(props: TitleProps) { return ( -

+

{props.children}

); diff --git a/src/views/search/HomeView.tsx b/src/views/search/HomeView.tsx index d646646f..180d456e 100644 --- a/src/views/search/HomeView.tsx +++ b/src/views/search/HomeView.tsx @@ -1,5 +1,6 @@ import { Icons } from "@/components/Icon"; import { SectionHeading } from "@/components/layout/SectionHeading"; +import { MediaGrid } from "@/components/media/MediaGrid"; import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; import { getIfBookmarkedFromPortable, @@ -20,9 +21,14 @@ function Bookmarks() { title={t("search.bookmarks") || "Bookmarks"} icon={Icons.BOOKMARK} > - {bookmarks.map((v) => ( - - ))} + + {bookmarks.map((v) => ( + + ))} + ); } @@ -44,13 +50,15 @@ function Watched() { title={t("search.continueWatching") || "Continue Watching"} icon={Icons.CLOCK} > - {watchedItems.map((v) => ( - - ))} + + {watchedItems.map((v) => ( + + ))} + ); } diff --git a/src/views/search/SearchLoadingView.tsx b/src/views/search/SearchLoadingView.tsx index e8ce46d2..4572c659 100644 --- a/src/views/search/SearchLoadingView.tsx +++ b/src/views/search/SearchLoadingView.tsx @@ -5,7 +5,7 @@ export function SearchLoadingView() { const { t } = useTranslation(); return ( ); diff --git a/src/views/search/SearchResultsView.tsx b/src/views/search/SearchResultsView.tsx index a948553c..6ce0001d 100644 --- a/src/views/search/SearchResultsView.tsx +++ b/src/views/search/SearchResultsView.tsx @@ -1,6 +1,7 @@ import { IconPatch } from "@/components/buttons/IconPatch"; import { Icons } from "@/components/Icon"; import { SectionHeading } from "@/components/layout/SectionHeading"; +import { MediaGrid } from "@/components/media/MediaGrid"; import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; import { useLoading } from "@/hooks/useLoading"; import { MWMassProviderOutput, MWQuery, SearchProviders } from "@/providers"; @@ -19,7 +20,7 @@ function SearchSuffix(props: { const icon: Icons = allFailed ? Icons.WARNING : Icons.EYE_SLASH; return ( -
+
- {results.results.map((v) => ( - - ))} + + {results.results.map((v) => ( + + ))} + ) : null} diff --git a/src/views/search/SearchView.tsx b/src/views/search/SearchView.tsx index d4f5e1d9..4d45e278 100644 --- a/src/views/search/SearchView.tsx +++ b/src/views/search/SearchView.tsx @@ -5,8 +5,8 @@ import { SearchBarInput } from "@/components/SearchBar"; import Sticky from "react-stickynode"; import { Title } from "@/components/text/Title"; import { useSearchQuery } from "@/hooks/useSearchQuery"; +import { WideContainer } from "@/components/layout/WideContainer"; import { useTranslation } from "react-i18next"; - import { SearchResultsPartial } from "./SearchResultsPartial"; export function SearchView() { @@ -21,16 +21,16 @@ export function SearchView() { return ( <> -
+
-
+
- {t("search.title")} + {t("search.title")}
- + - + ); } diff --git a/tailwind.config.js b/tailwind.config.js index f26c6c17..eb70d4d8 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -12,29 +12,29 @@ module.exports = { "bink-500": "#8D66B5", "bink-600": "#A87FD1", "bink-700": "#CD97D6", - "denim-100": "#131119", - "denim-200": "#1E1A29", - "denim-300": "#282336", - "denim-400": "#322D43", - "denim-500": "#433D55", - "denim-600": "#5A5370", - "denim-700": "#817998", + "denim-100": "#120F1D", + "denim-200": "#191526", + "denim-300": "#211D30", + "denim-400": "#2B263D", + "denim-500": "#38334A", + "denim-600": "#504B64", + "denim-700": "#7A758F" }, /* fonts */ fontFamily: { - "open-sans": "'Open Sans'", + "open-sans": "'Open Sans'" }, /* animations */ keyframes: { "loading-pin": { "0%, 40%, 100%": { height: "0.5em", "background-color": "#282336" }, - "20%": { height: "1em", "background-color": "white" }, - }, + "20%": { height: "1em", "background-color": "white" } + } }, - animation: { "loading-pin": "loading-pin 1.8s ease-in-out infinite" }, - }, + animation: { "loading-pin": "loading-pin 1.8s ease-in-out infinite" } + } }, - plugins: [require("tailwind-scrollbar")], + plugins: [require("tailwind-scrollbar"), require("@tailwindcss/line-clamp")] }; diff --git a/yarn.lock b/yarn.lock index b454d8fe..bd46f9e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -254,6 +254,11 @@ "@swc/core-win32-ia32-msvc" "1.3.22" "@swc/core-win32-x64-msvc" "1.3.22" +"@tailwindcss/line-clamp@^0.4.2": + "integrity" "sha512-HFzAQuqYCjyy/SX9sLGB1lroPzmcnWv1FHkIpmypte10hptf4oPUfucryMKovZh2u0uiS9U5Ty3GghWfEJGwVw==" + "resolved" "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.2.tgz" + "version" "0.4.2" + "@tootallnate/once@2": "version" "2.0.0" @@ -1942,16 +1947,16 @@ "version" "1.1.4" "json5@^1.0.1": - "integrity" "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==" - "resolved" "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz" - "version" "1.0.1" + "integrity" "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==" + "resolved" "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz" + "version" "1.0.2" dependencies: "minimist" "^1.2.0" "json5@^2.2.0": - "integrity" "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" - "resolved" "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz" - "version" "2.2.1" + "integrity" "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + "resolved" "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" + "version" "2.2.3" "jsonparse@^1.3.1": "version" "1.3.1" @@ -3225,7 +3230,7 @@ "resolved" "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-2.0.1.tgz" "version" "2.0.1" -"tailwindcss@^3.2.4", "tailwindcss@3.x": +"tailwindcss@^3.2.4", "tailwindcss@>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1", "tailwindcss@3.x": "integrity" "sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==" "resolved" "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz" "version" "3.2.4" From 9fba4226733c9e7fe16ed7f8b83c2d3fcaf2e6e6 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sat, 7 Jan 2023 23:48:09 +0100 Subject: [PATCH 003/135] fix circle figure --- src/views/search/SearchView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/search/SearchView.tsx b/src/views/search/SearchView.tsx index 4d45e278..c786cbe1 100644 --- a/src/views/search/SearchView.tsx +++ b/src/views/search/SearchView.tsx @@ -26,7 +26,7 @@ export function SearchView() {
-
+
From b98fdcd94d1b5a7699f736a7d6028d5186a87bf7 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sat, 7 Jan 2023 23:50:36 +0100 Subject: [PATCH 004/135] fix some margins --- src/views/search/SearchLoadingView.tsx | 2 +- src/views/search/SearchResultsView.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/search/SearchLoadingView.tsx b/src/views/search/SearchLoadingView.tsx index 4572c659..6d02ff51 100644 --- a/src/views/search/SearchLoadingView.tsx +++ b/src/views/search/SearchLoadingView.tsx @@ -5,7 +5,7 @@ export function SearchLoadingView() { const { t } = useTranslation(); return ( ); diff --git a/src/views/search/SearchResultsView.tsx b/src/views/search/SearchResultsView.tsx index 6ce0001d..5f602988 100644 --- a/src/views/search/SearchResultsView.tsx +++ b/src/views/search/SearchResultsView.tsx @@ -20,7 +20,7 @@ function SearchSuffix(props: { const icon: Icons = allFailed ? Icons.WARNING : Icons.EYE_SLASH; return ( -
+
Date: Sun, 8 Jan 2023 13:15:32 +0100 Subject: [PATCH 005/135] custom video player start --- src/components/video/VideoContext.tsx | 103 ++++++++++++++++++ src/components/video/VideoPlayer.tsx | 55 ++++++++++ .../video/controls/FullscreenControl.tsx | 26 +++++ .../video/controls/PauseControl.tsx | 32 ++++++ .../video/controls/SourceControl.tsx | 19 ++++ src/setup/App.tsx | 2 + src/views/TestView.tsx | 16 +++ yarn.lock | 33 +++++- 8 files changed, 281 insertions(+), 5 deletions(-) create mode 100644 src/components/video/VideoContext.tsx create mode 100644 src/components/video/VideoPlayer.tsx create mode 100644 src/components/video/controls/FullscreenControl.tsx create mode 100644 src/components/video/controls/PauseControl.tsx create mode 100644 src/components/video/controls/SourceControl.tsx create mode 100644 src/views/TestView.tsx diff --git a/src/components/video/VideoContext.tsx b/src/components/video/VideoContext.tsx new file mode 100644 index 00000000..dd8301d9 --- /dev/null +++ b/src/components/video/VideoContext.tsx @@ -0,0 +1,103 @@ +import React, { + createContext, + MutableRefObject, + useEffect, + useReducer, +} from "react"; + +interface VideoPlayerContextType { + source: null | string; + playerWrapper: HTMLDivElement | null; + player: HTMLVideoElement | null; + controlState: "paused" | "playing"; + fullscreen: boolean; +} +const initial = ( + player: HTMLVideoElement | null = null, + wrapper: HTMLDivElement | null = null +): VideoPlayerContextType => ({ + source: null, + playerWrapper: wrapper, + player, + controlState: "paused", + fullscreen: false, +}); + +type VideoPlayerContextAction = + | { type: "SET_SOURCE"; url: string } + | { type: "CONTROL"; do: "PAUSE" | "PLAY"; soft?: boolean } + | { type: "FULLSCREEN"; do: "ENTER" | "EXIT"; soft?: boolean } + | { + type: "UPDATE_PLAYER"; + player: HTMLVideoElement | null; + playerWrapper: HTMLDivElement | null; + }; + +function videoPlayerContextReducer( + original: VideoPlayerContextType, + action: VideoPlayerContextAction +): VideoPlayerContextType { + const video = { ...original }; + if (action.type === "SET_SOURCE") { + video.source = action.url; + return video; + } + if (action.type === "CONTROL") { + if (action.do === "PAUSE") video.controlState = "paused"; + else if (action.do === "PLAY") video.controlState = "playing"; + if (action.soft) return video; + + if (action.do === "PAUSE") video.player?.pause(); + else if (action.do === "PLAY") video.player?.play(); + return video; + } + if (action.type === "UPDATE_PLAYER") { + video.player = action.player; + video.playerWrapper = action.playerWrapper; + return video; + } + if (action.type === "FULLSCREEN") { + video.fullscreen = action.do === "ENTER"; + if (action.soft) return video; + + if (action.do === "ENTER") video.playerWrapper?.requestFullscreen(); + else document.exitFullscreen(); + return video; + } + + return original; +} + +export const VideoPlayerContext = createContext( + initial() +); +export const VideoPlayerDispatchContext = createContext< + React.Dispatch +>(null as any); + +export function VideoPlayerContextProvider(props: { + children: React.ReactNode; + player: MutableRefObject; + playerWrapper: MutableRefObject; +}) { + const [videoData, dispatch] = useReducer( + videoPlayerContextReducer, + initial() + ); + + useEffect(() => { + dispatch({ + type: "UPDATE_PLAYER", + player: props.player.current, + playerWrapper: props.playerWrapper.current, + }); + }, [props.player, props.playerWrapper]); + + return ( + + + {props.children} + + + ); +} diff --git a/src/components/video/VideoPlayer.tsx b/src/components/video/VideoPlayer.tsx new file mode 100644 index 00000000..d2395bb7 --- /dev/null +++ b/src/components/video/VideoPlayer.tsx @@ -0,0 +1,55 @@ +import { forwardRef, useCallback, useContext, useEffect, useRef } from "react"; +import { + VideoPlayerContext, + VideoPlayerContextProvider, + VideoPlayerDispatchContext, +} from "./VideoContext"; + +interface VideoPlayerProps { + children?: React.ReactNode; +} + +const VideoPlayerInternals = forwardRef((props, ref) => { + const video = useContext(VideoPlayerContext); + const dispatch = useContext(VideoPlayerDispatchContext); + + const onPlay = useCallback(() => { + dispatch({ + type: "CONTROL", + do: "PLAY", + soft: true, + }); + }, [dispatch]); + const onPause = useCallback(() => { + dispatch({ + type: "CONTROL", + do: "PAUSE", + soft: true, + }); + }, [dispatch]); + + useEffect(() => {}, []); + + return ( + + ); +}); + +export function VideoPlayer(props: VideoPlayerProps) { + const playerRef = useRef(null); + const playerWrapperRef = useRef(null); + + return ( + +
+ + {props.children} +
+
+ ); +} diff --git a/src/components/video/controls/FullscreenControl.tsx b/src/components/video/controls/FullscreenControl.tsx new file mode 100644 index 00000000..332df675 --- /dev/null +++ b/src/components/video/controls/FullscreenControl.tsx @@ -0,0 +1,26 @@ +import { useCallback, useContext } from "react"; +import { + VideoPlayerContext, + VideoPlayerDispatchContext, +} from "../VideoContext"; + +export function FullscreenControl() { + const dispatch = useContext(VideoPlayerDispatchContext); + const video = useContext(VideoPlayerContext); + + const handleClick = useCallback(() => { + dispatch({ + type: "FULLSCREEN", + do: video.fullscreen ? "EXIT" : "ENTER", + }); + }, [video, dispatch]); + + let text = "not fullscreen"; + if (video.fullscreen) text = "in fullscreen"; + + return ( + + ); +} diff --git a/src/components/video/controls/PauseControl.tsx b/src/components/video/controls/PauseControl.tsx new file mode 100644 index 00000000..9ea9bcb6 --- /dev/null +++ b/src/components/video/controls/PauseControl.tsx @@ -0,0 +1,32 @@ +import { useCallback, useContext } from "react"; +import { + VideoPlayerContext, + VideoPlayerDispatchContext, +} from "../VideoContext"; + +export function PauseControl() { + const dispatch = useContext(VideoPlayerDispatchContext); + const video = useContext(VideoPlayerContext); + + const handleClick = useCallback(() => { + if (video.controlState === "playing") + dispatch({ + type: "CONTROL", + do: "PAUSE", + }); + else if (video.controlState === "paused") + dispatch({ + type: "CONTROL", + do: "PLAY", + }); + }, [video, dispatch]); + + let text = "paused"; + if (video.controlState === "playing") text = "playing"; + + return ( + + ); +} diff --git a/src/components/video/controls/SourceControl.tsx b/src/components/video/controls/SourceControl.tsx new file mode 100644 index 00000000..e7ad0c9d --- /dev/null +++ b/src/components/video/controls/SourceControl.tsx @@ -0,0 +1,19 @@ +import { useContext, useEffect } from "react"; +import { VideoPlayerDispatchContext } from "../VideoContext"; + +interface SourceControlProps { + source: string; +} + +export function SourceControl(props: SourceControlProps) { + const dispatch = useContext(VideoPlayerDispatchContext); + + useEffect(() => { + dispatch({ + type: "SET_SOURCE", + url: props.source, + }); + }, [props.source, dispatch]); + + return null; +} diff --git a/src/setup/App.tsx b/src/setup/App.tsx index b23cf7ee..091aa967 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -6,6 +6,7 @@ import { WatchedContextProvider } from "@/state/watched"; import { NotFoundPage } from "@/views/notfound/NotFoundView"; import { MediaView } from "@/views/MediaView"; import { SearchView } from "@/views/search/SearchView"; +import { TestView } from "@/views/TestView"; function App() { return ( @@ -18,6 +19,7 @@ function App() { + diff --git a/src/views/TestView.tsx b/src/views/TestView.tsx new file mode 100644 index 00000000..01e0af45 --- /dev/null +++ b/src/views/TestView.tsx @@ -0,0 +1,16 @@ +import { FullscreenControl } from "@/components/video/controls/FullscreenControl"; +import { PauseControl } from "@/components/video/controls/PauseControl"; +import { SourceControl } from "@/components/video/controls/SourceControl"; +import { VideoPlayer } from "@/components/video/VideoPlayer"; + +// test videos: https://gist.github.com/jsturgis/3b19447b304616f18657 + +export function TestView() { + return ( + + + + + + ); +} diff --git a/yarn.lock b/yarn.lock index bd46f9e2..75b2fabb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,7 +10,7 @@ "core-js-pure" "^3.25.1" "regenerator-runtime" "^0.13.11" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.14.5", "@babel/runtime@^7.18.9", "@babel/runtime@^7.19.4", "@babel/runtime@^7.20.6": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.14.5", "@babel/runtime@^7.18.9", "@babel/runtime@^7.19.4", "@babel/runtime@^7.20.6", "@babel/runtime@^7.4.5", "@babel/runtime@^7.9.2": "integrity" "sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==" "resolved" "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz" "version" "7.20.6" @@ -780,7 +780,7 @@ dependencies: "ip-regex" "^4.1.0" -"classnames@^2.0.0": +"classnames@^2.0.0", "classnames@^2.2.6": "integrity" "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" "resolved" "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz" "version" "2.3.2" @@ -2101,6 +2101,11 @@ "resolved" "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" "version" "4.6.2" +"lodash.throttle@^4.1.1": + "integrity" "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" + "resolved" "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz" + "version" "4.1.1" + "lodash@^4.17.15": "integrity" "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" "resolved" "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" @@ -2787,7 +2792,7 @@ dependencies: "read" "1" -"prop-types@^15.6.0", "prop-types@^15.6.2", "prop-types@^15.8.1": +"prop-types@^15.6.0", "prop-types@^15.6.2", "prop-types@^15.7.2", "prop-types@^15.8.1": "integrity" "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==" "resolved" "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" "version" "15.8.1" @@ -2821,7 +2826,7 @@ dependencies: "performance-now" "^2.1.0" -"react-dom@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^16 || ^17 || ^18", "react-dom@^17.0.2": +"react-dom@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^16 || ^17 || ^18", "react-dom@^17.0.2": "integrity" "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==" "resolved" "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz" "version" "17.0.2" @@ -2882,7 +2887,7 @@ "shallowequal" "^1.0.0" "subscribe-ui-event" "^2.0.6" -"react@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16 || ^17 || ^18", "react@^17.0.2", "react@>= 16.8.0", "react@>=15", "react@17.0.2": +"react@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16 || ^17 || ^18", "react@^17.0.2", "react@>= 16.8.0", "react@>=15", "react@17.0.2": "integrity" "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==" "resolved" "https://registry.npmjs.org/react/-/react-17.0.2.tgz" "version" "17.0.2" @@ -2941,6 +2946,13 @@ dependencies: "picomatch" "^2.2.1" +"redux@^4.0.1": + "integrity" "sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==" + "resolved" "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz" + "version" "4.2.0" + dependencies: + "@babel/runtime" "^7.9.2" + "regenerator-runtime@^0.13.11": "integrity" "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" "resolved" "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz" @@ -3415,6 +3427,17 @@ "resolved" "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz" "version" "1.0.1" +"video-react@^0.16.0": + "integrity" "sha512-138NHPS8bmgqCYVCdbv2GVFhXntemNHWGw9AN8iJSzr3jizXMmWJd2LTBppr4hZJUbyW1A1tPZ3CQXZUaexMVA==" + "resolved" "https://registry.npmjs.org/video-react/-/video-react-0.16.0.tgz" + "version" "0.16.0" + dependencies: + "@babel/runtime" "^7.4.5" + "classnames" "^2.2.6" + "lodash.throttle" "^4.1.1" + "prop-types" "^15.7.2" + "redux" "^4.0.1" + "vite-plugin-package-version@^1.0.2": "integrity" "sha512-xCJMR0KD4rqSUwINyHJlLizio2VzYzaMrRkqC9xWaVGXgw1lIrzdD+wBUf1XDM8EhL1JoQ7aykLOfKrlZd1SoQ==" "resolved" "https://registry.npmjs.org/vite-plugin-package-version/-/vite-plugin-package-version-1.0.2.tgz" From 3a67d50f425d71ab9f07da513526d0d20acdadd6 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sun, 8 Jan 2023 15:37:16 +0100 Subject: [PATCH 006/135] video player starter --- src/components/video/VideoContext.tsx | 72 +++++++------------ src/components/video/VideoPlayer.tsx | 42 ++--------- .../video/controls/FullscreenControl.tsx | 41 +++++------ .../video/controls/PauseControl.tsx | 26 ++----- src/components/video/hooks/controlVideo.ts | 20 ++++++ src/components/video/hooks/useVideoPlayer.ts | 55 ++++++++++++++ 6 files changed, 137 insertions(+), 119 deletions(-) create mode 100644 src/components/video/hooks/controlVideo.ts create mode 100644 src/components/video/hooks/useVideoPlayer.ts diff --git a/src/components/video/VideoContext.tsx b/src/components/video/VideoContext.tsx index dd8301d9..bc1bedea 100644 --- a/src/components/video/VideoContext.tsx +++ b/src/components/video/VideoContext.tsx @@ -1,36 +1,30 @@ import React, { createContext, MutableRefObject, + useContext, useEffect, useReducer, } from "react"; +import { + initialPlayerState, + PlayerState, + useVideoPlayer, +} from "./hooks/useVideoPlayer"; interface VideoPlayerContextType { - source: null | string; - playerWrapper: HTMLDivElement | null; - player: HTMLVideoElement | null; - controlState: "paused" | "playing"; - fullscreen: boolean; + source: string | null; + state: PlayerState; } -const initial = ( - player: HTMLVideoElement | null = null, - wrapper: HTMLDivElement | null = null -): VideoPlayerContextType => ({ +const initial: VideoPlayerContextType = { source: null, - playerWrapper: wrapper, - player, - controlState: "paused", - fullscreen: false, -}); + state: initialPlayerState, +}; type VideoPlayerContextAction = | { type: "SET_SOURCE"; url: string } - | { type: "CONTROL"; do: "PAUSE" | "PLAY"; soft?: boolean } - | { type: "FULLSCREEN"; do: "ENTER" | "EXIT"; soft?: boolean } | { type: "UPDATE_PLAYER"; - player: HTMLVideoElement | null; - playerWrapper: HTMLDivElement | null; + state: PlayerState; }; function videoPlayerContextReducer( @@ -42,35 +36,16 @@ function videoPlayerContextReducer( video.source = action.url; return video; } - if (action.type === "CONTROL") { - if (action.do === "PAUSE") video.controlState = "paused"; - else if (action.do === "PLAY") video.controlState = "playing"; - if (action.soft) return video; - - if (action.do === "PAUSE") video.player?.pause(); - else if (action.do === "PLAY") video.player?.play(); - return video; - } if (action.type === "UPDATE_PLAYER") { - video.player = action.player; - video.playerWrapper = action.playerWrapper; - return video; - } - if (action.type === "FULLSCREEN") { - video.fullscreen = action.do === "ENTER"; - if (action.soft) return video; - - if (action.do === "ENTER") video.playerWrapper?.requestFullscreen(); - else document.exitFullscreen(); + video.state = action.state; return video; } return original; } -export const VideoPlayerContext = createContext( - initial() -); +export const VideoPlayerContext = + createContext(initial); export const VideoPlayerDispatchContext = createContext< React.Dispatch >(null as any); @@ -78,20 +53,19 @@ export const VideoPlayerDispatchContext = createContext< export function VideoPlayerContextProvider(props: { children: React.ReactNode; player: MutableRefObject; - playerWrapper: MutableRefObject; }) { + const { playerState } = useVideoPlayer(props.player); const [videoData, dispatch] = useReducer( videoPlayerContextReducer, - initial() + initial ); useEffect(() => { dispatch({ type: "UPDATE_PLAYER", - player: props.player.current, - playerWrapper: props.playerWrapper.current, + state: playerState, }); - }, [props.player, props.playerWrapper]); + }, [playerState]); return ( @@ -101,3 +75,11 @@ export function VideoPlayerContextProvider(props: { ); } + +export function useVideoPlayerState() { + const { state } = useContext(VideoPlayerContext); + + return { + videoState: state, + }; +} diff --git a/src/components/video/VideoPlayer.tsx b/src/components/video/VideoPlayer.tsx index d2395bb7..d1b7f341 100644 --- a/src/components/video/VideoPlayer.tsx +++ b/src/components/video/VideoPlayer.tsx @@ -1,37 +1,15 @@ -import { forwardRef, useCallback, useContext, useEffect, useRef } from "react"; -import { - VideoPlayerContext, - VideoPlayerContextProvider, - VideoPlayerDispatchContext, -} from "./VideoContext"; +import { forwardRef, useContext, useRef } from "react"; +import { VideoPlayerContext, VideoPlayerContextProvider } from "./VideoContext"; interface VideoPlayerProps { children?: React.ReactNode; } -const VideoPlayerInternals = forwardRef((props, ref) => { +const VideoPlayerInternals = forwardRef((_, ref) => { const video = useContext(VideoPlayerContext); - const dispatch = useContext(VideoPlayerDispatchContext); - - const onPlay = useCallback(() => { - dispatch({ - type: "CONTROL", - do: "PLAY", - soft: true, - }); - }, [dispatch]); - const onPause = useCallback(() => { - dispatch({ - type: "CONTROL", - do: "PAUSE", - soft: true, - }); - }, [dispatch]); - - useEffect(() => {}, []); return ( -
- ) + ); } diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index 3be9d0b5..df844c83 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { MWMediaType, MWQuery } from "@/providers"; import { useTranslation } from "react-i18next"; +import { MWMediaType, MWQuery } from "@/providers"; import { DropdownButton } from "./buttons/DropdownButton"; import { Icon, Icons } from "./Icon"; import { TextInputControl } from "./text-inputs/TextInputControl"; diff --git a/src/components/layout/Backdrop.tsx b/src/components/layout/Backdrop.tsx index 3ded3afc..57719e29 100644 --- a/src/components/layout/Backdrop.tsx +++ b/src/components/layout/Backdrop.tsx @@ -1,6 +1,6 @@ import React, { createRef, useEffect, useState } from "react"; -import { useFade } from "@/hooks/useFade"; import { createPortal } from "react-dom"; +import { useFade } from "@/hooks/useFade"; interface BackdropProps { onClick?: (e: MouseEvent) => void; diff --git a/src/components/layout/BrandPill.tsx b/src/components/layout/BrandPill.tsx index 82a037cb..cef38ab5 100644 --- a/src/components/layout/BrandPill.tsx +++ b/src/components/layout/BrandPill.tsx @@ -1,5 +1,5 @@ -import { Icon, Icons } from "@/components/Icon"; import { useTranslation } from "react-i18next"; +import { Icon, Icons } from "@/components/Icon"; export function BrandPill(props: { clickable?: boolean }) { const { t } = useTranslation(); diff --git a/src/components/layout/Loading.tsx b/src/components/layout/Loading.tsx index 7af05dfe..cff6a503 100644 --- a/src/components/layout/Loading.tsx +++ b/src/components/layout/Loading.tsx @@ -8,10 +8,10 @@ export function Loading(props: LoadingProps) {
-
-
-
-
+
+
+
+
{props.text && props.text.length ? (

{props.text}

diff --git a/src/components/layout/Paper.tsx b/src/components/layout/Paper.tsx index e2e8475b..a87895ae 100644 --- a/src/components/layout/Paper.tsx +++ b/src/components/layout/Paper.tsx @@ -1,14 +1,16 @@ import { ReactNode } from "react"; export interface PaperProps { - children?: ReactNode, - className?: string, + children?: ReactNode; + className?: string; } export function Paper(props: PaperProps) { return ( -
+
{props.children}
- ) + ); } diff --git a/src/components/layout/Seasons.tsx b/src/components/layout/Seasons.tsx index b6baab05..f2416372 100644 --- a/src/components/layout/Seasons.tsx +++ b/src/components/layout/Seasons.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; +import { useTranslation } from "react-i18next"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Dropdown, OptionItem } from "@/components/Dropdown"; import { Icons } from "@/components/Icon"; @@ -14,7 +15,6 @@ import { MWPortableMedia, } from "@/providers"; import { getSeasonDataFromMedia } from "@/providers/methods/seasons"; -import { useTranslation } from "react-i18next"; export interface SeasonsProps { media: MWMedia; @@ -37,7 +37,7 @@ export function LoadingSeasons(props: { error?: boolean }) { ) : (
-

{t('seasons.failed')}

+

{t("seasons.failed")}

)}
@@ -75,7 +75,7 @@ export function Seasons(props: SeasonsProps) { const mapSeason = (season: MWMediaSeason) => ({ id: season.id, - name: season.title || `${t('seasons.season', { season: season.sort })}`, + name: season.title || `${t("seasons.season", { season: season.sort })}`, }); const options = seasons.seasons.map(mapSeason); diff --git a/src/components/media/EpisodeButton.tsx b/src/components/media/EpisodeButton.tsx index c4e851d0..f3e4375c 100644 --- a/src/components/media/EpisodeButton.tsx +++ b/src/components/media/EpisodeButton.tsx @@ -9,12 +9,12 @@ export function Episode(props: EpisodeProps) { return (
+

{props.content.map((item, index) => ( {index !== 0 ? ( diff --git a/src/components/text/Link.tsx b/src/components/text/Link.tsx index 7505f41c..1451114e 100644 --- a/src/components/text/Link.tsx +++ b/src/components/text/Link.tsx @@ -16,22 +16,27 @@ interface ILinkPropsInternal extends ILinkPropsBase { to: string; } -type LinkProps = - | ILinkPropsExternal - | ILinkPropsInternal - | ILinkPropsBase; +type LinkProps = ILinkPropsExternal | ILinkPropsInternal | ILinkPropsBase; export function Link(props: LinkProps) { const isExternal = !!(props as ILinkPropsExternal).url; const isInternal = !!(props as ILinkPropsInternal).to; const content = ( - + {props.children} ); if (isExternal) - return {content}; + return ( + + {content} + + ); if (isInternal) return ( {content} diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts index 509337b6..fdc9b6db 100644 --- a/src/hooks/useDebounce.ts +++ b/src/hooks/useDebounce.ts @@ -4,17 +4,14 @@ export function useDebounce(value: T, delay: number): T { // State and setters for debounced value const [debouncedValue, setDebouncedValue] = useState(value); - useEffect( - () => { - const handler = setTimeout(() => { - setDebouncedValue(value); - }, delay); - return () => { - clearTimeout(handler); - }; - }, - [value, delay] - ); + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + return () => { + clearTimeout(handler); + }; + }, [value, delay]); return debouncedValue; } diff --git a/src/hooks/useFade.ts b/src/hooks/useFade.ts index e03413fd..58438acf 100644 --- a/src/hooks/useFade.ts +++ b/src/hooks/useFade.ts @@ -1,7 +1,9 @@ import React, { useEffect, useState } from "react"; -import './useFade.css' +import "./useFade.css"; -export const useFade = (initial = false): [boolean, React.Dispatch>, any] => { +export const useFade = ( + initial = false +): [boolean, React.Dispatch>, any] => { const [show, setShow] = useState(initial); const [isVisible, setVisible] = useState(show); @@ -20,7 +22,7 @@ export const useFade = (initial = false): [boolean, React.Dispatch; - -export interface MWQuery { - searchQuery: string; - type: MWMediaType; -} - -export interface MWMediaProviderBase { - id: string; // id of provider, must be unique - enabled: boolean; - type: MWMediaType[]; - displayName: string; - - getMediaFromPortable(media: MWPortableMedia): Promise; - searchForMedia(query: MWQuery): Promise; - getStream(media: MWPortableMedia): Promise; - getSeasonDataFromMedia?: (media: MWPortableMedia) => Promise; -} - -export type MWMediaProviderSeries = MWMediaProviderBase & { - getSeasonDataFromMedia: (media: MWPortableMedia) => Promise; -}; - -export type MWMediaProvider = MWMediaProviderBase; - -export interface MWMediaProviderMetadata { - exists: boolean; - id?: string; - enabled: boolean; - type: MWMediaType[]; - provider?: MWMediaProvider; -} - -export interface MWMassProviderOutput { - providers: { - id: string; - success: boolean; - }[]; - results: MWMedia[]; - stats: { - total: number; - failed: number; - succeeded: number; - }; -} +export enum MWMediaType { + MOVIE = "movie", + SERIES = "series", + ANIME = "anime", +} + +export interface MWPortableMedia { + mediaId: string; + mediaType: MWMediaType; + providerId: string; + seasonId?: string; + episodeId?: string; +} + +export type MWMediaStreamType = "m3u8" | "mp4"; +export interface MWMediaCaption { + id: string; + url: string; + label: string; +} +export interface MWMediaStream { + url: string; + type: MWMediaStreamType; + captions: MWMediaCaption[]; +} + +export interface MWMediaMeta extends MWPortableMedia { + title: string; + year: string; + seasonCount?: number; +} + +export interface MWMediaEpisode { + sort: number; + id: string; + title: string; +} +export interface MWMediaSeason { + sort: number; + id: string; + title?: string; + type: "season" | "special"; + episodes: MWMediaEpisode[]; +} +export interface MWMediaSeasons { + seasons: MWMediaSeason[]; +} + +export interface MWMedia extends MWMediaMeta { + seriesData?: MWMediaSeasons; +} + +export type MWProviderMediaResult = Omit; + +export interface MWQuery { + searchQuery: string; + type: MWMediaType; +} + +export interface MWMediaProviderBase { + id: string; // id of provider, must be unique + enabled: boolean; + type: MWMediaType[]; + displayName: string; + + getMediaFromPortable(media: MWPortableMedia): Promise; + searchForMedia(query: MWQuery): Promise; + getStream(media: MWPortableMedia): Promise; + getSeasonDataFromMedia?: (media: MWPortableMedia) => Promise; +} + +export type MWMediaProviderSeries = MWMediaProviderBase & { + getSeasonDataFromMedia: (media: MWPortableMedia) => Promise; +}; + +export type MWMediaProvider = MWMediaProviderBase; + +export interface MWMediaProviderMetadata { + exists: boolean; + id?: string; + enabled: boolean; + type: MWMediaType[]; + provider?: MWMediaProvider; +} + +export interface MWMassProviderOutput { + providers: { + id: string; + success: boolean; + }[]; + results: MWMedia[]; + stats: { + total: number; + failed: number; + succeeded: number; + }; +} diff --git a/src/setup/i18n.ts b/src/setup/i18n.ts index 8ab960b1..312380b1 100644 --- a/src/setup/i18n.ts +++ b/src/setup/i18n.ts @@ -1,8 +1,8 @@ -import i18n from 'i18next'; -import { initReactI18next } from 'react-i18next'; +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; -import Backend from 'i18next-http-backend'; -import LanguageDetector from 'i18next-browser-languagedetector'; +import Backend from "i18next-http-backend"; +import LanguageDetector from "i18next-browser-languagedetector"; i18n // load translation using http -> see /public/locales (i.e. https://github.com/i18next/react-i18next/tree/master/example/react/public/locales) @@ -17,12 +17,11 @@ i18n // init i18next // for all options read: https://www.i18next.com/overview/configuration-options .init({ - fallbackLng: 'en-GB', + fallbackLng: "en-GB", interpolation: { escapeValue: false, // not needed for react as it escapes by default - } + }, }); - -export default i18n; \ No newline at end of file +export default i18n; diff --git a/src/state/bookmark/index.ts b/src/state/bookmark/index.ts index 1b3fa9eb..2edd280c 100644 --- a/src/state/bookmark/index.ts +++ b/src/state/bookmark/index.ts @@ -1 +1 @@ -export * from "./context"; \ No newline at end of file +export * from "./context"; diff --git a/src/views/MediaView.tsx b/src/views/MediaView.tsx index 02ea86e0..88fe66df 100644 --- a/src/views/MediaView.tsx +++ b/src/views/MediaView.tsx @@ -1,5 +1,6 @@ import { ReactElement, useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; +import { useTranslation } from "react-i18next"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icons } from "@/components/Icon"; import { Navigation } from "@/components/layout/Navigation"; @@ -29,7 +30,6 @@ import { useBookmarkContext, } from "@/state/bookmark"; import { getWatchedFromPortable, useWatchedContext } from "@/state/watched"; -import { useTranslation } from "react-i18next"; import { NotFoundChecks } from "./notfound/NotFoundChecks"; interface StyledMediaViewProps { diff --git a/src/views/notfound/NotFoundView.tsx b/src/views/notfound/NotFoundView.tsx index 6ba492ac..14cb7829 100644 --- a/src/views/notfound/NotFoundView.tsx +++ b/src/views/notfound/NotFoundView.tsx @@ -1,10 +1,10 @@ import { ReactNode } from "react"; +import { useTranslation } from "react-i18next"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icons } from "@/components/Icon"; import { Navigation } from "@/components/layout/Navigation"; import { ArrowLink } from "@/components/text/ArrowLink"; import { Title } from "@/components/text/Title"; -import { useTranslation } from "react-i18next"; function NotFoundWrapper(props: { children?: ReactNode }) { return ( diff --git a/src/views/search/HomeView.tsx b/src/views/search/HomeView.tsx index 180d456e..16f68e7c 100644 --- a/src/views/search/HomeView.tsx +++ b/src/views/search/HomeView.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from "react-i18next"; import { Icons } from "@/components/Icon"; import { SectionHeading } from "@/components/layout/SectionHeading"; import { MediaGrid } from "@/components/media/MediaGrid"; @@ -7,7 +8,6 @@ import { useBookmarkContext, } from "@/state/bookmark"; import { useWatchedContext } from "@/state/watched"; -import { useTranslation } from "react-i18next"; function Bookmarks() { const { t } = useTranslation(); diff --git a/src/views/search/SearchLoadingView.tsx b/src/views/search/SearchLoadingView.tsx index 6d02ff51..54cbeef9 100644 --- a/src/views/search/SearchLoadingView.tsx +++ b/src/views/search/SearchLoadingView.tsx @@ -1,5 +1,5 @@ -import { Loading } from "@/components/layout/Loading"; import { useTranslation } from "react-i18next"; +import { Loading } from "@/components/layout/Loading"; export function SearchLoadingView() { const { t } = useTranslation(); diff --git a/src/views/search/SearchResultsPartial.tsx b/src/views/search/SearchResultsPartial.tsx index 96934fa8..59281093 100644 --- a/src/views/search/SearchResultsPartial.tsx +++ b/src/views/search/SearchResultsPartial.tsx @@ -1,6 +1,6 @@ +import { useEffect, useMemo, useState } from "react"; import { useDebounce } from "@/hooks/useDebounce"; import { MWQuery } from "@/providers"; -import { useEffect, useMemo, useState } from "react"; import { HomeView } from "./HomeView"; import { SearchLoadingView } from "./SearchLoadingView"; import { SearchResultsView } from "./SearchResultsView"; diff --git a/src/views/search/SearchResultsView.tsx b/src/views/search/SearchResultsView.tsx index 5f602988..6a775a23 100644 --- a/src/views/search/SearchResultsView.tsx +++ b/src/views/search/SearchResultsView.tsx @@ -1,3 +1,5 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icons } from "@/components/Icon"; import { SectionHeading } from "@/components/layout/SectionHeading"; @@ -5,8 +7,6 @@ import { MediaGrid } from "@/components/media/MediaGrid"; import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; import { useLoading } from "@/hooks/useLoading"; import { MWMassProviderOutput, MWQuery, SearchProviders } from "@/providers"; -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; import { SearchLoadingView } from "./SearchLoadingView"; function SearchSuffix(props: { diff --git a/src/views/search/SearchView.tsx b/src/views/search/SearchView.tsx index c786cbe1..ad61b696 100644 --- a/src/views/search/SearchView.tsx +++ b/src/views/search/SearchView.tsx @@ -1,12 +1,12 @@ import { useCallback, useState } from "react"; +import Sticky from "react-stickynode"; +import { useTranslation } from "react-i18next"; import { Navigation } from "@/components/layout/Navigation"; import { ThinContainer } from "@/components/layout/ThinContainer"; import { SearchBarInput } from "@/components/SearchBar"; -import Sticky from "react-stickynode"; import { Title } from "@/components/text/Title"; import { useSearchQuery } from "@/hooks/useSearchQuery"; import { WideContainer } from "@/components/layout/WideContainer"; -import { useTranslation } from "react-i18next"; import { SearchResultsPartial } from "./SearchResultsPartial"; export function SearchView() { From 218a14d5f6a72eea95453a233d8c53784d2fcf26 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sun, 8 Jan 2023 16:23:42 +0100 Subject: [PATCH 009/135] fullscreen video --- src/components/video/VideoContext.tsx | 3 +- src/components/video/VideoPlayer.tsx | 14 +++++-- .../video/controls/FullscreenControl.tsx | 39 +++++++++---------- .../video/controls/PauseControl.tsx | 10 +++-- src/components/video/hooks/controlVideo.ts | 17 +++++++- src/components/video/hooks/useVideoPlayer.ts | 29 +++++++++++--- src/views/TestView.tsx | 12 +++--- 7 files changed, 84 insertions(+), 40 deletions(-) diff --git a/src/components/video/VideoContext.tsx b/src/components/video/VideoContext.tsx index bc1bedea..bf8d4e95 100644 --- a/src/components/video/VideoContext.tsx +++ b/src/components/video/VideoContext.tsx @@ -53,8 +53,9 @@ export const VideoPlayerDispatchContext = createContext< export function VideoPlayerContextProvider(props: { children: React.ReactNode; player: MutableRefObject; + wrapper: MutableRefObject; }) { - const { playerState } = useVideoPlayer(props.player); + const { playerState } = useVideoPlayer(props.player, props.wrapper); const [videoData, dispatch] = useReducer( videoPlayerContextReducer, initial diff --git a/src/components/video/VideoPlayer.tsx b/src/components/video/VideoPlayer.tsx index d1b7f341..1dddbb1f 100644 --- a/src/components/video/VideoPlayer.tsx +++ b/src/components/video/VideoPlayer.tsx @@ -9,7 +9,7 @@ const VideoPlayerInternals = forwardRef((_, ref) => { const video = useContext(VideoPlayerContext); return ( -

+ +
{props.children}
+
); } diff --git a/src/components/video/controls/FullscreenControl.tsx b/src/components/video/controls/FullscreenControl.tsx index 62c9e9f3..1cd2ce41 100644 --- a/src/components/video/controls/FullscreenControl.tsx +++ b/src/components/video/controls/FullscreenControl.tsx @@ -1,27 +1,24 @@ -// import { useCallback, useContext } from "react"; -// import { -// VideoPlayerContext, -// VideoPlayerDispatchContext, -// } from "../VideoContext"; +import { useCallback } from "react"; +import { useVideoPlayerState } from "../VideoContext"; export function FullscreenControl() { - return

Hello world

; - // const dispatch = useContext(VideoPlayerDispatchContext); - // const video = useContext(VideoPlayerContext); + const { videoState } = useVideoPlayerState(); - // const handleClick = useCallback(() => { - // dispatch({ - // type: "FULLSCREEN", - // do: video.fullscreen ? "EXIT" : "ENTER", - // }); - // }, [video, dispatch]); + const handleClick = useCallback(() => { + if (videoState.isFullscreen) videoState.exitFullscreen(); + else videoState.enterFullscreen(); + }, [videoState]); - // let text = "not fullscreen"; - // if (video.fullscreen) text = "in fullscreen"; + let text = "not fullscreen"; + if (videoState.isFullscreen) text = "in fullscreen"; - // return ( - // - // ); + return ( + + ); } diff --git a/src/components/video/controls/PauseControl.tsx b/src/components/video/controls/PauseControl.tsx index 3b4c408d..3f19100d 100644 --- a/src/components/video/controls/PauseControl.tsx +++ b/src/components/video/controls/PauseControl.tsx @@ -9,11 +9,15 @@ export function PauseControl() { else videoState.play(); }, [videoState]); - let text = "paused"; - if (videoState?.isPlaying) text = "playing"; + let text = + videoState.isPlaying || videoState.isSeeking ? "playing" : "paused"; return ( - ); diff --git a/src/components/video/hooks/controlVideo.ts b/src/components/video/hooks/controlVideo.ts index 4b980526..824a8d42 100644 --- a/src/components/video/hooks/controlVideo.ts +++ b/src/components/video/hooks/controlVideo.ts @@ -1,14 +1,21 @@ export interface PlayerControls { play(): void; pause(): void; + exitFullscreen(): void; + enterFullscreen(): void; } export const initialControls: PlayerControls = { play: () => null, pause: () => null, + enterFullscreen: () => null, + exitFullscreen: () => null, }; -export function populateControls(player: HTMLVideoElement): PlayerControls { +export function populateControls( + player: HTMLVideoElement, + wrapper: HTMLDivElement +): PlayerControls { return { play() { player.play(); @@ -16,5 +23,13 @@ export function populateControls(player: HTMLVideoElement): PlayerControls { pause() { player.pause(); }, + enterFullscreen() { + if (!document.fullscreenEnabled || document.fullscreenElement) return; + wrapper.requestFullscreen(); + }, + exitFullscreen() { + if (!document.fullscreenElement) return; + document.exitFullscreen(); + }, }; } diff --git a/src/components/video/hooks/useVideoPlayer.ts b/src/components/video/hooks/useVideoPlayer.ts index 2e513951..029f8cc8 100644 --- a/src/components/video/hooks/useVideoPlayer.ts +++ b/src/components/video/hooks/useVideoPlayer.ts @@ -8,11 +8,15 @@ import { export type PlayerState = { isPlaying: boolean; isPaused: boolean; + isSeeking: boolean; + isFullscreen: boolean; } & PlayerControls; -export const initialPlayerState = { +export const initialPlayerState: PlayerState = { isPlaying: false, isPaused: true, + isFullscreen: false, + isSeeking: false, ...initialControls, }; @@ -24,6 +28,8 @@ function readState(player: HTMLVideoElement, update: SetPlayer) { }; state.isPaused = player.paused; state.isPlaying = !player.paused; + state.isFullscreen = !!document.fullscreenElement; + state.isSeeking = player.seeking; update(state); } @@ -35,19 +41,32 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { player.addEventListener("play", () => { update((s) => ({ ...s, isPaused: false, isPlaying: true })); }); + player.addEventListener("seeking", () => { + update((s) => ({ ...s, isSeeking: true })); + }); + player.addEventListener("seeked", () => { + update((s) => ({ ...s, isSeeking: false })); + }); + document.addEventListener("fullscreenchange", () => { + update((s) => ({ ...s, isFullscreen: !!document.fullscreenElement })); + }); } -export function useVideoPlayer(ref: MutableRefObject) { +export function useVideoPlayer( + ref: MutableRefObject, + wrapperRef: MutableRefObject +) { const [state, setState] = useState(initialPlayerState); useEffect(() => { const player = ref.current; - if (player) { + const wrapper = wrapperRef.current; + if (player && wrapper) { readState(player, setState); registerListeners(player, setState); - setState((s) => ({ ...s, ...populateControls(player) })); + setState((s) => ({ ...s, ...populateControls(player, wrapper) })); } - }, [ref]); + }, [ref, wrapperRef]); return { playerState: state, diff --git a/src/views/TestView.tsx b/src/views/TestView.tsx index 01e0af45..c87eceba 100644 --- a/src/views/TestView.tsx +++ b/src/views/TestView.tsx @@ -7,10 +7,12 @@ import { VideoPlayer } from "@/components/video/VideoPlayer"; export function TestView() { return ( - - - - - +
+ + + + + +
); } From 61abce93869af56c788cc3a18f6e251c232f0b77 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sun, 8 Jan 2023 17:51:38 +0100 Subject: [PATCH 010/135] buffering --- src/components/video/VideoPlayer.tsx | 2 +- .../video/controls/PauseControl.tsx | 2 +- .../video/controls/ProgressControl.tsx | 47 +++++++++++ .../video/controls/VolumeControl.tsx | 34 ++++++++ src/components/video/hooks/controlVideo.ts | 18 +++++ src/components/video/hooks/useVideoPlayer.ts | 80 ++++++++++++++++--- src/components/video/hooks/utils.ts | 8 ++ src/views/TestView.tsx | 19 ++++- 8 files changed, 197 insertions(+), 13 deletions(-) create mode 100644 src/components/video/controls/ProgressControl.tsx create mode 100644 src/components/video/controls/VolumeControl.tsx create mode 100644 src/components/video/hooks/utils.ts diff --git a/src/components/video/VideoPlayer.tsx b/src/components/video/VideoPlayer.tsx index 1dddbb1f..c442f6e5 100644 --- a/src/components/video/VideoPlayer.tsx +++ b/src/components/video/VideoPlayer.tsx @@ -9,7 +9,7 @@ const VideoPlayerInternals = forwardRef((_, ref) => { const video = useContext(VideoPlayerContext); return ( -
+
+
+
+ ); +} diff --git a/src/components/video/controls/VolumeControl.tsx b/src/components/video/controls/VolumeControl.tsx new file mode 100644 index 00000000..92499ee6 --- /dev/null +++ b/src/components/video/controls/VolumeControl.tsx @@ -0,0 +1,34 @@ +import { useCallback, useRef } from "react"; +import { useVideoPlayerState } from "../VideoContext"; + +export function VolumeControl() { + const { videoState } = useVideoPlayerState(); + const ref = useRef(null); + + const percentage = `${(videoState.volume * 100).toFixed(2)}%`; + + const handleClick = useCallback( + (e: React.MouseEvent) => { + if (!ref.current) return; + const rect = ref.current.getBoundingClientRect(); + const pos = (e.pageX - rect.left) / ref.current.offsetWidth; + videoState.setVolume(pos); + }, + [videoState, ref] + ); + + return ( +
+
+
+ ); +} diff --git a/src/components/video/hooks/controlVideo.ts b/src/components/video/hooks/controlVideo.ts index 824a8d42..f4e2bdcb 100644 --- a/src/components/video/hooks/controlVideo.ts +++ b/src/components/video/hooks/controlVideo.ts @@ -3,6 +3,8 @@ export interface PlayerControls { pause(): void; exitFullscreen(): void; enterFullscreen(): void; + setTime(time: number): void; + setVolume(volume: number): void; } export const initialControls: PlayerControls = { @@ -10,6 +12,8 @@ export const initialControls: PlayerControls = { pause: () => null, enterFullscreen: () => null, exitFullscreen: () => null, + setTime: () => null, + setVolume: () => null, }; export function populateControls( @@ -31,5 +35,19 @@ export function populateControls( if (!document.fullscreenElement) return; document.exitFullscreen(); }, + setTime(t) { + // clamp time between 0 and max duration + let time = Math.min(t, player.duration); + time = Math.max(0, time); + // eslint-disable-next-line no-param-reassign + player.currentTime = time; + }, + setVolume(v) { + // clamp time between 0 and 1 + let volume = Math.min(v, 1); + volume = Math.max(0, volume); + // eslint-disable-next-line no-param-reassign + player.volume = volume; + }, }; } diff --git a/src/components/video/hooks/useVideoPlayer.ts b/src/components/video/hooks/useVideoPlayer.ts index 029f8cc8..9682b7bd 100644 --- a/src/components/video/hooks/useVideoPlayer.ts +++ b/src/components/video/hooks/useVideoPlayer.ts @@ -4,12 +4,17 @@ import { PlayerControls, populateControls, } from "./controlVideo"; +import { handleBuffered } from "./utils"; export type PlayerState = { isPlaying: boolean; isPaused: boolean; isSeeking: boolean; isFullscreen: boolean; + time: number; + duration: number; + volume: number; + buffered: number; } & PlayerControls; export const initialPlayerState: PlayerState = { @@ -17,6 +22,10 @@ export const initialPlayerState: PlayerState = { isPaused: true, isFullscreen: false, isSeeking: false, + time: 0, + duration: 0, + volume: 0, + buffered: 0, ...initialControls, }; @@ -30,26 +39,77 @@ function readState(player: HTMLVideoElement, update: SetPlayer) { state.isPlaying = !player.paused; state.isFullscreen = !!document.fullscreenElement; state.isSeeking = player.seeking; + state.time = player.currentTime; + state.duration = player.duration; + state.volume = player.volume; + state.buffered = handleBuffered(player.currentTime, player.buffered); update(state); } function registerListeners(player: HTMLVideoElement, update: SetPlayer) { - player.addEventListener("pause", () => { + const pause = () => { update((s) => ({ ...s, isPaused: true, isPlaying: false })); - }); - player.addEventListener("play", () => { + }; + const play = () => { update((s) => ({ ...s, isPaused: false, isPlaying: true })); - }); - player.addEventListener("seeking", () => { + }; + const seeking = () => { update((s) => ({ ...s, isSeeking: true })); - }); - player.addEventListener("seeked", () => { + }; + const seeked = () => { update((s) => ({ ...s, isSeeking: false })); - }); - document.addEventListener("fullscreenchange", () => { + }; + const fullscreenchange = () => { update((s) => ({ ...s, isFullscreen: !!document.fullscreenElement })); - }); + }; + const timeupdate = () => { + update((s) => ({ + ...s, + duration: player.duration, + time: player.currentTime, + })); + }; + const loadedmetadata = () => { + update((s) => ({ + ...s, + duration: player.duration, + })); + }; + const volumechange = () => { + update((s) => ({ + ...s, + volume: player.volume, + })); + }; + const progress = () => { + update((s) => ({ + ...s, + buffered: handleBuffered(player.currentTime, player.buffered), + })); + }; + + player.addEventListener("pause", pause); + player.addEventListener("play", play); + player.addEventListener("seeking", seeking); + player.addEventListener("seeked", seeked); + document.addEventListener("fullscreenchange", fullscreenchange); + player.addEventListener("timeupdate", timeupdate); + player.addEventListener("loadedmetadata", loadedmetadata); + player.addEventListener("volumechange", volumechange); + player.addEventListener("progress", progress); + + return () => { + player.removeEventListener("pause", pause); + player.removeEventListener("play", play); + player.removeEventListener("seeking", seeking); + player.removeEventListener("seeked", seeked); + document.removeEventListener("fullscreenchange", fullscreenchange); + player.removeEventListener("timeupdate", timeupdate); + player.removeEventListener("loadedmetadata", loadedmetadata); + player.removeEventListener("volumechange", volumechange); + player.removeEventListener("progress", progress); + }; } export function useVideoPlayer( diff --git a/src/components/video/hooks/utils.ts b/src/components/video/hooks/utils.ts new file mode 100644 index 00000000..ee19ae6a --- /dev/null +++ b/src/components/video/hooks/utils.ts @@ -0,0 +1,8 @@ +export function handleBuffered(time: number, buffered: TimeRanges): number { + for (let i = 0; i < buffered.length; i += 1) { + if (buffered.start(buffered.length - 1 - i) < time) { + return buffered.end(buffered.length - 1 - i); + } + } + return 0; +} diff --git a/src/views/TestView.tsx b/src/views/TestView.tsx index c87eceba..fdc3d8e6 100644 --- a/src/views/TestView.tsx +++ b/src/views/TestView.tsx @@ -1,17 +1,34 @@ import { FullscreenControl } from "@/components/video/controls/FullscreenControl"; import { PauseControl } from "@/components/video/controls/PauseControl"; +import { ProgressControl } from "@/components/video/controls/ProgressControl"; import { SourceControl } from "@/components/video/controls/SourceControl"; +import { VolumeControl } from "@/components/video/controls/VolumeControl"; import { VideoPlayer } from "@/components/video/VideoPlayer"; // test videos: https://gist.github.com/jsturgis/3b19447b304616f18657 +// TODO video todos: +// - captions +// - make pretty +// - show fullscreen button depending on is available (document.fullscreenEnabled) +// - better seeking +// - improve seekables +// - buffering +// - error handling +// - auto-play prop option +// - middle pause button +// - improve pausing while seeking/buffering +// - captions +// - show formatted time export function TestView() { return (
- + + +
); From 09634c6f972175070b08e5a9f28eb17560c20ef0 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sun, 8 Jan 2023 17:59:25 +0100 Subject: [PATCH 011/135] todos --- src/views/TestView.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/views/TestView.tsx b/src/views/TestView.tsx index fdc3d8e6..24007f53 100644 --- a/src/views/TestView.tsx +++ b/src/views/TestView.tsx @@ -20,6 +20,8 @@ import { VideoPlayer } from "@/components/video/VideoPlayer"; // - improve pausing while seeking/buffering // - captions // - show formatted time +// - IOS support: (no volume, fullscreen video element instead of wrapper) +// - IpadOS support: (fullscreen video wrapper should work, see (lookmovie.io) ) export function TestView() { return (
From a9cf056276e8536d98935eee1ce136c86eaefc8b Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sun, 8 Jan 2023 18:01:51 +0100 Subject: [PATCH 012/135] add hls todo --- src/views/TestView.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/views/TestView.tsx b/src/views/TestView.tsx index 24007f53..3cd902ca 100644 --- a/src/views/TestView.tsx +++ b/src/views/TestView.tsx @@ -22,6 +22,7 @@ import { VideoPlayer } from "@/components/video/VideoPlayer"; // - show formatted time // - IOS support: (no volume, fullscreen video element instead of wrapper) // - IpadOS support: (fullscreen video wrapper should work, see (lookmovie.io) ) +// - HLS support: feature detection otherwise use HLS.js export function TestView() { return (
From 44149203cb8a0d126e7fd8c266202d1eeeff8ce7 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sun, 8 Jan 2023 20:36:46 +0100 Subject: [PATCH 013/135] autoplay and fullscreen feature detection --- src/components/video/VideoPlayer.tsx | 18 +++++++++++++++--- .../video/controls/FullscreenControl.tsx | 4 ++++ src/views/TestView.tsx | 16 ++++++++++++---- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/components/video/VideoPlayer.tsx b/src/components/video/VideoPlayer.tsx index c442f6e5..8857d9fa 100644 --- a/src/components/video/VideoPlayer.tsx +++ b/src/components/video/VideoPlayer.tsx @@ -2,14 +2,23 @@ import { forwardRef, useContext, useRef } from "react"; import { VideoPlayerContext, VideoPlayerContextProvider } from "./VideoContext"; interface VideoPlayerProps { + autoPlay?: boolean; children?: React.ReactNode; } -const VideoPlayerInternals = forwardRef((_, ref) => { +const VideoPlayerInternals = forwardRef< + HTMLVideoElement, + { autoPlay: boolean } +>((props, ref) => { const video = useContext(VideoPlayerContext); return ( -
diff --git a/src/components/video/controls/FullscreenControl.tsx b/src/components/video/controls/FullscreenControl.tsx index 1cd2ce41..c045ceb6 100644 --- a/src/components/video/controls/FullscreenControl.tsx +++ b/src/components/video/controls/FullscreenControl.tsx @@ -1,6 +1,8 @@ import { useCallback } from "react"; import { useVideoPlayerState } from "../VideoContext"; +const canFullscreen = document.fullscreenEnabled; + export function FullscreenControl() { const { videoState } = useVideoPlayerState(); @@ -9,6 +11,8 @@ export function FullscreenControl() { else videoState.enterFullscreen(); }, [videoState]); + if (!canFullscreen) return null; + let text = "not fullscreen"; if (videoState.isFullscreen) text = "in fullscreen"; diff --git a/src/views/TestView.tsx b/src/views/TestView.tsx index 3cd902ca..d2d8d387 100644 --- a/src/views/TestView.tsx +++ b/src/views/TestView.tsx @@ -4,19 +4,18 @@ import { ProgressControl } from "@/components/video/controls/ProgressControl"; import { SourceControl } from "@/components/video/controls/SourceControl"; import { VolumeControl } from "@/components/video/controls/VolumeControl"; import { VideoPlayer } from "@/components/video/VideoPlayer"; +import { useCallback, useState } from "react"; // test videos: https://gist.github.com/jsturgis/3b19447b304616f18657 // TODO video todos: // - captions // - make pretty -// - show fullscreen button depending on is available (document.fullscreenEnabled) // - better seeking // - improve seekables // - buffering // - error handling -// - auto-play prop option -// - middle pause button +// - middle pause button + click to pause // - improve pausing while seeking/buffering // - captions // - show formatted time @@ -24,9 +23,18 @@ import { VideoPlayer } from "@/components/video/VideoPlayer"; // - IpadOS support: (fullscreen video wrapper should work, see (lookmovie.io) ) // - HLS support: feature detection otherwise use HLS.js export function TestView() { + const [show, setShow] = useState(false); + const handleClick = useCallback(() => { + setShow((v) => !v); + }, [setShow]); + + if (!show) { + return

Click me to show

; + } + return (
- + From b43b8b19e4c4c181b2652510faf0dc5eb768ece0 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sun, 8 Jan 2023 21:18:45 +0100 Subject: [PATCH 014/135] loading + time control Co-authored-by: James Hawkins --- .../video/controls/LoadingControl.tsx | 9 +++++ src/components/video/controls/TimeControl.tsx | 36 +++++++++++++++++++ src/components/video/hooks/useVideoPlayer.ts | 27 +++++++++++--- src/views/TestView.tsx | 7 ++-- 4 files changed, 71 insertions(+), 8 deletions(-) create mode 100644 src/components/video/controls/LoadingControl.tsx create mode 100644 src/components/video/controls/TimeControl.tsx diff --git a/src/components/video/controls/LoadingControl.tsx b/src/components/video/controls/LoadingControl.tsx new file mode 100644 index 00000000..489976b1 --- /dev/null +++ b/src/components/video/controls/LoadingControl.tsx @@ -0,0 +1,9 @@ +import { useVideoPlayerState } from "../VideoContext"; + +export function LoadingControl() { + const { videoState } = useVideoPlayerState(); + + if (!videoState.isLoading) return null; + + return

Loading...

; +} diff --git a/src/components/video/controls/TimeControl.tsx b/src/components/video/controls/TimeControl.tsx new file mode 100644 index 00000000..3cf151ea --- /dev/null +++ b/src/components/video/controls/TimeControl.tsx @@ -0,0 +1,36 @@ +import { useVideoPlayerState } from "../VideoContext"; + +function durationExceedsHour(secs: number): boolean { + return secs > 60 * 60; +} + +function formatSeconds(secs: number, showHours = false): string { + let time = secs; + const seconds = time % 60; + + time /= 60; + const minutes = time % 60; + + time /= 60; + const hours = minutes % 60; + + const minuteString = `${Math.round(minutes) + .toString() + .padStart(2)}:${Math.round(seconds).toString().padStart(2, "0")}`; + + if (!showHours) return minuteString; + return `${Math.round(hours).toString()}:${minuteString}`; +} + +export function TimeControl() { + const { videoState } = useVideoPlayerState(); + const hasHours = durationExceedsHour(videoState.duration); + const time = formatSeconds(videoState.time, hasHours); + const duration = formatSeconds(videoState.duration, hasHours); + + return ( +

+ {time} / {duration} +

+ ); +} diff --git a/src/components/video/hooks/useVideoPlayer.ts b/src/components/video/hooks/useVideoPlayer.ts index 9682b7bd..d3d2d95b 100644 --- a/src/components/video/hooks/useVideoPlayer.ts +++ b/src/components/video/hooks/useVideoPlayer.ts @@ -10,6 +10,7 @@ export type PlayerState = { isPlaying: boolean; isPaused: boolean; isSeeking: boolean; + isLoading: boolean; isFullscreen: boolean; time: number; duration: number; @@ -21,6 +22,7 @@ export const initialPlayerState: PlayerState = { isPlaying: false, isPaused: true, isFullscreen: false, + isLoading: false, isSeeking: false, time: 0, duration: 0, @@ -43,16 +45,26 @@ function readState(player: HTMLVideoElement, update: SetPlayer) { state.duration = player.duration; state.volume = player.volume; state.buffered = handleBuffered(player.currentTime, player.buffered); + state.isLoading = false; update(state); } function registerListeners(player: HTMLVideoElement, update: SetPlayer) { const pause = () => { - update((s) => ({ ...s, isPaused: true, isPlaying: false })); + update((s) => ({ + ...s, + isPaused: true, + isPlaying: false, + })); }; - const play = () => { - update((s) => ({ ...s, isPaused: false, isPlaying: true })); + const playing = () => { + update((s) => ({ + ...s, + isPaused: false, + isPlaying: true, + isLoading: false, + })); }; const seeking = () => { update((s) => ({ ...s, isSeeking: true })); @@ -60,6 +72,9 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { const seeked = () => { update((s) => ({ ...s, isSeeking: false })); }; + const waiting = () => { + update((s) => ({ ...s, isLoading: true })); + }; const fullscreenchange = () => { update((s) => ({ ...s, isFullscreen: !!document.fullscreenElement })); }; @@ -90,7 +105,7 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { }; player.addEventListener("pause", pause); - player.addEventListener("play", play); + player.addEventListener("playing", playing); player.addEventListener("seeking", seeking); player.addEventListener("seeked", seeked); document.addEventListener("fullscreenchange", fullscreenchange); @@ -98,10 +113,11 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { player.addEventListener("loadedmetadata", loadedmetadata); player.addEventListener("volumechange", volumechange); player.addEventListener("progress", progress); + player.addEventListener("waiting", waiting); return () => { player.removeEventListener("pause", pause); - player.removeEventListener("play", play); + player.removeEventListener("playing", playing); player.removeEventListener("seeking", seeking); player.removeEventListener("seeked", seeked); document.removeEventListener("fullscreenchange", fullscreenchange); @@ -109,6 +125,7 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { player.removeEventListener("loadedmetadata", loadedmetadata); player.removeEventListener("volumechange", volumechange); player.removeEventListener("progress", progress); + player.removeEventListener("waiting", waiting); }; } diff --git a/src/views/TestView.tsx b/src/views/TestView.tsx index d2d8d387..bc0292a5 100644 --- a/src/views/TestView.tsx +++ b/src/views/TestView.tsx @@ -1,7 +1,9 @@ import { FullscreenControl } from "@/components/video/controls/FullscreenControl"; +import { LoadingControl } from "@/components/video/controls/LoadingControl"; import { PauseControl } from "@/components/video/controls/PauseControl"; import { ProgressControl } from "@/components/video/controls/ProgressControl"; import { SourceControl } from "@/components/video/controls/SourceControl"; +import { TimeControl } from "@/components/video/controls/TimeControl"; import { VolumeControl } from "@/components/video/controls/VolumeControl"; import { VideoPlayer } from "@/components/video/VideoPlayer"; import { useCallback, useState } from "react"; @@ -9,16 +11,13 @@ import { useCallback, useState } from "react"; // test videos: https://gist.github.com/jsturgis/3b19447b304616f18657 // TODO video todos: -// - captions // - make pretty // - better seeking // - improve seekables -// - buffering // - error handling // - middle pause button + click to pause // - improve pausing while seeking/buffering // - captions -// - show formatted time // - IOS support: (no volume, fullscreen video element instead of wrapper) // - IpadOS support: (fullscreen video wrapper should work, see (lookmovie.io) ) // - HLS support: feature detection otherwise use HLS.js @@ -39,6 +38,8 @@ export function TestView() { + +
From 098f6af0ae526f15b77950c59d4ce380419c697c Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sun, 8 Jan 2023 22:29:38 +0100 Subject: [PATCH 015/135] Backdrop + improved seeking --- src/components/video/DecoratedVideoPlayer.tsx | 24 +++++++++ src/components/video/VideoPlayer.tsx | 2 +- .../video/controls/BackdropControl.tsx | 53 +++++++++++++++++++ .../video/controls/ProgressControl.tsx | 45 ++++++++++++---- src/setup/index.css | 4 ++ src/views/TestView.tsx | 24 +++------ 6 files changed, 124 insertions(+), 28 deletions(-) create mode 100644 src/components/video/DecoratedVideoPlayer.tsx create mode 100644 src/components/video/controls/BackdropControl.tsx diff --git a/src/components/video/DecoratedVideoPlayer.tsx b/src/components/video/DecoratedVideoPlayer.tsx new file mode 100644 index 00000000..8c1af036 --- /dev/null +++ b/src/components/video/DecoratedVideoPlayer.tsx @@ -0,0 +1,24 @@ +import { BackdropControl } from "./controls/BackdropControl"; +import { FullscreenControl } from "./controls/FullscreenControl"; +import { LoadingControl } from "./controls/LoadingControl"; +import { PauseControl } from "./controls/PauseControl"; +import { ProgressControl } from "./controls/ProgressControl"; +import { TimeControl } from "./controls/TimeControl"; +import { VolumeControl } from "./controls/VolumeControl"; +import { VideoPlayer, VideoPlayerProps } from "./VideoPlayer"; + +export function DecoratedVideoPlayer(props: VideoPlayerProps) { + return ( + + + + + + + + + + {props.children} + + ); +} diff --git a/src/components/video/VideoPlayer.tsx b/src/components/video/VideoPlayer.tsx index 8857d9fa..4222365d 100644 --- a/src/components/video/VideoPlayer.tsx +++ b/src/components/video/VideoPlayer.tsx @@ -1,7 +1,7 @@ import { forwardRef, useContext, useRef } from "react"; import { VideoPlayerContext, VideoPlayerContextProvider } from "./VideoContext"; -interface VideoPlayerProps { +export interface VideoPlayerProps { autoPlay?: boolean; children?: React.ReactNode; } diff --git a/src/components/video/controls/BackdropControl.tsx b/src/components/video/controls/BackdropControl.tsx new file mode 100644 index 00000000..3629c86e --- /dev/null +++ b/src/components/video/controls/BackdropControl.tsx @@ -0,0 +1,53 @@ +import { useCallback, useRef, useState } from "react"; +import { useVideoPlayerState } from "../VideoContext"; + +interface BackdropControlProps { + children?: React.ReactNode; +} + +export function BackdropControl(props: BackdropControlProps) { + const { videoState } = useVideoPlayerState(); + const [moved, setMoved] = useState(false); + const timeout = useRef | null>(null); + + const handleMouseMove = useCallback(() => { + setMoved(true); + if (timeout.current) clearTimeout(timeout.current); + timeout.current = setTimeout(() => { + setMoved(false); + timeout.current = null; + }, 3000); + }, [timeout, setMoved]); + + const handleClick = useCallback(() => { + if (videoState.isPlaying) videoState.pause(); + else videoState.play(); + }, [videoState]); + + const showUI = moved || videoState.isPaused; + + return ( +
+
+
+
+
{showUI ? props.children : null}
+
+ ); +} diff --git a/src/components/video/controls/ProgressControl.tsx b/src/components/video/controls/ProgressControl.tsx index 98c0ee16..4d423845 100644 --- a/src/components/video/controls/ProgressControl.tsx +++ b/src/components/video/controls/ProgressControl.tsx @@ -1,34 +1,61 @@ -import { useCallback, useRef } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useVideoPlayerState } from "../VideoContext"; export function ProgressControl() { const { videoState } = useVideoPlayerState(); const ref = useRef(null); + const [mouseDown, setMouseDown] = useState(false); + const [progress, setProgress] = useState(0); - const watchProgress = `${( + let watchProgress = `${( (videoState.time / videoState.duration) * 100 ).toFixed(2)}%`; + if (mouseDown) watchProgress = `${progress}%`; + const bufferProgress = `${( (videoState.buffered / videoState.duration) * 100 ).toFixed(2)}%`; - const handleClick = useCallback( - (e: React.MouseEvent) => { + useEffect(() => { + function mouseMove(ev: MouseEvent) { + if (!mouseDown || !ref.current) return; + const rect = ref.current.getBoundingClientRect(); + const pos = ((ev.pageX - rect.left) / ref.current.offsetWidth) * 100; + setProgress(pos); + } + + function mouseUp(ev: MouseEvent) { + if (!mouseDown) return; + setMouseDown(false); + document.body.removeAttribute("data-no-select"); + if (!ref.current) return; const rect = ref.current.getBoundingClientRect(); - const pos = (e.pageX - rect.left) / ref.current.offsetWidth; + const pos = (ev.pageX - rect.left) / ref.current.offsetWidth; videoState.setTime(pos * videoState.duration); - }, - [videoState, ref] - ); + } + + document.addEventListener("mousemove", mouseMove); + document.addEventListener("mouseup", mouseUp); + + return () => { + document.removeEventListener("mousemove", mouseMove); + document.removeEventListener("mouseup", mouseUp); + }; + }, [mouseDown, videoState]); + + const handleMouseDown = useCallback(() => { + setMouseDown(true); + document.body.setAttribute("data-no-select", "true"); + }, []); return (
{ setShow((v) => !v); }, [setShow]); @@ -33,15 +27,9 @@ export function TestView() { return (
- - - - - - - + - +
); } From 024325f64047844c720719d36674d8e82b07a259 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Mon, 9 Jan 2023 21:51:24 +0100 Subject: [PATCH 016/135] styling of video player controls --- src/components/Icon.tsx | 12 ++ src/components/video/DecoratedVideoPlayer.tsx | 19 ++- src/components/video/VideoPlayer.tsx | 2 +- .../video/controls/BackdropControl.tsx | 32 +++-- .../video/controls/FullscreenControl.tsx | 21 ++-- .../video/controls/PauseControl.tsx | 22 ++-- .../video/controls/ProgressControl.tsx | 114 +++++++++--------- src/components/video/controls/TimeControl.tsx | 14 ++- .../video/controls/VolumeControl.tsx | 94 +++++++++++---- .../video/parts/VideoPlayerIconButton.tsx | 26 ++++ src/hooks/useProgressBar.ts | 66 ++++++++++ 11 files changed, 302 insertions(+), 120 deletions(-) create mode 100644 src/components/video/parts/VideoPlayerIconButton.tsx create mode 100644 src/hooks/useProgressBar.ts diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 65d47891..45ea593b 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -14,6 +14,12 @@ export enum Icons { MOVIE_WEB = "movieWeb", DISCORD = "discord", GITHUB = "github", + PLAY = "play", + PAUSE = "pause", + EXPAND = "expand", + COMPRESS = "compress", + VOLUME = "volume", + VOLUME_X = "volume_x", } export interface IconProps { @@ -37,6 +43,12 @@ const iconList: Record = { movieWeb: ``, discord: ``, github: ``, + play: ``, + pause: ``, + expand: ``, + compress: ``, + volume: ``, + volume_x: ``, }; export function Icon(props: IconProps) { diff --git a/src/components/video/DecoratedVideoPlayer.tsx b/src/components/video/DecoratedVideoPlayer.tsx index 8c1af036..3731c5a6 100644 --- a/src/components/video/DecoratedVideoPlayer.tsx +++ b/src/components/video/DecoratedVideoPlayer.tsx @@ -11,12 +11,19 @@ export function DecoratedVideoPlayer(props: VideoPlayerProps) { return ( - - - - - - +
+ +
+
+ +
+ + + +
+ +
+
{props.children} diff --git a/src/components/video/VideoPlayer.tsx b/src/components/video/VideoPlayer.tsx index 4222365d..89f632e4 100644 --- a/src/components/video/VideoPlayer.tsx +++ b/src/components/video/VideoPlayer.tsx @@ -31,7 +31,7 @@ export function VideoPlayer(props: VideoPlayerProps) { return (
| null>(null); + const clickareaRef = useRef(null); const handleMouseMove = useCallback(() => { setMoved(true); @@ -19,10 +20,19 @@ export function BackdropControl(props: BackdropControlProps) { }, 3000); }, [timeout, setMoved]); - const handleClick = useCallback(() => { - if (videoState.isPlaying) videoState.pause(); - else videoState.play(); - }, [videoState]); + const handleMouseLeave = useCallback(() => { + setMoved(false); + }, [setMoved]); + + const handleClick = useCallback( + (e: React.MouseEvent) => { + if (!clickareaRef.current || clickareaRef.current !== e.target) return; + + if (videoState.isPlaying) videoState.pause(); + else videoState.play(); + }, + [videoState, clickareaRef] + ); const showUI = moved || videoState.isPaused; @@ -30,24 +40,28 @@ export function BackdropControl(props: BackdropControlProps) {
-
{showUI ? props.children : null}
+
+ {showUI ? props.children : null} +
); } diff --git a/src/components/video/controls/FullscreenControl.tsx b/src/components/video/controls/FullscreenControl.tsx index c045ceb6..67b1e2e4 100644 --- a/src/components/video/controls/FullscreenControl.tsx +++ b/src/components/video/controls/FullscreenControl.tsx @@ -1,9 +1,15 @@ +import { Icons } from "@/components/Icon"; import { useCallback } from "react"; +import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton"; import { useVideoPlayerState } from "../VideoContext"; const canFullscreen = document.fullscreenEnabled; -export function FullscreenControl() { +interface Props { + className?: string; +} + +export function FullscreenControl(props: Props) { const { videoState } = useVideoPlayerState(); const handleClick = useCallback(() => { @@ -13,16 +19,11 @@ export function FullscreenControl() { if (!canFullscreen) return null; - let text = "not fullscreen"; - if (videoState.isFullscreen) text = "in fullscreen"; - return ( - + icon={videoState.isFullscreen ? Icons.COMPRESS : Icons.EXPAND} + /> ); } diff --git a/src/components/video/controls/PauseControl.tsx b/src/components/video/controls/PauseControl.tsx index 5b2c4466..e528469b 100644 --- a/src/components/video/controls/PauseControl.tsx +++ b/src/components/video/controls/PauseControl.tsx @@ -1,7 +1,13 @@ +import { Icons } from "@/components/Icon"; import { useCallback } from "react"; +import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton"; import { useVideoPlayerState } from "../VideoContext"; -export function PauseControl() { +interface Props { + className?: string; +} + +export function PauseControl(props: Props) { const { videoState } = useVideoPlayerState(); const handleClick = useCallback(() => { @@ -9,16 +15,14 @@ export function PauseControl() { else videoState.play(); }, [videoState]); - const text = - videoState.isPlaying || videoState.isSeeking ? "playing" : "paused"; + const icon = + videoState.isPlaying || videoState.isSeeking ? Icons.PAUSE : Icons.PLAY; return ( - + /> ); } diff --git a/src/components/video/controls/ProgressControl.tsx b/src/components/video/controls/ProgressControl.tsx index 4d423845..2b6c6777 100644 --- a/src/components/video/controls/ProgressControl.tsx +++ b/src/components/video/controls/ProgressControl.tsx @@ -1,74 +1,68 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { + makePercentage, + makePercentageString, + useProgressBar, +} from "@/hooks/useProgressBar"; +import { useCallback, useRef } from "react"; import { useVideoPlayerState } from "../VideoContext"; export function ProgressControl() { const { videoState } = useVideoPlayerState(); const ref = useRef(null); - const [mouseDown, setMouseDown] = useState(false); - const [progress, setProgress] = useState(0); - let watchProgress = `${( - (videoState.time / videoState.duration) * - 100 - ).toFixed(2)}%`; - if (mouseDown) watchProgress = `${progress}%`; - - const bufferProgress = `${( - (videoState.buffered / videoState.duration) * - 100 - ).toFixed(2)}%`; - - useEffect(() => { - function mouseMove(ev: MouseEvent) { - if (!mouseDown || !ref.current) return; - const rect = ref.current.getBoundingClientRect(); - const pos = ((ev.pageX - rect.left) / ref.current.offsetWidth) * 100; - setProgress(pos); - } - - function mouseUp(ev: MouseEvent) { - if (!mouseDown) return; - setMouseDown(false); - document.body.removeAttribute("data-no-select"); - - if (!ref.current) return; - const rect = ref.current.getBoundingClientRect(); - const pos = (ev.pageX - rect.left) / ref.current.offsetWidth; - videoState.setTime(pos * videoState.duration); - } - - document.addEventListener("mousemove", mouseMove); - document.addEventListener("mouseup", mouseUp); + const commitTime = useCallback( + (percentage) => { + videoState.setTime(percentage * videoState.duration); + }, + [videoState] + ); + const { dragging, dragPercentage, dragMouseDown } = useProgressBar( + ref, + commitTime + ); - return () => { - document.removeEventListener("mousemove", mouseMove); - document.removeEventListener("mouseup", mouseUp); - }; - }, [mouseDown, videoState]); + let watchProgress = makePercentageString( + makePercentage((videoState.time / videoState.duration) * 100) + ); + if (dragging) + watchProgress = makePercentageString(makePercentage(dragPercentage)); - const handleMouseDown = useCallback(() => { - setMouseDown(true); - document.body.setAttribute("data-no-select", "true"); - }, []); + const bufferProgress = makePercentageString( + makePercentage((videoState.buffered / videoState.duration) * 100) + ); return ( -
-
+
+ ref={ref} + className="-my-3 flex h-8 items-center" + onMouseDown={dragMouseDown} + > +
+
+
+
+
+
+
); } diff --git a/src/components/video/controls/TimeControl.tsx b/src/components/video/controls/TimeControl.tsx index 3cf151ea..35d86351 100644 --- a/src/components/video/controls/TimeControl.tsx +++ b/src/components/video/controls/TimeControl.tsx @@ -22,15 +22,21 @@ function formatSeconds(secs: number, showHours = false): string { return `${Math.round(hours).toString()}:${minuteString}`; } -export function TimeControl() { +interface Props { + className?: string; +} + +export function TimeControl(props: Props) { const { videoState } = useVideoPlayerState(); const hasHours = durationExceedsHour(videoState.duration); const time = formatSeconds(videoState.time, hasHours); const duration = formatSeconds(videoState.duration, hasHours); return ( -

- {time} / {duration} -

+
+

+ {time} / {duration} +

+
); } diff --git a/src/components/video/controls/VolumeControl.tsx b/src/components/video/controls/VolumeControl.tsx index 92499ee6..ea30e536 100644 --- a/src/components/video/controls/VolumeControl.tsx +++ b/src/components/video/controls/VolumeControl.tsx @@ -1,34 +1,86 @@ -import { useCallback, useRef } from "react"; +import { Icon, Icons } from "@/components/Icon"; +import { + makePercentage, + makePercentageString, + useProgressBar, +} from "@/hooks/useProgressBar"; +import { useCallback, useRef, useState } from "react"; import { useVideoPlayerState } from "../VideoContext"; -export function VolumeControl() { +interface Props { + className?: string; +} + +// TODO make hoveredOnce false when control bar appears + +export function VolumeControl(props: Props) { const { videoState } = useVideoPlayerState(); const ref = useRef(null); + const [storedVolume, setStoredVolume] = useState(1); + const [hoveredOnce, setHoveredOnce] = useState(false); - const percentage = `${(videoState.volume * 100).toFixed(2)}%`; - - const handleClick = useCallback( - (e: React.MouseEvent) => { - if (!ref.current) return; - const rect = ref.current.getBoundingClientRect(); - const pos = (e.pageX - rect.left) / ref.current.offsetWidth; - videoState.setVolume(pos); + const commitVolume = useCallback( + (percentage) => { + videoState.setVolume(percentage); + setStoredVolume(percentage); }, - [videoState, ref] + [videoState, setStoredVolume] ); + const { dragging, dragPercentage, dragMouseDown } = useProgressBar( + ref, + commitVolume, + true + ); + + const handleClick = useCallback(() => { + if (videoState.volume > 0) { + videoState.setVolume(0); + setStoredVolume(videoState.volume); + } else { + videoState.setVolume(storedVolume > 0 ? storedVolume : 1); + } + }, [videoState, setStoredVolume, storedVolume]); + + const handleMouseEnter = useCallback(() => { + setHoveredOnce(true); + }, [setHoveredOnce]); + + let percentage = makePercentage(videoState.volume * 100); + if (dragging) percentage = makePercentage(dragPercentage); + const percentageString = makePercentageString(percentage); return ( -
+
+ className="pointer-events-auto flex cursor-pointer items-center" + onMouseEnter={handleMouseEnter} + > +
+ 0 ? Icons.VOLUME : Icons.VOLUME_X} /> +
+
+
+
+
+
+
+
+
+
+
); } diff --git a/src/components/video/parts/VideoPlayerIconButton.tsx b/src/components/video/parts/VideoPlayerIconButton.tsx new file mode 100644 index 00000000..610550bb --- /dev/null +++ b/src/components/video/parts/VideoPlayerIconButton.tsx @@ -0,0 +1,26 @@ +import { Icon, Icons } from "@/components/Icon"; +import React from "react"; + +export interface VideoPlayerIconButtonProps { + onClick?: (e: React.MouseEvent) => void; + icon: Icons; + text?: string; + className?: string; +} + +export function VideoPlayerIconButton(props: VideoPlayerIconButtonProps) { + return ( +
+ +
+ ); +} diff --git a/src/hooks/useProgressBar.ts b/src/hooks/useProgressBar.ts new file mode 100644 index 00000000..d0fee788 --- /dev/null +++ b/src/hooks/useProgressBar.ts @@ -0,0 +1,66 @@ +import React, { RefObject, useCallback, useEffect, useState } from "react"; + +export function makePercentageString(num: number) { + return `${num.toFixed(2)}%`; +} + +export function makePercentage(num: number) { + return Number(Math.max(0, Math.min(num, 100)).toFixed(2)); +} + +export function useProgressBar( + barRef: RefObject, + commit: (percentage: number) => void, + commitImmediately = false +) { + const [mouseDown, setMouseDown] = useState(false); + const [progress, setProgress] = useState(0); + + useEffect(() => { + function mouseMove(ev: MouseEvent) { + if (!mouseDown || !barRef.current) return; + const rect = barRef.current.getBoundingClientRect(); + const pos = ((ev.pageX - rect.left) / barRef.current.offsetWidth) * 100; + setProgress(pos); + if (commitImmediately) commit(pos); + } + + function mouseUp(ev: MouseEvent) { + if (!mouseDown) return; + setMouseDown(false); + document.body.removeAttribute("data-no-select"); + + if (!barRef.current) return; + const rect = barRef.current.getBoundingClientRect(); + const pos = (ev.pageX - rect.left) / barRef.current.offsetWidth; + commit(pos); + } + + document.addEventListener("mousemove", mouseMove); + document.addEventListener("mouseup", mouseUp); + + return () => { + document.removeEventListener("mousemove", mouseMove); + document.removeEventListener("mouseup", mouseUp); + }; + }, [mouseDown, barRef, commit, commitImmediately]); + + const dragMouseDown = useCallback( + (ev: React.MouseEvent) => { + setMouseDown(true); + document.body.setAttribute("data-no-select", "true"); + + if (!barRef.current) return; + const rect = barRef.current.getBoundingClientRect(); + const pos = ((ev.pageX - rect.left) / barRef.current.offsetWidth) * 100; + setProgress(pos); + }, + [setProgress, barRef] + ); + + return { + dragging: mouseDown, + dragPercentage: progress, + dragMouseDown, + }; +} From 351b35ef98e3ac8e54fe8c5c631d36db3689f26a Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Tue, 10 Jan 2023 00:27:04 +0100 Subject: [PATCH 017/135] add top bar and improve ui feel --- src/components/video/DecoratedVideoPlayer.tsx | 10 +++++-- .../video/controls/BackdropControl.tsx | 6 ++-- .../video/controls/ProgressControl.tsx | 4 +-- .../video/controls/VolumeControl.tsx | 4 +-- .../video/parts/VideoPlayerHeader.tsx | 28 +++++++++++++++++++ src/views/TestView.tsx | 11 ++++++-- 6 files changed, 52 insertions(+), 11 deletions(-) create mode 100644 src/components/video/parts/VideoPlayerHeader.tsx diff --git a/src/components/video/DecoratedVideoPlayer.tsx b/src/components/video/DecoratedVideoPlayer.tsx index 3731c5a6..01891a26 100644 --- a/src/components/video/DecoratedVideoPlayer.tsx +++ b/src/components/video/DecoratedVideoPlayer.tsx @@ -5,8 +5,11 @@ import { PauseControl } from "./controls/PauseControl"; import { ProgressControl } from "./controls/ProgressControl"; import { TimeControl } from "./controls/TimeControl"; import { VolumeControl } from "./controls/VolumeControl"; +import { VideoPlayerHeader } from "./parts/VideoPlayerHeader"; import { VideoPlayer, VideoPlayerProps } from "./VideoPlayer"; +// TODO animate items away when hidden + export function DecoratedVideoPlayer(props: VideoPlayerProps) { return ( @@ -14,9 +17,9 @@ export function DecoratedVideoPlayer(props: VideoPlayerProps) {
-
+
-
+
@@ -24,6 +27,9 @@ export function DecoratedVideoPlayer(props: VideoPlayerProps) {
+
+ +
{props.children} diff --git a/src/components/video/controls/BackdropControl.tsx b/src/components/video/controls/BackdropControl.tsx index d232d579..4026884f 100644 --- a/src/components/video/controls/BackdropControl.tsx +++ b/src/components/video/controls/BackdropControl.tsx @@ -5,6 +5,8 @@ interface BackdropControlProps { children?: React.ReactNode; } +// TODO add double click to toggle fullscreen + export function BackdropControl(props: BackdropControlProps) { const { videoState } = useVideoPlayerState(); const [moved, setMoved] = useState(false); @@ -50,12 +52,12 @@ export function BackdropControl(props: BackdropControlProps) { }`} />
diff --git a/src/components/video/controls/ProgressControl.tsx b/src/components/video/controls/ProgressControl.tsx index 2b6c6777..ec1cae1d 100644 --- a/src/components/video/controls/ProgressControl.tsx +++ b/src/components/video/controls/ProgressControl.tsx @@ -44,13 +44,13 @@ export function ProgressControl() { }`} >
diff --git a/src/components/video/parts/VideoPlayerHeader.tsx b/src/components/video/parts/VideoPlayerHeader.tsx new file mode 100644 index 00000000..d0cf55f2 --- /dev/null +++ b/src/components/video/parts/VideoPlayerHeader.tsx @@ -0,0 +1,28 @@ +import { Icon, Icons } from "@/components/Icon"; +import { BrandPill } from "@/components/layout/BrandPill"; + +interface VideoPlayerHeaderProps { + title: string; + onClick?: () => void; +} + +export function VideoPlayerHeader(props: VideoPlayerHeaderProps) { + return ( +
+
+

+ + + Back to home + + + {props.title} +

+
+ +
+ ); +} diff --git a/src/views/TestView.tsx b/src/views/TestView.tsx index 8c43c08b..5fe1db5c 100644 --- a/src/views/TestView.tsx +++ b/src/views/TestView.tsx @@ -5,13 +5,18 @@ import { useCallback, useState } from "react"; // test videos: https://gist.github.com/jsturgis/3b19447b304616f18657 // TODO video todos: -// - make pretty -// - improve seekables +// - improve seekables (if possible) // - error handling // - middle pause button +// - double click backdrop to toggle fullscreen +// - make volume bar collapse when hovering away from left control section +// - animate UI when showing/hiding +// - shortcuts when player is active +// - save volume in localstorage so persists between page reloads // - improve pausing while seeking/buffering +// - volume control flashes old value when updating +// - progress control flashes old value when updating // - captions -// - backdrop better click handling // - IOS support: (no volume, fullscreen video element instead of wrapper) // - IpadOS support: (fullscreen video wrapper should work, see (lookmovie.io) ) // - HLS support: feature detection otherwise use HLS.js From 2d9b66d9b8816a453bf21006969009a015a980c3 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Tue, 10 Jan 2023 01:01:51 +0100 Subject: [PATCH 018/135] fullscreen on iphone/ipad --- package.json | 2 ++ .../video/controls/FullscreenControl.tsx | 3 +-- src/components/video/hooks/controlVideo.ts | 17 +++++++++++++---- src/components/video/hooks/fullscreen.ts | 6 ++++++ src/components/video/hooks/useVideoPlayer.ts | 5 +++-- src/views/TestView.tsx | 3 +-- yarn.lock | 10 ++++++++++ 7 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 src/components/video/hooks/fullscreen.ts diff --git a/package.json b/package.json index 7a1256a4..f6678488 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "dependencies": { "@headlessui/react": "^1.5.0", "crypto-js": "^4.1.1", + "fscreen": "^1.2.0", "fuse.js": "^6.4.6", "hls.js": "^1.0.7", "i18next": "^22.4.5", @@ -44,6 +45,7 @@ "devDependencies": { "@tailwindcss/line-clamp": "^0.4.2", "@types/crypto-js": "^4.1.1", + "@types/fscreen": "^1.0.1", "@types/node": "^17.0.15", "@types/react": "^17.0.39", "@types/react-dom": "^17.0.11", diff --git a/src/components/video/controls/FullscreenControl.tsx b/src/components/video/controls/FullscreenControl.tsx index 67b1e2e4..321d5665 100644 --- a/src/components/video/controls/FullscreenControl.tsx +++ b/src/components/video/controls/FullscreenControl.tsx @@ -2,8 +2,7 @@ import { Icons } from "@/components/Icon"; import { useCallback } from "react"; import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton"; import { useVideoPlayerState } from "../VideoContext"; - -const canFullscreen = document.fullscreenEnabled; +import { canFullscreen } from "../hooks/fullscreen"; interface Props { className?: string; diff --git a/src/components/video/hooks/controlVideo.ts b/src/components/video/hooks/controlVideo.ts index f4e2bdcb..33413cab 100644 --- a/src/components/video/hooks/controlVideo.ts +++ b/src/components/video/hooks/controlVideo.ts @@ -1,3 +1,6 @@ +import fscreen from "fscreen"; +import { canFullscreen, isSafari } from "./fullscreen"; + export interface PlayerControls { play(): void; pause(): void; @@ -28,12 +31,18 @@ export function populateControls( player.pause(); }, enterFullscreen() { - if (!document.fullscreenEnabled || document.fullscreenElement) return; - wrapper.requestFullscreen(); + if (!canFullscreen || fscreen.fullscreenElement) return; + if (fscreen.fullscreenEnabled) { + fscreen.requestFullscreen(wrapper); + return; + } + if (isSafari) { + (player as any).webkitEnterFullscreen(); + } }, exitFullscreen() { - if (!document.fullscreenElement) return; - document.exitFullscreen(); + if (!fscreen.fullscreenElement) return; + fscreen.exitFullscreen(); }, setTime(t) { // clamp time between 0 and max duration diff --git a/src/components/video/hooks/fullscreen.ts b/src/components/video/hooks/fullscreen.ts new file mode 100644 index 00000000..f5bd96ae --- /dev/null +++ b/src/components/video/hooks/fullscreen.ts @@ -0,0 +1,6 @@ +import fscreen from "fscreen"; + +export const isSafari = /^((?!chrome|android).)*safari/i.test( + navigator.userAgent +); +export const canFullscreen = fscreen.fullscreenEnabled || isSafari; diff --git a/src/components/video/hooks/useVideoPlayer.ts b/src/components/video/hooks/useVideoPlayer.ts index d3d2d95b..1dddf81a 100644 --- a/src/components/video/hooks/useVideoPlayer.ts +++ b/src/components/video/hooks/useVideoPlayer.ts @@ -1,3 +1,4 @@ +import fscreen from "fscreen"; import React, { MutableRefObject, useEffect, useState } from "react"; import { initialControls, @@ -108,7 +109,7 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { player.addEventListener("playing", playing); player.addEventListener("seeking", seeking); player.addEventListener("seeked", seeked); - document.addEventListener("fullscreenchange", fullscreenchange); + fscreen.addEventListener("fullscreenchange", fullscreenchange); player.addEventListener("timeupdate", timeupdate); player.addEventListener("loadedmetadata", loadedmetadata); player.addEventListener("volumechange", volumechange); @@ -120,7 +121,7 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { player.removeEventListener("playing", playing); player.removeEventListener("seeking", seeking); player.removeEventListener("seeked", seeked); - document.removeEventListener("fullscreenchange", fullscreenchange); + fscreen.removeEventListener("fullscreenchange", fullscreenchange); player.removeEventListener("timeupdate", timeupdate); player.removeEventListener("loadedmetadata", loadedmetadata); player.removeEventListener("volumechange", volumechange); diff --git a/src/views/TestView.tsx b/src/views/TestView.tsx index 5fe1db5c..503e9e3b 100644 --- a/src/views/TestView.tsx +++ b/src/views/TestView.tsx @@ -17,8 +17,7 @@ import { useCallback, useState } from "react"; // - volume control flashes old value when updating // - progress control flashes old value when updating // - captions -// - IOS support: (no volume, fullscreen video element instead of wrapper) -// - IpadOS support: (fullscreen video wrapper should work, see (lookmovie.io) ) +// - IOS & IpadOS support: (no volume) // - HLS support: feature detection otherwise use HLS.js export function TestView() { const [show, setShow] = useState(true); diff --git a/yarn.lock b/yarn.lock index a6a82c6c..9f0c1b5d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -267,6 +267,11 @@ "resolved" "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.1.tgz" "version" "4.1.1" +"@types/fscreen@^1.0.1": + "integrity" "sha512-hV2d0BreihMGtrg+EdAFOIl/O2EL5vhAheHJUztGE/lPFZIN8ZCpGFL8hCbtyi1CfhKjDRCf47sHjP+FwJ4q0Q==" + "resolved" "https://registry.npmjs.org/@types/fscreen/-/fscreen-1.0.1.tgz" + "version" "1.0.1" + "@types/history@^4.7.11": "integrity" "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==" "resolved" "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz" @@ -1440,6 +1445,11 @@ "resolved" "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" "version" "1.0.0" +"fscreen@^1.2.0": + "integrity" "sha512-hlq4+BU0hlPmwsFjwGGzZ+OZ9N/wq9Ljg/sq3pX+2CD7hrJsX9tJgWWK/wiNTFM212CLHWhicOoqwXyZGGetJg==" + "resolved" "https://registry.npmjs.org/fscreen/-/fscreen-1.2.0.tgz" + "version" "1.2.0" + "function-bind@^1.1.1": "integrity" "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" "resolved" "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" From 02ef6c5bf16eac80f2fc713d9a143c5ba26dcc8e Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Tue, 10 Jan 2023 01:03:51 +0100 Subject: [PATCH 019/135] add todo --- src/views/TestView.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/views/TestView.tsx b/src/views/TestView.tsx index 503e9e3b..a1820f34 100644 --- a/src/views/TestView.tsx +++ b/src/views/TestView.tsx @@ -7,6 +7,7 @@ import { useCallback, useState } from "react"; // TODO video todos: // - improve seekables (if possible) // - error handling +// - buffering // - middle pause button // - double click backdrop to toggle fullscreen // - make volume bar collapse when hovering away from left control section From 35c7ac4b8d5e7a8a3d5d5241213258e36d5144bd Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Tue, 10 Jan 2023 19:53:55 +0100 Subject: [PATCH 020/135] lots of UI changes for video player --- package.json | 4 + src/components/Icon.tsx | 6 +- src/components/layout/Spinner.css | 19 ++++ src/components/layout/Spinner.tsx | 5 + src/components/video/DecoratedVideoPlayer.tsx | 99 ++++++++++++++++--- src/components/video/VideoContext.tsx | 11 ++- src/components/video/VideoPlayer.tsx | 18 +++- .../video/controls/BackdropControl.tsx | 20 +++- .../video/controls/FullscreenControl.tsx | 4 +- .../video/controls/LoadingControl.tsx | 3 +- .../video/controls/MiddlePauseControl.tsx | 27 +++++ .../video/controls/ProgressControl.tsx | 8 +- .../controls/ProgressListenerControl.tsx | 39 ++++++++ .../video/controls/SourceControl.tsx | 4 +- src/components/video/controls/TimeControl.tsx | 16 ++- .../video/controls/VolumeControl.tsx | 15 +-- src/components/video/hooks/controlVideo.ts | 91 +++++++++++++++-- src/components/video/hooks/fullscreen.ts | 6 -- src/components/video/hooks/useVideoPlayer.ts | 50 +++++++--- src/components/video/hooks/volumeStore.ts | 25 +++++ src/hooks/useProgressBar.ts | 4 +- src/utils/detectFeatures.ts | 40 ++++++++ src/views/TestView.tsx | 31 +++--- yarn.lock | 63 +++++++++++- 24 files changed, 516 insertions(+), 92 deletions(-) create mode 100644 src/components/layout/Spinner.css create mode 100644 src/components/layout/Spinner.tsx create mode 100644 src/components/video/controls/MiddlePauseControl.tsx create mode 100644 src/components/video/controls/ProgressListenerControl.tsx delete mode 100644 src/components/video/hooks/fullscreen.ts create mode 100644 src/components/video/hooks/volumeStore.ts create mode 100644 src/utils/detectFeatures.ts diff --git a/package.json b/package.json index f6678488..02c93cc2 100644 --- a/package.json +++ b/package.json @@ -13,12 +13,14 @@ "i18next-browser-languagedetector": "^7.0.1", "i18next-http-backend": "^2.1.0", "json5": "^2.2.0", + "lodash.throttle": "^4.1.1", "nanoid": "^4.0.0", "react": "^17.0.2", "react-dom": "^17.0.2", "react-i18next": "^12.1.1", "react-router-dom": "^5.2.0", "react-stickynode": "^4.1.0", + "react-transition-group": "^4.4.5", "srt-webvtt": "^2.0.0", "unpacker": "^1.0.1" }, @@ -46,12 +48,14 @@ "@tailwindcss/line-clamp": "^0.4.2", "@types/crypto-js": "^4.1.1", "@types/fscreen": "^1.0.1", + "@types/lodash.throttle": "^4.1.7", "@types/node": "^17.0.15", "@types/react": "^17.0.39", "@types/react-dom": "^17.0.11", "@types/react-router": "^5.1.18", "@types/react-router-dom": "^5.3.3", "@types/react-stickynode": "^4.0.0", + "@types/react-transition-group": "^4.4.5", "@typescript-eslint/eslint-plugin": "^5.13.0", "@typescript-eslint/parser": "^5.13.0", "@vitejs/plugin-react-swc": "^3.0.0", diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 45ea593b..18398ace 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -1,3 +1,5 @@ +import { memo } from "react"; + export enum Icons { SEARCH = "search", BOOKMARK = "bookmark", @@ -51,11 +53,11 @@ const iconList: Record = { volume_x: ``, }; -export function Icon(props: IconProps) { +export const Icon = memo((props: IconProps) => { return ( ); -} +}); diff --git a/src/components/layout/Spinner.css b/src/components/layout/Spinner.css new file mode 100644 index 00000000..0ec7f274 --- /dev/null +++ b/src/components/layout/Spinner.css @@ -0,0 +1,19 @@ +.spinner { + width: 48px; + height: 48px; + border: 5px solid white; + border-bottom-color: transparent; + border-radius: 50%; + display: inline-block; + box-sizing: border-box; + animation: spinner-rotation 800ms linear infinite; +} + +@keyframes spinner-rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/src/components/layout/Spinner.tsx b/src/components/layout/Spinner.tsx new file mode 100644 index 00000000..98dae6e3 --- /dev/null +++ b/src/components/layout/Spinner.tsx @@ -0,0 +1,5 @@ +import "./Spinner.css"; + +export function Spinner() { + return
; +} diff --git a/src/components/video/DecoratedVideoPlayer.tsx b/src/components/video/DecoratedVideoPlayer.tsx index 01891a26..222fd920 100644 --- a/src/components/video/DecoratedVideoPlayer.tsx +++ b/src/components/video/DecoratedVideoPlayer.tsx @@ -1,35 +1,106 @@ +import { useCallback, useRef, useState } from "react"; +import { CSSTransition } from "react-transition-group"; import { BackdropControl } from "./controls/BackdropControl"; import { FullscreenControl } from "./controls/FullscreenControl"; import { LoadingControl } from "./controls/LoadingControl"; +import { MiddlePauseControl } from "./controls/MiddlePauseControl"; import { PauseControl } from "./controls/PauseControl"; import { ProgressControl } from "./controls/ProgressControl"; import { TimeControl } from "./controls/TimeControl"; import { VolumeControl } from "./controls/VolumeControl"; import { VideoPlayerHeader } from "./parts/VideoPlayerHeader"; +import { useVideoPlayerState } from "./VideoContext"; import { VideoPlayer, VideoPlayerProps } from "./VideoPlayer"; -// TODO animate items away when hidden +function LeftSideControls() { + const { videoState } = useVideoPlayerState(); + + const handleMouseEnter = useCallback(() => { + videoState.setLeftControlsHover(true); + }, [videoState]); + const handleMouseLeave = useCallback(() => { + videoState.setLeftControlsHover(false); + }, [videoState]); + + return ( +
+ + + +
+ ); +} export function DecoratedVideoPlayer(props: VideoPlayerProps) { + const top = useRef(null); + const bottom = useRef(null); + const [show, setShow] = useState(false); + + const onBackdropChange = useCallback( + (showing: boolean) => { + setShow(showing); + }, + [setShow] + ); + return ( - +
-
- -
- - - -
- -
-
-
- +
+
+ +
+ +
+ +
+ +
+
+ + +
+ +
+
{props.children} diff --git a/src/components/video/VideoContext.tsx b/src/components/video/VideoContext.tsx index bf8d4e95..cbee333b 100644 --- a/src/components/video/VideoContext.tsx +++ b/src/components/video/VideoContext.tsx @@ -7,24 +7,26 @@ import React, { } from "react"; import { initialPlayerState, - PlayerState, + PlayerContext, useVideoPlayer, } from "./hooks/useVideoPlayer"; interface VideoPlayerContextType { source: string | null; - state: PlayerState; + sourceType: "m3u8" | "mp4"; + state: PlayerContext; } const initial: VideoPlayerContextType = { source: null, + sourceType: "mp4", state: initialPlayerState, }; type VideoPlayerContextAction = - | { type: "SET_SOURCE"; url: string } + | { type: "SET_SOURCE"; url: string; sourceType: "m3u8" | "mp4" } | { type: "UPDATE_PLAYER"; - state: PlayerState; + state: PlayerContext; }; function videoPlayerContextReducer( @@ -34,6 +36,7 @@ function videoPlayerContextReducer( const video = { ...original }; if (action.type === "SET_SOURCE") { video.source = action.url; + video.sourceType = action.sourceType; return video; } if (action.type === "UPDATE_PLAYER") { diff --git a/src/components/video/VideoPlayer.tsx b/src/components/video/VideoPlayer.tsx index 89f632e4..0f914693 100644 --- a/src/components/video/VideoPlayer.tsx +++ b/src/components/video/VideoPlayer.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useContext, useRef } from "react"; +import { forwardRef, useContext, useEffect, useRef } from "react"; import { VideoPlayerContext, VideoPlayerContextProvider } from "./VideoContext"; export interface VideoPlayerProps { @@ -11,16 +11,24 @@ const VideoPlayerInternals = forwardRef< { autoPlay: boolean } >((props, ref) => { const video = useContext(VideoPlayerContext); + const didInitialize = useRef(null); + useEffect(() => { + if (didInitialize.current) return; + if (!video.state.hasInitialized || !video.source) return; + video.state.initPlayer(video.source, video.sourceType); + didInitialize.current = true; + }, [didInitialize, video]); + + // muted attribute is required for safari, as they cant change the volume itself return ( + /> ); }); @@ -31,7 +39,7 @@ export function VideoPlayer(props: VideoPlayerProps) { return (
void; } -// TODO add double click to toggle fullscreen - export function BackdropControl(props: BackdropControlProps) { const { videoState } = useVideoPlayerState(); const [moved, setMoved] = useState(false); @@ -35,7 +34,19 @@ export function BackdropControl(props: BackdropControlProps) { }, [videoState, clickareaRef] ); + const handleDoubleClick = useCallback( + (e: React.MouseEvent) => { + if (!clickareaRef.current || clickareaRef.current !== e.target) return; + + if (!videoState.isFullscreen) videoState.enterFullscreen(); + else videoState.exitFullscreen(); + }, + [videoState, clickareaRef] + ); + useEffect(() => { + props.onBackdropChange?.(moved || videoState.isPaused); + }, [videoState, moved, props]); const showUI = moved || videoState.isPaused; return ( @@ -45,6 +56,7 @@ export function BackdropControl(props: BackdropControlProps) { onMouseLeave={handleMouseLeave} ref={clickareaRef} onClick={handleClick} + onDoubleClick={handleDoubleClick} >
- {showUI ? props.children : null} + {props.children}
); diff --git a/src/components/video/controls/FullscreenControl.tsx b/src/components/video/controls/FullscreenControl.tsx index 321d5665..9d44264a 100644 --- a/src/components/video/controls/FullscreenControl.tsx +++ b/src/components/video/controls/FullscreenControl.tsx @@ -1,8 +1,8 @@ import { Icons } from "@/components/Icon"; +import { canFullscreen } from "@/utils/detectFeatures"; import { useCallback } from "react"; import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton"; import { useVideoPlayerState } from "../VideoContext"; -import { canFullscreen } from "../hooks/fullscreen"; interface Props { className?: string; @@ -16,7 +16,7 @@ export function FullscreenControl(props: Props) { else videoState.enterFullscreen(); }, [videoState]); - if (!canFullscreen) return null; + if (!canFullscreen()) return null; return ( Loading...

; + return ; } diff --git a/src/components/video/controls/MiddlePauseControl.tsx b/src/components/video/controls/MiddlePauseControl.tsx new file mode 100644 index 00000000..9bbbe08c --- /dev/null +++ b/src/components/video/controls/MiddlePauseControl.tsx @@ -0,0 +1,27 @@ +import { Icon, Icons } from "@/components/Icon"; +import { useCallback } from "react"; +import { useVideoPlayerState } from "../VideoContext"; + +export function MiddlePauseControl() { + const { videoState } = useVideoPlayerState(); + + const handleClick = useCallback(() => { + if (videoState?.isPlaying) videoState.pause(); + else videoState.play(); + }, [videoState]); + + if (videoState.hasPlayedOnce) return null; + if (videoState.isPlaying) return null; + + return ( +
+ +
+ ); +} diff --git a/src/components/video/controls/ProgressControl.tsx b/src/components/video/controls/ProgressControl.tsx index ec1cae1d..b8e277a0 100644 --- a/src/components/video/controls/ProgressControl.tsx +++ b/src/components/video/controls/ProgressControl.tsx @@ -3,12 +3,13 @@ import { makePercentageString, useProgressBar, } from "@/hooks/useProgressBar"; -import { useCallback, useRef } from "react"; +import { useCallback, useEffect, useRef } from "react"; import { useVideoPlayerState } from "../VideoContext"; export function ProgressControl() { const { videoState } = useVideoPlayerState(); const ref = useRef(null); + const dragRef = useRef(false); const commitTime = useCallback( (percentage) => { @@ -20,6 +21,11 @@ export function ProgressControl() { ref, commitTime ); + useEffect(() => { + if (dragRef.current === dragging) return; + dragRef.current = dragging; + videoState.setSeeking(dragging); + }, [dragRef, dragging, videoState]); let watchProgress = makePercentageString( makePercentage((videoState.time / videoState.duration) * 100) diff --git a/src/components/video/controls/ProgressListenerControl.tsx b/src/components/video/controls/ProgressListenerControl.tsx new file mode 100644 index 00000000..bdcc8f07 --- /dev/null +++ b/src/components/video/controls/ProgressListenerControl.tsx @@ -0,0 +1,39 @@ +import { useEffect, useMemo, useRef } from "react"; +import throttle from "lodash.throttle"; +import { useVideoPlayerState } from "../VideoContext"; + +interface Props { + startAt?: number; + onProgress?: (time: number, duration: number) => void; +} + +export function ProgressListenerControl(props: Props) { + const { videoState } = useVideoPlayerState(); + const didInitialize = useRef(null); + + // time updates (throttled) + const updateTime = useMemo( + () => throttle((a: number, b: number) => props.onProgress?.(a, b), 1000), + [props] + ); + useEffect(() => { + if (!videoState.isPlaying) return; + if (videoState.duration === 0 || videoState.time === 0) return; + updateTime(videoState.time, videoState.duration); + }, [videoState, updateTime]); + useEffect(() => { + return () => { + updateTime.cancel(); + }; + }, [updateTime]); + + // initialize + useEffect(() => { + if (didInitialize.current) return; + if (!videoState.hasInitialized || Number.isNaN(videoState.duration)) return; + if (props.startAt !== undefined) videoState.setTime(props.startAt); + didInitialize.current = true; + }, [didInitialize, videoState, props]); + + return null; +} diff --git a/src/components/video/controls/SourceControl.tsx b/src/components/video/controls/SourceControl.tsx index e7ad0c9d..ddb13984 100644 --- a/src/components/video/controls/SourceControl.tsx +++ b/src/components/video/controls/SourceControl.tsx @@ -3,6 +3,7 @@ import { VideoPlayerDispatchContext } from "../VideoContext"; interface SourceControlProps { source: string; + type: "m3u8" | "mp4"; } export function SourceControl(props: SourceControlProps) { @@ -12,8 +13,9 @@ export function SourceControl(props: SourceControlProps) { dispatch({ type: "SET_SOURCE", url: props.source, + sourceType: props.type, }); - }, [props.source, dispatch]); + }, [props, dispatch]); return null; } diff --git a/src/components/video/controls/TimeControl.tsx b/src/components/video/controls/TimeControl.tsx index 35d86351..aeeb99d4 100644 --- a/src/components/video/controls/TimeControl.tsx +++ b/src/components/video/controls/TimeControl.tsx @@ -5,6 +5,11 @@ function durationExceedsHour(secs: number): boolean { } function formatSeconds(secs: number, showHours = false): string { + if (Number.isNaN(secs)) { + if (showHours) return "0:00:00"; + return "0:00"; + } + let time = secs; const seconds = time % 60; @@ -14,12 +19,13 @@ function formatSeconds(secs: number, showHours = false): string { time /= 60; const hours = minutes % 60; - const minuteString = `${Math.round(minutes) + if (!showHours) + return `${Math.round(minutes).toString()}:${Math.round(seconds) + .toString() + .padStart(2, "0")}`; + return `${Math.round(hours).toString()}:${Math.round(minutes) .toString() - .padStart(2)}:${Math.round(seconds).toString().padStart(2, "0")}`; - - if (!showHours) return minuteString; - return `${Math.round(hours).toString()}:${minuteString}`; + .padStart(2, "0")}:${Math.round(seconds).toString().padStart(2, "0")}`; } interface Props { diff --git a/src/components/video/controls/VolumeControl.tsx b/src/components/video/controls/VolumeControl.tsx index 72f2a196..cfad9441 100644 --- a/src/components/video/controls/VolumeControl.tsx +++ b/src/components/video/controls/VolumeControl.tsx @@ -4,15 +4,14 @@ import { makePercentageString, useProgressBar, } from "@/hooks/useProgressBar"; -import { useCallback, useRef, useState } from "react"; +import { canChangeVolume } from "@/utils/detectFeatures"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useVideoPlayerState } from "../VideoContext"; interface Props { className?: string; } -// TODO make hoveredOnce false when control bar appears - export function VolumeControl(props: Props) { const { videoState } = useVideoPlayerState(); const ref = useRef(null); @@ -32,6 +31,10 @@ export function VolumeControl(props: Props) { true ); + useEffect(() => { + if (!videoState.leftControlHovering) setHoveredOnce(false); + }, [videoState, setHoveredOnce]); + const handleClick = useCallback(() => { if (videoState.volume > 0) { videoState.setVolume(0); @@ -41,8 +44,8 @@ export function VolumeControl(props: Props) { } }, [videoState, setStoredVolume, storedVolume]); - const handleMouseEnter = useCallback(() => { - setHoveredOnce(true); + const handleMouseEnter = useCallback(async () => { + if (await canChangeVolume()) setHoveredOnce(true); }, [setHoveredOnce]); let percentage = makePercentage(videoState.volume * 100); @@ -59,7 +62,7 @@ export function VolumeControl(props: Props) { 0 ? Icons.VOLUME : Icons.VOLUME_X} />
diff --git a/src/components/video/hooks/controlVideo.ts b/src/components/video/hooks/controlVideo.ts index 33413cab..e5c7c8bc 100644 --- a/src/components/video/hooks/controlVideo.ts +++ b/src/components/video/hooks/controlVideo.ts @@ -1,5 +1,14 @@ +import Hls from "hls.js"; +import { + canChangeVolume, + canFullscreen, + canFullscreenAnyElement, + canWebkitFullscreen, +} from "@/utils/detectFeatures"; import fscreen from "fscreen"; -import { canFullscreen, isSafari } from "./fullscreen"; +import React, { RefObject } from "react"; +import { PlayerState } from "./useVideoPlayer"; +import { getStoredVolume, setStoredVolume } from "./volumeStore"; export interface PlayerControls { play(): void; @@ -8,6 +17,9 @@ export interface PlayerControls { enterFullscreen(): void; setTime(time: number): void; setVolume(volume: number): void; + setSeeking(active: boolean): void; + setLeftControlsHover(hovering: boolean): void; + initPlayer(sourceUrl: string, sourceType: "m3u8" | "mp4"): void; } export const initialControls: PlayerControls = { @@ -17,12 +29,20 @@ export const initialControls: PlayerControls = { exitFullscreen: () => null, setTime: () => null, setVolume: () => null, + setSeeking: () => null, + setLeftControlsHover: () => null, + initPlayer: () => null, }; export function populateControls( - player: HTMLVideoElement, - wrapper: HTMLDivElement + playerEl: HTMLVideoElement, + wrapperEl: HTMLDivElement, + update: (s: React.SetStateAction) => void, + state: RefObject ): PlayerControls { + const player = playerEl; + const wrapper = wrapperEl; + return { play() { player.play(); @@ -31,12 +51,12 @@ export function populateControls( player.pause(); }, enterFullscreen() { - if (!canFullscreen || fscreen.fullscreenElement) return; - if (fscreen.fullscreenEnabled) { + if (!canFullscreen() || fscreen.fullscreenElement) return; + if (canFullscreenAnyElement()) { fscreen.requestFullscreen(wrapper); return; } - if (isSafari) { + if (canWebkitFullscreen()) { (player as any).webkitEnterFullscreen(); } }, @@ -48,15 +68,66 @@ export function populateControls( // clamp time between 0 and max duration let time = Math.min(t, player.duration); time = Math.max(0, time); - // eslint-disable-next-line no-param-reassign + + if (Number.isNaN(time)) return; + + // update state player.currentTime = time; + update((s) => ({ ...s, time })); }, - setVolume(v) { + async setVolume(v) { // clamp time between 0 and 1 let volume = Math.min(v, 1); volume = Math.max(0, volume); - // eslint-disable-next-line no-param-reassign - player.volume = volume; + + // update state + if (await canChangeVolume()) player.volume = volume; + update((s) => ({ ...s, volume })); + + // update localstorage + setStoredVolume(volume); + }, + setSeeking(active) { + const currentState = state.current; + if (!currentState) return; + + // if it was playing when starting to seek, play again + if (!active) { + if (!currentState.pausedWhenSeeking) this.play(); + return; + } + + // when seeking we pause the video + update((s) => ({ ...s, pausedWhenSeeking: s.isPaused })); + this.pause(); + }, + setLeftControlsHover(hovering) { + update((s) => ({ ...s, leftControlHovering: hovering })); + }, + initPlayer(sourceUrl: string, sourceType: "m3u8" | "mp4") { + this.setVolume(getStoredVolume()); + + if (sourceType === "m3u8") { + if (player.canPlayType("application/vnd.apple.mpegurl")) { + player.src = sourceUrl; + } else { + // HLS support + if (!Hls.isSupported()) throw new Error("HLS not supported"); // TODO handle errors + + const hls = new Hls(); + + hls.on(Hls.Events.ERROR, (event, data) => { + // eslint-disable-next-line no-alert + if (data.fatal) alert("HLS fatal error"); + console.error("HLS error", data); // TODO handle errors + }); + + hls.attachMedia(player); + hls.loadSource(sourceUrl); + } + } else if (sourceType === "mp4") { + player.src = sourceUrl; + } }, }; } diff --git a/src/components/video/hooks/fullscreen.ts b/src/components/video/hooks/fullscreen.ts deleted file mode 100644 index f5bd96ae..00000000 --- a/src/components/video/hooks/fullscreen.ts +++ /dev/null @@ -1,6 +0,0 @@ -import fscreen from "fscreen"; - -export const isSafari = /^((?!chrome|android).)*safari/i.test( - navigator.userAgent -); -export const canFullscreen = fscreen.fullscreenEnabled || isSafari; diff --git a/src/components/video/hooks/useVideoPlayer.ts b/src/components/video/hooks/useVideoPlayer.ts index 1dddf81a..ae2a199d 100644 --- a/src/components/video/hooks/useVideoPlayer.ts +++ b/src/components/video/hooks/useVideoPlayer.ts @@ -1,5 +1,6 @@ +import { canChangeVolume } from "@/utils/detectFeatures"; import fscreen from "fscreen"; -import React, { MutableRefObject, useEffect, useState } from "react"; +import React, { MutableRefObject, useEffect, useRef, useState } from "react"; import { initialControls, PlayerControls, @@ -17,9 +18,15 @@ export type PlayerState = { duration: number; volume: number; buffered: number; -} & PlayerControls; + pausedWhenSeeking: boolean; + hasInitialized: boolean; + leftControlHovering: boolean; + hasPlayedOnce: boolean; +}; + +export type PlayerContext = PlayerState & PlayerControls; -export const initialPlayerState: PlayerState = { +export const initialPlayerState: PlayerContext = { isPlaying: false, isPaused: true, isFullscreen: false, @@ -29,10 +36,14 @@ export const initialPlayerState: PlayerState = { duration: 0, volume: 0, buffered: 0, + pausedWhenSeeking: false, + hasInitialized: false, + leftControlHovering: false, + hasPlayedOnce: false, ...initialControls, }; -type SetPlayer = (s: React.SetStateAction) => void; +type SetPlayer = (s: React.SetStateAction) => void; function readState(player: HTMLVideoElement, update: SetPlayer) { const state = { @@ -47,8 +58,13 @@ function readState(player: HTMLVideoElement, update: SetPlayer) { state.volume = player.volume; state.buffered = handleBuffered(player.currentTime, player.buffered); state.isLoading = false; + state.hasInitialized = true; - update(state); + update((s) => ({ + ...state, + pausedWhenSeeking: s.pausedWhenSeeking, + hasPlayedOnce: s.hasPlayedOnce, + })); } function registerListeners(player: HTMLVideoElement, update: SetPlayer) { @@ -65,6 +81,7 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { isPaused: false, isPlaying: true, isLoading: false, + hasPlayedOnce: true, })); }; const seeking = () => { @@ -92,11 +109,12 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { duration: player.duration, })); }; - const volumechange = () => { - update((s) => ({ - ...s, - volume: player.volume, - })); + const volumechange = async () => { + if (await canChangeVolume()) + update((s) => ({ + ...s, + volume: player.volume, + })); }; const progress = () => { update((s) => ({ @@ -135,6 +153,7 @@ export function useVideoPlayer( wrapperRef: MutableRefObject ) { const [state, setState] = useState(initialPlayerState); + const stateRef = useRef(null); useEffect(() => { const player = ref.current; @@ -142,9 +161,16 @@ export function useVideoPlayer( if (player && wrapper) { readState(player, setState); registerListeners(player, setState); - setState((s) => ({ ...s, ...populateControls(player, wrapper) })); + setState((s) => ({ + ...s, + ...populateControls(player, wrapper, setState as any, stateRef), + })); } - }, [ref, wrapperRef]); + }, [ref, wrapperRef, stateRef]); + + useEffect(() => { + stateRef.current = state; + }, [state, stateRef]); return { playerState: state, diff --git a/src/components/video/hooks/volumeStore.ts b/src/components/video/hooks/volumeStore.ts new file mode 100644 index 00000000..3b328810 --- /dev/null +++ b/src/components/video/hooks/volumeStore.ts @@ -0,0 +1,25 @@ +import { versionedStoreBuilder } from "@/utils/storage"; + +export const volumeStore = versionedStoreBuilder() + .setKey("mw-volume") + .addVersion({ + version: 0, + create() { + return { + volume: 1, + }; + }, + }) + .build(); + +export function getStoredVolume(): number { + const store = volumeStore.get(); + return store.volume; +} + +export function setStoredVolume(volume: number) { + const store = volumeStore.get(); + store.save({ + volume, + }); +} diff --git a/src/hooks/useProgressBar.ts b/src/hooks/useProgressBar.ts index d0fee788..7bb7070b 100644 --- a/src/hooks/useProgressBar.ts +++ b/src/hooks/useProgressBar.ts @@ -20,8 +20,8 @@ export function useProgressBar( function mouseMove(ev: MouseEvent) { if (!mouseDown || !barRef.current) return; const rect = barRef.current.getBoundingClientRect(); - const pos = ((ev.pageX - rect.left) / barRef.current.offsetWidth) * 100; - setProgress(pos); + const pos = (ev.pageX - rect.left) / barRef.current.offsetWidth; + setProgress(pos * 100); if (commitImmediately) commit(pos); } diff --git a/src/utils/detectFeatures.ts b/src/utils/detectFeatures.ts new file mode 100644 index 00000000..15be4c69 --- /dev/null +++ b/src/utils/detectFeatures.ts @@ -0,0 +1,40 @@ +import fscreen from "fscreen"; + +export const isSafari = /^((?!chrome|android).)*safari/i.test( + navigator.userAgent +); + +let cachedVolumeResult: boolean | null = null; +export async function canChangeVolume(): Promise { + if (cachedVolumeResult === null) { + const timeoutPromise = new Promise((resolve) => { + setTimeout(() => resolve(false), 1e3); + }); + const promise = new Promise((resolve) => { + const video = document.createElement("video"); + const handler = () => { + video.removeEventListener("volumechange", handler); + resolve(true); + }; + + video.addEventListener("volumechange", handler); + + video.volume = 0.5; + }); + + cachedVolumeResult = await Promise.race([promise, timeoutPromise]); + } + return cachedVolumeResult; +} + +export function canFullscreenAnyElement(): boolean { + return fscreen.fullscreenEnabled; +} + +export function canWebkitFullscreen(): boolean { + return canFullscreenAnyElement() || isSafari; +} + +export function canFullscreen(): boolean { + return canFullscreenAnyElement() || canWebkitFullscreen(); +} diff --git a/src/views/TestView.tsx b/src/views/TestView.tsx index a1820f34..5dd391f3 100644 --- a/src/views/TestView.tsx +++ b/src/views/TestView.tsx @@ -1,3 +1,4 @@ +import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl"; import { SourceControl } from "@/components/video/controls/SourceControl"; import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer"; import { useCallback, useState } from "react"; @@ -5,21 +6,16 @@ import { useCallback, useState } from "react"; // test videos: https://gist.github.com/jsturgis/3b19447b304616f18657 // TODO video todos: -// - improve seekables (if possible) // - error handling -// - buffering -// - middle pause button -// - double click backdrop to toggle fullscreen -// - make volume bar collapse when hovering away from left control section -// - animate UI when showing/hiding -// - shortcuts when player is active -// - save volume in localstorage so persists between page reloads -// - improve pausing while seeking/buffering -// - volume control flashes old value when updating -// - progress control flashes old value when updating // - captions -// - IOS & IpadOS support: (no volume) -// - HLS support: feature detection otherwise use HLS.js +// - mobile UI +// - safari fullscreen will make video overlap player controls +// - safari progress bar is fucked + +// TODO optional todos: +// - shortcuts when player is active +// - improve seekables (if possible) + export function TestView() { const [show, setShow] = useState(true); const handleClick = useCallback(() => { @@ -33,7 +29,14 @@ export function TestView() { return (
- + + console.log(a, b)} + />
); diff --git a/yarn.lock b/yarn.lock index 9f0c1b5d..e3e9dda4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,7 +10,7 @@ "core-js-pure" "^3.25.1" "regenerator-runtime" "^0.13.11" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.14.5", "@babel/runtime@^7.18.9", "@babel/runtime@^7.19.4", "@babel/runtime@^7.20.6": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.14.5", "@babel/runtime@^7.18.9", "@babel/runtime@^7.19.4", "@babel/runtime@^7.20.6", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": "integrity" "sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==" "resolved" "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz" "version" "7.20.6" @@ -287,6 +287,18 @@ "resolved" "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" "version" "0.0.29" +"@types/lodash.throttle@^4.1.7": + "integrity" "sha512-znwGDpjCHQ4FpLLx19w4OXDqq8+OvREa05H89obtSyXyOFKL3dDjCslsmfBz0T2FU8dmf5Wx1QvogbINiGIu9g==" + "resolved" "https://registry.npmjs.org/@types/lodash.throttle/-/lodash.throttle-4.1.7.tgz" + "version" "4.1.7" + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + "integrity" "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==" + "resolved" "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz" + "version" "4.14.191" + "@types/node@^17.0.15", "@types/node@>= 14": "integrity" "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==" "resolved" "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz" @@ -328,6 +340,13 @@ dependencies: "@types/react" "*" +"@types/react-transition-group@^4.4.5": + "integrity" "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==" + "resolved" "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz" + "version" "4.4.5" + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@^17", "@types/react@^17.0.39": "integrity" "sha512-vwk8QqVODi0VaZZpDXQCmEmiOuyjEFPY7Ttaw5vjM112LOq37yz1CDJGrRJwA1fYEq4Iitd5rnjd1yWAc/bT+A==" "resolved" "https://registry.npmjs.org/@types/react/-/react-17.0.52.tgz" @@ -998,6 +1017,14 @@ dependencies: "esutils" "^2.0.2" +"dom-helpers@^5.0.1": + "integrity" "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==" + "resolved" "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz" + "version" "5.2.1" + dependencies: + "@babel/runtime" "^7.8.7" + "csstype" "^3.0.2" + "electron-to-chromium@^1.4.251": "integrity" "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==" "resolved" "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz" @@ -1011,6 +1038,13 @@ "resolved" "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" "version" "9.2.2" +"encoding@^0.1.0": + "integrity" "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==" + "resolved" "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz" + "version" "0.1.13" + dependencies: + "iconv-lite" "^0.6.2" + "encoding@^0.1.13": "version" "0.1.13" dependencies: @@ -1725,6 +1759,8 @@ "@babel/runtime" "^7.20.6" "iconv-lite@^0.6.2": + "integrity" "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==" + "resolved" "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" "version" "0.6.3" dependencies: "safer-buffer" ">= 2.1.2 < 3.0.0" @@ -2123,6 +2159,11 @@ "resolved" "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" "version" "4.6.2" +"lodash.throttle@^4.1.1": + "integrity" "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" + "resolved" "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz" + "version" "4.1.1" + "lodash@^4.17.15": "integrity" "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" "resolved" "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" @@ -2850,7 +2891,7 @@ dependencies: "performance-now" "^2.1.0" -"react-dom@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^16 || ^17 || ^18", "react-dom@^17.0.2": +"react-dom@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^16 || ^17 || ^18", "react-dom@^17.0.2", "react-dom@>=16.6.0": "integrity" "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==" "resolved" "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz" "version" "17.0.2" @@ -2911,7 +2952,17 @@ "shallowequal" "^1.0.0" "subscribe-ui-event" "^2.0.6" -"react@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16 || ^17 || ^18", "react@^17.0.2", "react@>= 16.8.0", "react@>=15", "react@17.0.2": +"react-transition-group@^4.4.5": + "integrity" "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==" + "resolved" "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz" + "version" "4.4.5" + dependencies: + "@babel/runtime" "^7.5.5" + "dom-helpers" "^5.0.1" + "loose-envify" "^1.4.0" + "prop-types" "^15.6.2" + +"react@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16 || ^17 || ^18", "react@^17.0.2", "react@>= 16.8.0", "react@>=15", "react@>=16.6.0", "react@17.0.2": "integrity" "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==" "resolved" "https://registry.npmjs.org/react/-/react-17.0.2.tgz" "version" "17.0.2" @@ -3047,6 +3098,8 @@ "queue-microtask" "^1.2.2" "safe-buffer@~5.2.0": + "integrity" "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + "resolved" "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" "version" "5.2.1" "safe-regex-test@^1.0.0": @@ -3059,6 +3112,8 @@ "is-regex" "^1.1.4" "safer-buffer@>= 2.1.2 < 3.0.0": + "integrity" "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "resolved" "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" "version" "2.1.2" "scheduler@^0.20.2": @@ -3173,6 +3228,8 @@ "minipass" "^3.1.1" "string_decoder@^1.1.1": + "integrity" "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==" + "resolved" "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" "version" "1.3.0" dependencies: "safe-buffer" "~5.2.0" From d28e6e6735413c541c7d5bd21002b829384c6d66 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Tue, 10 Jan 2023 21:18:10 +0100 Subject: [PATCH 021/135] implement video player on mediapage --- src/components/media/VideoPlayer.tsx | 109 ------------------ src/components/video/DecoratedVideoPlayer.tsx | 11 +- src/components/video/controls/TimeControl.tsx | 2 +- .../video/parts/VideoPlayerHeader.tsx | 27 +++-- src/views/MediaView.tsx | 47 +++++--- src/views/TestView.tsx | 8 +- 6 files changed, 62 insertions(+), 142 deletions(-) delete mode 100644 src/components/media/VideoPlayer.tsx diff --git a/src/components/media/VideoPlayer.tsx b/src/components/media/VideoPlayer.tsx deleted file mode 100644 index 0009922e..00000000 --- a/src/components/media/VideoPlayer.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { ReactElement, useEffect, useRef, useState } from "react"; -import Hls from "hls.js"; -import { IconPatch } from "@/components/buttons/IconPatch"; -import { Icons } from "@/components/Icon"; -import { Loading } from "@/components/layout/Loading"; -import { MWMediaCaption, MWMediaStream } from "@/providers"; - -export interface VideoPlayerProps { - source: MWMediaStream; - captions: MWMediaCaption[]; - startAt?: number; - onProgress?: (event: ProgressEvent) => void; -} - -export function SkeletonVideoPlayer(props: { error?: boolean }) { - return ( -
- {props.error ? ( -
- -

Couldn't get your stream

-
- ) : ( -
- -

Getting your stream...

-
- )} -
- ); -} - -export function VideoPlayer(props: VideoPlayerProps) { - const videoRef = useRef(null); - const [hasErrored, setErrored] = useState(false); - const [isLoading, setLoading] = useState(true); - const showVideo = !isLoading && !hasErrored; - const mustUseHls = props.source.type === "m3u8"; - - // reset if stream url changes - useEffect(() => { - setLoading(true); - setErrored(false); - - // hls support - if (mustUseHls) { - if (!videoRef.current) return; - - if (!Hls.isSupported()) { - setLoading(false); - setErrored(true); - return; - } - - const hls = new Hls(); - - if (videoRef.current.canPlayType("application/vnd.apple.mpegurl")) { - videoRef.current.src = props.source.url; - return; - } - - hls.attachMedia(videoRef.current); - hls.loadSource(props.source.url); - - hls.on(Hls.Events.ERROR, (event, data) => { - setErrored(true); - console.error(data); - }); - } - }, [props.source.url, videoRef, mustUseHls]); - - let skeletonUi: null | ReactElement = null; - if (hasErrored) { - skeletonUi = ; - } else if (isLoading) { - skeletonUi = ; - } - - return ( - <> - {skeletonUi} - - - ); -} diff --git a/src/components/video/DecoratedVideoPlayer.tsx b/src/components/video/DecoratedVideoPlayer.tsx index 222fd920..e1582d5c 100644 --- a/src/components/video/DecoratedVideoPlayer.tsx +++ b/src/components/video/DecoratedVideoPlayer.tsx @@ -12,6 +12,11 @@ import { VideoPlayerHeader } from "./parts/VideoPlayerHeader"; import { useVideoPlayerState } from "./VideoContext"; import { VideoPlayer, VideoPlayerProps } from "./VideoPlayer"; +interface DecoratedVideoPlayerProps { + title?: string; + onGoBack?: () => void; +} + function LeftSideControls() { const { videoState } = useVideoPlayerState(); @@ -35,7 +40,9 @@ function LeftSideControls() { ); } -export function DecoratedVideoPlayer(props: VideoPlayerProps) { +export function DecoratedVideoPlayer( + props: VideoPlayerProps & DecoratedVideoPlayerProps +) { const top = useRef(null); const bottom = useRef(null); const [show, setShow] = useState(false); @@ -98,7 +105,7 @@ export function DecoratedVideoPlayer(props: VideoPlayerProps) { ref={top} className="pointer-events-auto absolute inset-x-0 top-0 flex flex-col py-6 px-8 pb-2" > - +
diff --git a/src/components/video/controls/TimeControl.tsx b/src/components/video/controls/TimeControl.tsx index aeeb99d4..42e78329 100644 --- a/src/components/video/controls/TimeControl.tsx +++ b/src/components/video/controls/TimeControl.tsx @@ -17,7 +17,7 @@ function formatSeconds(secs: number, showHours = false): string { const minutes = time % 60; time /= 60; - const hours = minutes % 60; + const hours = time % 60; if (!showHours) return `${Math.round(minutes).toString()}:${Math.round(seconds) diff --git a/src/components/video/parts/VideoPlayerHeader.tsx b/src/components/video/parts/VideoPlayerHeader.tsx index d0cf55f2..83138b19 100644 --- a/src/components/video/parts/VideoPlayerHeader.tsx +++ b/src/components/video/parts/VideoPlayerHeader.tsx @@ -2,24 +2,31 @@ import { Icon, Icons } from "@/components/Icon"; import { BrandPill } from "@/components/layout/BrandPill"; interface VideoPlayerHeaderProps { - title: string; + title?: string; onClick?: () => void; } export function VideoPlayerHeader(props: VideoPlayerHeaderProps) { + const showDivider = props.title || props.onClick; return (

- - - Back to home - - - {props.title} + {props.onClick ? ( + + + Back to home + + ) : null} + {showDivider ? ( + + ) : null} + {props.title ? ( + {props.title} + ) : null}

diff --git a/src/views/MediaView.tsx b/src/views/MediaView.tsx index 88fe66df..b82332e1 100644 --- a/src/views/MediaView.tsx +++ b/src/views/MediaView.tsx @@ -1,4 +1,4 @@ -import { ReactElement, useEffect, useState } from "react"; +import { ReactElement, useCallback, useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { IconPatch } from "@/components/buttons/IconPatch"; @@ -6,10 +6,8 @@ import { Icons } from "@/components/Icon"; import { Navigation } from "@/components/layout/Navigation"; import { Paper } from "@/components/layout/Paper"; import { LoadingSeasons, Seasons } from "@/components/layout/Seasons"; -import { - SkeletonVideoPlayer, - VideoPlayer, -} from "@/components/media/VideoPlayer"; +import { SkeletonVideoPlayer } from "@/components/media/VideoPlayer"; +import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer"; import { ArrowLink } from "@/components/text/ArrowLink"; import { DotList } from "@/components/text/DotList"; import { Title } from "@/components/text/Title"; @@ -30,6 +28,8 @@ import { useBookmarkContext, } from "@/state/bookmark"; import { getWatchedFromPortable, useWatchedContext } from "@/state/watched"; +import { SourceControl } from "@/components/video/controls/SourceControl"; +import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl"; import { NotFoundChecks } from "./notfound/NotFoundChecks"; interface StyledMediaViewProps { @@ -38,28 +38,37 @@ interface StyledMediaViewProps { } function StyledMediaView(props: StyledMediaViewProps) { + const reactHistory = useHistory(); const watchedStore = useWatchedContext(); const startAtTime: number | undefined = getWatchedFromPortable( watchedStore.watched.items, props.media )?.progress; - function updateProgress(e: Event) { - if (!props.media) return; - const el: HTMLVideoElement = e.currentTarget as HTMLVideoElement; - if (el.currentTime <= 30) { - return; // Don't update stored progress if less than 30s into the video - } - watchedStore.updateProgress(props.media, el.currentTime, el.duration); - } + const updateProgress = useCallback( + (time: number, duration: number) => { + // Don't update stored progress if less than 30s into the video + if (time <= 30) return; + watchedStore.updateProgress(props.media, time, duration); + }, + [props, watchedStore] + ); + + const goBack = useCallback(() => { + if (reactHistory.action !== "POP") reactHistory.goBack(); + else reactHistory.push("/"); + }, [reactHistory]); return ( - updateProgress(e)} - startAt={startAtTime} - /> +
+ + + + +
); } diff --git a/src/views/TestView.tsx b/src/views/TestView.tsx index 5dd391f3..eaac96b6 100644 --- a/src/views/TestView.tsx +++ b/src/views/TestView.tsx @@ -10,12 +10,18 @@ import { useCallback, useState } from "react"; // - captions // - mobile UI // - safari fullscreen will make video overlap player controls -// - safari progress bar is fucked +// - safari progress bar is fucked (video doesnt change time but video.currentTime does change) // TODO optional todos: // - shortcuts when player is active // - improve seekables (if possible) +// TODO stuff to test: +// - browser: firefox, chrome, edge, safari desktop +// - phones: android firefox, android chrome, iphone safari +// - devices: ipadOS +// - features: HLS, error handling + export function TestView() { const [show, setShow] = useState(true); const handleClick = useCallback(() => { From 46e933dfb734d80230fe6665ac9600bf28aa8944 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Tue, 10 Jan 2023 21:23:53 +0100 Subject: [PATCH 022/135] fix skeleton --- src/views/MediaView.tsx | 20 +++++++++++++++++++- src/views/TestView.tsx | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/views/MediaView.tsx b/src/views/MediaView.tsx index b82332e1..77d54d2c 100644 --- a/src/views/MediaView.tsx +++ b/src/views/MediaView.tsx @@ -6,7 +6,6 @@ import { Icons } from "@/components/Icon"; import { Navigation } from "@/components/layout/Navigation"; import { Paper } from "@/components/layout/Paper"; import { LoadingSeasons, Seasons } from "@/components/layout/Seasons"; -import { SkeletonVideoPlayer } from "@/components/media/VideoPlayer"; import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer"; import { ArrowLink } from "@/components/text/ArrowLink"; import { DotList } from "@/components/text/DotList"; @@ -30,6 +29,7 @@ import { import { getWatchedFromPortable, useWatchedContext } from "@/state/watched"; import { SourceControl } from "@/components/video/controls/SourceControl"; import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl"; +import { Loading } from "@/components/layout/Loading"; import { NotFoundChecks } from "./notfound/NotFoundChecks"; interface StyledMediaViewProps { @@ -37,6 +37,24 @@ interface StyledMediaViewProps { stream: MWMediaStream; } +export function SkeletonVideoPlayer(props: { error?: boolean }) { + return ( +
+ {props.error ? ( +
+ +

Couldn't get your stream

+
+ ) : ( +
+ +

Getting your stream...

+
+ )} +
+ ); +} + function StyledMediaView(props: StyledMediaViewProps) { const reactHistory = useHistory(); const watchedStore = useWatchedContext(); diff --git a/src/views/TestView.tsx b/src/views/TestView.tsx index eaac96b6..5688b1c8 100644 --- a/src/views/TestView.tsx +++ b/src/views/TestView.tsx @@ -20,7 +20,7 @@ import { useCallback, useState } from "react"; // - browser: firefox, chrome, edge, safari desktop // - phones: android firefox, android chrome, iphone safari // - devices: ipadOS -// - features: HLS, error handling +// - features: HLS, error handling, preload interactions export function TestView() { const [show, setShow] = useState(true); From 8268abc45d6d989c8e527fe9045114229a3f73d9 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Tue, 10 Jan 2023 22:43:27 +0100 Subject: [PATCH 023/135] add search backend --- src/backend/metadata/search.ts | 68 +++++++++++++++++++++++ src/backend/providermeta/.gitkeep | 1 + src/backend/providers/.gitkeep | 1 + src/components/media/MediaCard.tsx | 47 +++++----------- src/components/media/WatchedMediaCard.tsx | 19 +------ src/hooks/useLoading.ts | 11 +++- src/state/bookmark/store.ts | 9 +++ src/state/watched/store.ts | 9 +++ src/views/TestView.tsx | 11 ++++ src/views/search/SearchResultsView.tsx | 49 +++++----------- 10 files changed, 138 insertions(+), 87 deletions(-) create mode 100644 src/backend/metadata/search.ts create mode 100644 src/backend/providermeta/.gitkeep create mode 100644 src/backend/providers/.gitkeep diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts new file mode 100644 index 00000000..c4e844d7 --- /dev/null +++ b/src/backend/metadata/search.ts @@ -0,0 +1,68 @@ +import { MWMediaType, MWQuery } from "@/providers"; + +const JW_API_BASE = "https://apis.justwatch.com"; + +type JWContentTypes = "movie" | "show"; + +type JWSearchQuery = { + content_types: JWContentTypes[]; + page: number; + page_size: number; + query: string; +}; + +type JWSearchResults = { + title: string; + poster?: string; + id: number; + original_release_year: number; + jw_entity_id: string; +}; + +type JWPage = { + items: T[]; + page: number; + page_size: number; + total_pages: number; + total_results: number; +}; + +export type MWSearchResult = { + title: string; + id: string; + year: string; + poster?: string; + type: MWMediaType; +}; + +export async function searchForMedia({ + searchQuery, + type, +}: MWQuery): Promise { + const body: JWSearchQuery = { + content_types: [], + page: 1, + query: searchQuery, + page_size: 40, + }; + if (type === MWMediaType.MOVIE) body.content_types.push("movie"); + else if (type === MWMediaType.SERIES) body.content_types.push("show"); + else if (type === MWMediaType.ANIME) + throw new Error("Anime search type is not supported"); + + const data = await fetch( + `${JW_API_BASE}/content/titles/en_US/popular?body=${encodeURIComponent( + JSON.stringify(body) + )}` + ).then((res) => res.json() as Promise>); + + return data.items.map((v) => ({ + title: v.title, + id: v.id.toString(), + year: v.original_release_year.toString(), + poster: v.poster + ? `https://images.justwatch.com${v.poster.replace("{profile}", "s166")}` + : undefined, + type, + })); +} diff --git a/src/backend/providermeta/.gitkeep b/src/backend/providermeta/.gitkeep new file mode 100644 index 00000000..37c97987 --- /dev/null +++ b/src/backend/providermeta/.gitkeep @@ -0,0 +1 @@ +this folder will be used for provider helper methods and the like diff --git a/src/backend/providers/.gitkeep b/src/backend/providers/.gitkeep new file mode 100644 index 00000000..8fbcff9c --- /dev/null +++ b/src/backend/providers/.gitkeep @@ -0,0 +1 @@ +the new list of all providers, the old ones will go and be rewritten diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 8174f2c8..34fb09b4 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -1,30 +1,16 @@ import { Link } from "react-router-dom"; -import { - convertMediaToPortable, - getProviderFromId, - MWMediaMeta, - MWMediaType, -} from "@/providers"; -import { serializePortableMedia } from "@/hooks/usePortableMedia"; import { DotList } from "@/components/text/DotList"; +import { MWSearchResult } from "@/backend/metadata/search"; +import { MWMediaType } from "@/providers"; export interface MediaCardProps { - media: MWMediaMeta; - // eslint-disable-next-line react/no-unused-prop-types - watchedPercentage: number; + media: MWSearchResult; linkable?: boolean; - series?: boolean; } // TODO add progress back -function MediaCardContent({ media, series, linkable }: MediaCardProps) { - const provider = getProviderFromId(media.providerId); - - if (!provider) { - return null; - } - +function MediaCardContent({ media, linkable }: MediaCardProps) { return (
-
+

{media.title} - {series && media.seasonId && media.episodeId ? ( - - S{media.seasonId} E{media.episodeId} - - ) : null}

- +
); @@ -56,17 +39,13 @@ function MediaCardContent({ media, series, linkable }: MediaCardProps) { export function MediaCard(props: MediaCardProps) { let link = "movie"; - if (props.media.mediaType === MWMediaType.SERIES) link = "series"; + if (props.media.type === MWMediaType.SERIES) link = "series"; const content = ; if (!props.linkable) return {content}; return ( - + {content} ); diff --git a/src/components/media/WatchedMediaCard.tsx b/src/components/media/WatchedMediaCard.tsx index f8338d57..f1d37374 100644 --- a/src/components/media/WatchedMediaCard.tsx +++ b/src/components/media/WatchedMediaCard.tsx @@ -1,23 +1,10 @@ -import { MWMediaMeta } from "@/providers"; -import { useWatchedContext, getWatchedFromPortable } from "@/state/watched"; +import { MWSearchResult } from "@/backend/metadata/search"; import { MediaCard } from "./MediaCard"; export interface WatchedMediaCardProps { - media: MWMediaMeta; - series?: boolean; + media: MWSearchResult; } export function WatchedMediaCard(props: WatchedMediaCardProps) { - const { watched } = useWatchedContext(); - const foundWatched = getWatchedFromPortable(watched.items, props.media); - const watchedPercentage = (foundWatched && foundWatched.percentage) || 0; - - return ( - - ); + return ; } diff --git a/src/hooks/useLoading.ts b/src/hooks/useLoading.ts index 05411004..247a05ed 100644 --- a/src/hooks/useLoading.ts +++ b/src/hooks/useLoading.ts @@ -2,7 +2,12 @@ import React, { useMemo, useRef, useState } from "react"; export function useLoading Promise>( action: T -) { +): [ + (...args: Parameters) => ReturnType | Promise, + boolean, + Error | undefined, + boolean +] { const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState(undefined); @@ -20,11 +25,11 @@ export function useLoading Promise>( const doAction = useMemo( () => - async (...args: Parameters) => { + async (...args: any) => { setLoading(true); setSuccess(false); setError(undefined); - return new Promise((resolve) => { + return new Promise((resolve) => { actionMemo(...args) .then((v) => { if (!isMounted.current) return resolve(undefined); diff --git a/src/state/bookmark/store.ts b/src/state/bookmark/store.ts index 17f06642..06456b78 100644 --- a/src/state/bookmark/store.ts +++ b/src/state/bookmark/store.ts @@ -4,6 +4,15 @@ export const BookmarkStore = versionedStoreBuilder() .setKey("mw-bookmarks") .addVersion({ version: 0, + }) + .addVersion({ + version: 1, + migrate() { + return { + // TODO actually migrate + bookmarks: [], + }; + }, create() { return { bookmarks: [], diff --git a/src/state/watched/store.ts b/src/state/watched/store.ts index 0b3a79f7..065de4ec 100644 --- a/src/state/watched/store.ts +++ b/src/state/watched/store.ts @@ -85,6 +85,15 @@ export const VideoProgressStore = versionedStoreBuilder() return output; }, + }) + .addVersion({ + version: 2, + migrate() { + // TODO actually migrate + return { + items: [], + }; + }, create() { return { items: [], diff --git a/src/views/TestView.tsx b/src/views/TestView.tsx index 5688b1c8..5314996d 100644 --- a/src/views/TestView.tsx +++ b/src/views/TestView.tsx @@ -1,6 +1,8 @@ +import { searchForMedia } from "@/backend/metadata/search"; import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl"; import { SourceControl } from "@/components/video/controls/SourceControl"; import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer"; +import { MWMediaType } from "@/providers"; import { useCallback, useState } from "react"; // test videos: https://gist.github.com/jsturgis/3b19447b304616f18657 @@ -32,6 +34,14 @@ export function TestView() { return

Click me to show

; } + async function search() { + const test = await searchForMedia({ + searchQuery: "tron", + type: MWMediaType.MOVIE, + }); + console.log(test); + } + return (
@@ -44,6 +54,7 @@ export function TestView() { onProgress={(a, b) => console.log(a, b)} /> +

search()}>click me to search

); } diff --git a/src/views/search/SearchResultsView.tsx b/src/views/search/SearchResultsView.tsx index 6a775a23..ad611516 100644 --- a/src/views/search/SearchResultsView.tsx +++ b/src/views/search/SearchResultsView.tsx @@ -6,38 +6,26 @@ import { SectionHeading } from "@/components/layout/SectionHeading"; import { MediaGrid } from "@/components/media/MediaGrid"; import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; import { useLoading } from "@/hooks/useLoading"; -import { MWMassProviderOutput, MWQuery, SearchProviders } from "@/providers"; +import { MWQuery } from "@/providers"; +import { MWSearchResult, searchForMedia } from "@/backend/metadata/search"; import { SearchLoadingView } from "./SearchLoadingView"; -function SearchSuffix(props: { - fails: number; - total: number; - resultsSize: number; -}) { +function SearchSuffix(props: { failed?: boolean; results?: number }) { const { t } = useTranslation(); - const allFailed: boolean = props.fails === props.total; - const icon: Icons = allFailed ? Icons.WARNING : Icons.EYE_SLASH; + const icon: Icons = props.failed ? Icons.WARNING : Icons.EYE_SLASH; return (
{/* standard suffix */} - {!allFailed ? ( + {!props.failed ? (
- {props.fails > 0 ? ( -

- {t("search.providersFailed", { - fails: props.fails, - total: props.total, - })} -

- ) : null} - {props.resultsSize > 0 ? ( + {(props.results ?? 0) > 0 ? (

{t("search.allResults")}

) : (

{t("search.noResults")}

@@ -46,7 +34,7 @@ function SearchSuffix(props: { ) : null} {/* Error result */} - {allFailed ? ( + {props.failed ? (

{t("search.allFailed")}

@@ -58,9 +46,9 @@ function SearchSuffix(props: { export function SearchResultsView({ searchQuery }: { searchQuery: MWQuery }) { const { t } = useTranslation(); - const [results, setResults] = useState(); + const [results, setResults] = useState([]); const [runSearchQuery, loading, error] = useLoading((query: MWQuery) => - SearchProviders(query) + searchForMedia(query) ); useEffect(() => { @@ -74,32 +62,25 @@ export function SearchResultsView({ searchQuery }: { searchQuery: MWQuery }) { }, [searchQuery, runSearchQuery]); if (loading) return ; - if (error) return ; + if (error) return ; if (!results) return null; return (
- {results?.results.length > 0 ? ( + {results.length > 0 ? ( - {results.results.map((v) => ( - + {results.map((v) => ( + ))} ) : null} - +
); } From f1257973e70ff15f9b1e98bcf8c52cfe10297b3d Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Wed, 11 Jan 2023 21:16:48 +0100 Subject: [PATCH 024/135] new backend interfaces --- src/backend/embeds/testEmbedScraper.ts | 20 +++++++++ src/backend/helpers/embed.ts | 24 ++++++++++ src/backend/helpers/provider.ts | 23 ++++++++++ src/backend/helpers/register.ts | 61 ++++++++++++++++++++++++++ src/backend/helpers/streams.ts | 9 ++++ src/backend/index.ts | 17 +++++++ src/backend/metadata/search.ts | 13 ++---- src/backend/metadata/types.ts | 13 ++++++ src/backend/providermeta/.gitkeep | 1 - src/backend/providers/.gitkeep | 1 - src/backend/providers/testProvider.ts | 32 ++++++++++++++ src/index.tsx | 1 + 12 files changed, 203 insertions(+), 12 deletions(-) create mode 100644 src/backend/embeds/testEmbedScraper.ts create mode 100644 src/backend/helpers/embed.ts create mode 100644 src/backend/helpers/provider.ts create mode 100644 src/backend/helpers/register.ts create mode 100644 src/backend/helpers/streams.ts create mode 100644 src/backend/index.ts create mode 100644 src/backend/metadata/types.ts delete mode 100644 src/backend/providermeta/.gitkeep delete mode 100644 src/backend/providers/.gitkeep create mode 100644 src/backend/providers/testProvider.ts diff --git a/src/backend/embeds/testEmbedScraper.ts b/src/backend/embeds/testEmbedScraper.ts new file mode 100644 index 00000000..8498c0a2 --- /dev/null +++ b/src/backend/embeds/testEmbedScraper.ts @@ -0,0 +1,20 @@ +import { MWEmbedType } from "../helpers/embed"; +import { registerEmbedScraper } from "../helpers/register"; +import { MWStreamType } from "../helpers/streams"; + +registerEmbedScraper({ + id: "testembed", + rank: 23, + for: MWEmbedType.OPENLOAD, + + async getStream({ progress, url }) { + console.log("scraping url: ", url); + progress(25); + progress(50); + progress(75); + return { + streamUrl: "hello-world", + type: MWStreamType.MP4, + }; + }, +}); diff --git a/src/backend/helpers/embed.ts b/src/backend/helpers/embed.ts new file mode 100644 index 00000000..20cd3b29 --- /dev/null +++ b/src/backend/helpers/embed.ts @@ -0,0 +1,24 @@ +import { MWStream } from "./streams"; + +export enum MWEmbedType { + OPENLOAD = "openload", +} + +export type MWEmbed = { + type: MWEmbedType | null; + url: string; +}; + +export type MWEmbedContext = { + progress(percentage: number): void; + url: string; +}; + +export type MWEmbedScraper = { + id: string; + for: MWEmbedType; + rank: number; + disabled?: boolean; + + getStream(ctx: MWEmbedContext): Promise; +}; diff --git a/src/backend/helpers/provider.ts b/src/backend/helpers/provider.ts new file mode 100644 index 00000000..38e4bbfa --- /dev/null +++ b/src/backend/helpers/provider.ts @@ -0,0 +1,23 @@ +import { MWMediaType } from "../metadata/types"; +import { MWEmbed } from "./embed"; +import { MWStream } from "./streams"; + +export type MWProviderScrapeResult = { + stream?: MWStream; + embeds: MWEmbed[]; +}; + +export type MWProviderContext = { + progress(percentage: number): void; + imdbId: string; + tmdbId: string; +}; + +export type MWProvider = { + id: string; + rank: number; + disabled?: boolean; + type: MWMediaType[]; + + scrape(ctx: MWProviderContext): Promise; +}; diff --git a/src/backend/helpers/register.ts b/src/backend/helpers/register.ts new file mode 100644 index 00000000..001aed16 --- /dev/null +++ b/src/backend/helpers/register.ts @@ -0,0 +1,61 @@ +import { MWEmbedScraper } from "./embed"; +import { MWProvider } from "./provider"; + +let providers: MWProvider[] = []; +let embeds: MWEmbedScraper[] = []; + +export function registerProvider(provider: MWProvider) { + if (provider.disabled) return; + providers.push(provider); +} +export function registerEmbedScraper(embed: MWEmbedScraper) { + if (embed.disabled) return; + embeds.push(embed); +} + +export function initializeScraperStore() { + // sort by ranking + providers = providers.sort((a, b) => a.rank - b.rank); + embeds = embeds.sort((a, b) => a.rank - b.rank); + + // check for invalid ranks + let lastRank: null | number = null; + providers.forEach((v) => { + if (lastRank === null) { + lastRank = v.rank; + return; + } + if (lastRank === v.rank) + throw new Error(`Duplicate rank number for provider ${v.id}`); + lastRank = v.rank; + }); + lastRank = null; + providers.forEach((v) => { + if (lastRank === null) { + lastRank = v.rank; + return; + } + if (lastRank === v.rank) + throw new Error(`Duplicate rank number for embed scraper ${v.id}`); + lastRank = v.rank; + }); + + // check for duplicate ids + const providerIds = providers.map((v) => v.id); + if ( + providerIds.length > 0 && + new Set(providerIds).size !== providerIds.length + ) + throw new Error("Duplicate IDS in providers"); + const embedIds = embeds.map((v) => v.id); + if (embedIds.length > 0 && new Set(embedIds).size !== embedIds.length) + throw new Error("Duplicate IDS in embed scrapers"); +} + +export function getProviders(): MWProvider[] { + return providers; +} + +export function getEmbeds(): MWEmbedScraper[] { + return embeds; +} diff --git a/src/backend/helpers/streams.ts b/src/backend/helpers/streams.ts new file mode 100644 index 00000000..6eb7257b --- /dev/null +++ b/src/backend/helpers/streams.ts @@ -0,0 +1,9 @@ +export enum MWStreamType { + MP4 = "mp4", + HLS = "hls", +} + +export type MWStream = { + streamUrl: string; + type: MWStreamType; +}; diff --git a/src/backend/index.ts b/src/backend/index.ts new file mode 100644 index 00000000..34983fad --- /dev/null +++ b/src/backend/index.ts @@ -0,0 +1,17 @@ +import { initializeScraperStore } from "./helpers/register"; + +// TODO backend system: +// - run providers/embedscrapers in webworkers for multithreading and isolation +// - caption support +// - hooks to run all providers one by one +// - move over old providers to new system +// - implement jons providers/embedscrapers + +// providers +// -- nothing here yet +import "./providers/testProvider"; + +// embeds +// -- nothing here yet + +initializeScraperStore(); diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index c4e844d7..fa26b35d 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -1,4 +1,5 @@ import { MWMediaType, MWQuery } from "@/providers"; +import { MWMediaMeta } from "./types"; const JW_API_BASE = "https://apis.justwatch.com"; @@ -27,18 +28,10 @@ type JWPage = { total_results: number; }; -export type MWSearchResult = { - title: string; - id: string; - year: string; - poster?: string; - type: MWMediaType; -}; - export async function searchForMedia({ searchQuery, type, -}: MWQuery): Promise { +}: MWQuery): Promise { const body: JWSearchQuery = { content_types: [], page: 1, @@ -56,7 +49,7 @@ export async function searchForMedia({ )}` ).then((res) => res.json() as Promise>); - return data.items.map((v) => ({ + return data.items.map((v) => ({ title: v.title, id: v.id.toString(), year: v.original_release_year.toString(), diff --git a/src/backend/metadata/types.ts b/src/backend/metadata/types.ts new file mode 100644 index 00000000..a74e0520 --- /dev/null +++ b/src/backend/metadata/types.ts @@ -0,0 +1,13 @@ +export enum MWMediaType { + MOVIE = "movie", + SERIES = "series", + ANIME = "anime", +} + +export type MWMediaMeta = { + title: string; + id: string; + year: string; + poster?: string; + type: MWMediaType; +}; diff --git a/src/backend/providermeta/.gitkeep b/src/backend/providermeta/.gitkeep deleted file mode 100644 index 37c97987..00000000 --- a/src/backend/providermeta/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -this folder will be used for provider helper methods and the like diff --git a/src/backend/providers/.gitkeep b/src/backend/providers/.gitkeep deleted file mode 100644 index 8fbcff9c..00000000 --- a/src/backend/providers/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -the new list of all providers, the old ones will go and be rewritten diff --git a/src/backend/providers/testProvider.ts b/src/backend/providers/testProvider.ts new file mode 100644 index 00000000..455325ca --- /dev/null +++ b/src/backend/providers/testProvider.ts @@ -0,0 +1,32 @@ +import { MWEmbedType } from "../helpers/embed"; +import { registerProvider } from "../helpers/register"; +import { MWStreamType } from "../helpers/streams"; +import { MWMediaType } from "../metadata/types"; + +registerProvider({ + id: "testprov", + rank: 42, + type: [MWMediaType.MOVIE], + + async scrape({ progress, imdbId, tmdbId }) { + console.log("scraping provider for: ", imdbId, tmdbId); + progress(25); + progress(50); + progress(75); + + // providers can optionally provide a stream themselves, + // incase they host their own streams instead of using embeds + return { + stream: { + streamUrl: "hello-world", + type: MWStreamType.HLS, + }, + embeds: [ + { + type: MWEmbedType.OPENLOAD, + url: "https://google.com", + }, + ], + }; + }, +}); diff --git a/src/index.tsx b/src/index.tsx index d6b93ba0..fbb6e122 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -7,6 +7,7 @@ import { conf } from "@/setup/config"; import App from "@/setup/App"; import "@/setup/i18n"; import "@/setup/index.css"; +import "@/backend"; // initialize const key = From e34ddddddbd28fb7aa0f5d0f928c5000faa6a3b6 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Wed, 11 Jan 2023 21:17:44 +0100 Subject: [PATCH 025/135] remove old providers --- src/providers/README.md | 31 - src/providers/index.ts | 42 -- src/providers/list/flixhq/index.ts | 89 --- src/providers/list/gdriveplayer/index.ts | 125 ---- src/providers/list/gomostream/index.ts | 125 ---- src/providers/list/superstream/LICENSE | 680 ------------------ src/providers/list/superstream/index.ts | 307 -------- src/providers/list/theflix/index.ts | 119 --- src/providers/list/theflix/portableToMedia.ts | 36 - src/providers/list/theflix/search.ts | 48 -- src/providers/list/xemovie/index.ts | 142 ---- src/providers/methods/contentCache.ts | 11 - src/providers/methods/helpers.ts | 65 -- src/providers/methods/providers.ts | 19 - src/providers/methods/search.ts | 105 --- src/providers/methods/seasons.ts | 50 -- src/providers/types.ts | 97 --- src/providers/wrapper.ts | 48 -- 18 files changed, 2139 deletions(-) delete mode 100644 src/providers/README.md delete mode 100644 src/providers/index.ts delete mode 100644 src/providers/list/flixhq/index.ts delete mode 100644 src/providers/list/gdriveplayer/index.ts delete mode 100644 src/providers/list/gomostream/index.ts delete mode 100644 src/providers/list/superstream/LICENSE delete mode 100644 src/providers/list/superstream/index.ts delete mode 100644 src/providers/list/theflix/index.ts delete mode 100644 src/providers/list/theflix/portableToMedia.ts delete mode 100644 src/providers/list/theflix/search.ts delete mode 100644 src/providers/list/xemovie/index.ts delete mode 100644 src/providers/methods/contentCache.ts delete mode 100644 src/providers/methods/helpers.ts delete mode 100644 src/providers/methods/providers.ts delete mode 100644 src/providers/methods/search.ts delete mode 100644 src/providers/methods/seasons.ts delete mode 100644 src/providers/types.ts delete mode 100644 src/providers/wrapper.ts diff --git a/src/providers/README.md b/src/providers/README.md deleted file mode 100644 index a32dcc4f..00000000 --- a/src/providers/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# the providers - -to make this as clear as possible, here is some extra information on how the interal system works regarding providers. - -| Term | explanation | -| ------------- | ------------------------------------------------------------------------------------- | -| Media | Object containing information about a piece of media. like title and its id's | -| PortableMedia | Object with just the identifiers of a piece of media. used for transport and saving | -| MediaStream | Object with a stream url in it. use it to view a piece of media. | -| Provider | group of methods to generate media and mediastreams from a source. aliased as scraper | - -All types are prefixed with MW (MovieWeb) to prevent clashing names. - -## Some rules - -1. **Never** remove a provider completely if it's been in use before. just disable it. -2. **Never** change the ID of a provider if it's been in use before. -3. **Never** change system of the media ID of a provider without making it backwards compatible - -All these rules are because `PortableMedia` objects need to stay functional. because: - -- It's used for routing, links would stop working -- It's used for storage, continue watching and bookmarks would stop working - -# The list of providers and their quirks - -Some providers have quirks, stuff they do differently than other providers - -## TheFlix - -- for series, the latest episode released will be one playing at first when you select it from search results diff --git a/src/providers/index.ts b/src/providers/index.ts deleted file mode 100644 index 5ea5cfbb..00000000 --- a/src/providers/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { getProviderFromId } from "./methods/helpers"; -import { MWMedia, MWPortableMedia, MWMediaStream } from "./types"; - -export * from "./types"; -export * from "./methods/helpers"; -export * from "./methods/providers"; -export * from "./methods/search"; - -/* - ** Turn media object into a portable media object - */ -export function convertMediaToPortable(media: MWMedia): MWPortableMedia { - return { - mediaId: media.mediaId, - providerId: media.providerId, - mediaType: media.mediaType, - episodeId: media.episodeId, - seasonId: media.seasonId, - }; -} - -/* - ** Turn portable media into media object - */ -export async function convertPortableToMedia( - portable: MWPortableMedia -): Promise { - const provider = getProviderFromId(portable.providerId); - return provider?.getMediaFromPortable(portable); -} - -/* - ** find provider from portable and get stream from that provider - */ -export async function getStream( - media: MWPortableMedia -): Promise { - const provider = getProviderFromId(media.providerId); - if (!provider) return undefined; - - return provider.getStream(media); -} diff --git a/src/providers/list/flixhq/index.ts b/src/providers/list/flixhq/index.ts deleted file mode 100644 index 304f0157..00000000 --- a/src/providers/list/flixhq/index.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { - MWMediaProvider, - MWMediaType, - MWPortableMedia, - MWMediaStream, - MWQuery, - MWProviderMediaResult, -} from "@/providers/types"; - -import { conf } from "@/setup/config"; - -export const flixhqProvider: MWMediaProvider = { - id: "flixhq", - enabled: true, - type: [MWMediaType.MOVIE], - displayName: "flixhq", - - async getMediaFromPortable( - media: MWPortableMedia - ): Promise { - const searchRes = await fetch( - `${ - conf().CORS_PROXY_URL - }https://api.consumet.org/movies/flixhq/info?id=${encodeURIComponent( - media.mediaId - )}` - ).then((d) => d.json()); - - return { - ...media, - title: searchRes.title, - year: searchRes.releaseDate, - } as MWProviderMediaResult; - }, - - async searchForMedia(query: MWQuery): Promise { - const searchRes = await fetch( - `${ - conf().CORS_PROXY_URL - }https://api.consumet.org/movies/flixhq/${encodeURIComponent( - query.searchQuery - )}` - ).then((d) => d.json()); - - const results: MWProviderMediaResult[] = (searchRes || []).results.map( - (item: any) => ({ - title: item.title, - year: item.releaseDate, - mediaId: item.id, - type: MWMediaType.MOVIE, - }) - ); - - return results; - }, - - async getStream(media: MWPortableMedia): Promise { - const searchRes = await fetch( - `${ - conf().CORS_PROXY_URL - }https://api.consumet.org/movies/flixhq/info?id=${encodeURIComponent( - media.mediaId - )}` - ).then((d) => d.json()); - - const params = new URLSearchParams({ - episodeId: searchRes.episodes[0].id, - mediaId: media.mediaId, - }); - - const watchRes = await fetch( - `${ - conf().CORS_PROXY_URL - }https://api.consumet.org/movies/flixhq/watch?${encodeURIComponent( - params.toString() - )}` - ).then((d) => d.json()); - - const source = watchRes.sources.reduce((p: any, c: any) => - c.quality > p.quality ? c : p - ); - - return { - url: source.url, - type: source.isM3U8 ? "m3u8" : "mp4", - captions: [], - } as MWMediaStream; - }, -}; diff --git a/src/providers/list/gdriveplayer/index.ts b/src/providers/list/gdriveplayer/index.ts deleted file mode 100644 index b1ed69d3..00000000 --- a/src/providers/list/gdriveplayer/index.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { unpack } from "unpacker"; -import CryptoJS from "crypto-js"; -import { - MWMediaProvider, - MWMediaType, - MWPortableMedia, - MWMediaStream, - MWQuery, - MWProviderMediaResult, -} from "@/providers/types"; - -import { conf } from "@/setup/config"; - -const format = { - stringify: (cipher: any) => { - const ct = cipher.ciphertext.toString(CryptoJS.enc.Base64); - const iv = cipher.iv.toString() || ""; - const salt = cipher.salt.toString() || ""; - return JSON.stringify({ - ct, - iv, - salt, - }); - }, - parse: (jsonStr: string) => { - const json = JSON.parse(jsonStr); - const ciphertext = CryptoJS.enc.Base64.parse(json.ct); - const iv = CryptoJS.enc.Hex.parse(json.iv) || ""; - const salt = CryptoJS.enc.Hex.parse(json.s) || ""; - - const cipher = CryptoJS.lib.CipherParams.create({ - ciphertext, - iv, - salt, - }); - return cipher; - }, -}; - -export const gDrivePlayerScraper: MWMediaProvider = { - id: "gdriveplayer", - enabled: true, - type: [MWMediaType.MOVIE], - displayName: "gdriveplayer", - - async getMediaFromPortable( - media: MWPortableMedia - ): Promise { - const res = await fetch( - `${conf().CORS_PROXY_URL}https://api.gdriveplayer.us/v1/imdb/${ - media.mediaId - }` - ).then((d) => d.json()); - - return { - ...media, - title: res.Title, - year: res.Year, - } as MWProviderMediaResult; - }, - - async searchForMedia(query: MWQuery): Promise { - const searchRes = await fetch( - `${ - conf().CORS_PROXY_URL - }https://api.gdriveplayer.us/v1/movie/search?title=${query.searchQuery}` - ).then((d) => d.json()); - - const results: MWProviderMediaResult[] = (searchRes || []).map( - (item: any) => ({ - title: item.title, - year: item.year, - mediaId: item.imdb, - }) - ); - - return results; - }, - - async getStream(media: MWPortableMedia): Promise { - const streamRes = await fetch( - `${ - conf().CORS_PROXY_URL - }https://database.gdriveplayer.us/player.php?imdb=${media.mediaId}` - ).then((d) => d.text()); - const page = new DOMParser().parseFromString(streamRes, "text/html"); - - const script: HTMLElement | undefined = Array.from( - page.querySelectorAll("script") - ).find((e) => e.textContent?.includes("eval")); - - if (!script || !script.textContent) { - throw new Error("Could not find stream"); - } - - /// NOTE: this code requires re-write, it's not safe - const data = unpack(script.textContent) - .split("var data=\\'")[1] - .split("\\'")[0] - .replace(/\\/g, ""); - const decryptedData = unpack( - CryptoJS.AES.decrypt( - data, - "alsfheafsjklNIWORNiolNIOWNKLNXakjsfwnBdwjbwfkjbJjkopfjweopjASoiwnrflakefneiofrt", - { format } - ).toString(CryptoJS.enc.Utf8) - ); - // eslint-disable-next-line - const sources = JSON.parse( - JSON.stringify( - eval( - decryptedData - .split("sources:")[1] - .split(",image")[0] - .replace(/\\/g, "") - .replace(/document\.referrer/g, '""') - ) - ) - ); - const source = sources[sources.length - 1]; - /// END - - return { url: `https:${source.file}`, type: source.type, captions: [] }; - }, -}; diff --git a/src/providers/list/gomostream/index.ts b/src/providers/list/gomostream/index.ts deleted file mode 100644 index e9d65d88..00000000 --- a/src/providers/list/gomostream/index.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { unpack } from "unpacker"; -import json5 from "json5"; -import { - MWMediaProvider, - MWMediaType, - MWPortableMedia, - MWMediaStream, - MWQuery, - MWProviderMediaResult, -} from "@/providers/types"; - -import { conf } from "@/setup/config"; - -export const gomostreamScraper: MWMediaProvider = { - id: "gomostream", - enabled: true, - type: [MWMediaType.MOVIE], - displayName: "gomostream", - - async getMediaFromPortable( - media: MWPortableMedia - ): Promise { - const params = new URLSearchParams({ - apikey: conf().OMDB_API_KEY, - i: media.mediaId, - type: media.mediaType, - }); - - const res = await fetch( - `${conf().CORS_PROXY_URL}http://www.omdbapi.com/?${encodeURIComponent( - params.toString() - )}` - ).then((d) => d.json()); - - return { - ...media, - title: res.Title, - year: res.Year, - } as MWProviderMediaResult; - }, - - async searchForMedia(query: MWQuery): Promise { - const term = query.searchQuery.toLowerCase(); - - const params = new URLSearchParams({ - apikey: conf().OMDB_API_KEY, - s: term, - type: query.type, - }); - const searchRes = await fetch( - `${conf().CORS_PROXY_URL}http://www.omdbapi.com/?${encodeURIComponent( - params.toString() - )}` - ).then((d) => d.json()); - - const results: MWProviderMediaResult[] = (searchRes.Search || []).map( - (d: any) => - ({ - title: d.Title, - year: d.Year, - mediaId: d.imdbID, - } as MWProviderMediaResult) - ); - - return results; - }, - - async getStream(media: MWPortableMedia): Promise { - const type = - media.mediaType === MWMediaType.SERIES ? "show" : media.mediaType; - const res1 = await fetch( - `${conf().CORS_PROXY_URL}https://gomo.to/${type}/${media.mediaId}` - ).then((d) => d.text()); - if (res1 === "Movie not available." || res1 === "Episode not available.") - throw new Error(res1); - - const tc = res1.match(/var tc = '(.+)';/)?.[1] || ""; - const _token = res1.match(/"_token": "(.+)",/)?.[1] || ""; - - const fd = new FormData(); - fd.append("tokenCode", tc); - fd.append("_token", _token); - - const src = await fetch( - `${conf().CORS_PROXY_URL}https://gomo.to/decoding_v3.php`, - { - method: "POST", - body: fd, - headers: { - "x-token": `${tc.slice(5, 13).split("").reverse().join("")}13574199`, - }, - } - ).then((d) => d.json()); - const embeds = src.filter((url: string) => url.includes("gomo.to")); - - // maybe try all embeds in the future - const embedUrl = embeds[1]; - const res2 = await fetch(`${conf().CORS_PROXY_URL}${embedUrl}`).then((d) => - d.text() - ); - - const res2DOM = new DOMParser().parseFromString(res2, "text/html"); - if (res2DOM.body.innerText === "File was deleted") - throw new Error("File was deleted"); - - const script = Array.from(res2DOM.querySelectorAll("script")).find( - (s: HTMLScriptElement) => - s.innerHTML.includes("eval(function(p,a,c,k,e,d") - )?.innerHTML; - if (!script) throw new Error("Could not get packed data"); - - const unpacked = unpack(script); - const rawSources = /sources:(\[.*?\])/.exec(unpacked); - if (!rawSources) throw new Error("Could not get rawSources"); - - const sources = json5.parse(rawSources[1]); - const streamUrl = sources[0].file; - - const streamType = streamUrl.split(".").at(-1); - if (streamType !== "mp4" && streamType !== "m3u8") - throw new Error("Unsupported stream type"); - - return { url: streamUrl, type: streamType, captions: [] }; - }, -}; diff --git a/src/providers/list/superstream/LICENSE b/src/providers/list/superstream/LICENSE deleted file mode 100644 index 3f5347b0..00000000 --- a/src/providers/list/superstream/LICENSE +++ /dev/null @@ -1,680 +0,0 @@ -Credit goes to @ImZaw and @Blatzar from https://github.com/recloudstream/cloudstream -All files in the current directory (src/providers/list/superstream) are derived from https://github.com/recloudstream/cloudstream-extensions/blob/master/SuperStream/src/main/kotlin/com/lagradost/SuperStream.kt -Below is the license associated with the source of the derived work. - - - - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/src/providers/list/superstream/index.ts b/src/providers/list/superstream/index.ts deleted file mode 100644 index 832e141a..00000000 --- a/src/providers/list/superstream/index.ts +++ /dev/null @@ -1,307 +0,0 @@ -// this is derived from https://github.com/recloudstream/cloudstream-extensions -// for more info please check the LICENSE file in the same directory - -import { customAlphabet } from "nanoid"; -import toWebVTT from "srt-webvtt"; -import CryptoJS from "crypto-js"; -import { conf } from "@/setup/config"; -import { - MWMediaProvider, - MWMediaType, - MWPortableMedia, - MWMediaStream, - MWQuery, - MWMediaSeasons, - MWProviderMediaResult, -} from "@/providers/types"; - -const nanoid = customAlphabet("0123456789abcdef", 32); - -// CONSTANTS, read below (taken from og) -// We do not want content scanners to notice this scraping going on so we've hidden all constants -// The source has its origins in China so I added some extra security with banned words -// Mayhaps a tiny bit unethical, but this source is just too good :) -// If you are copying this code please use precautions so they do not change their api. -const iv = atob("d0VpcGhUbiE="); -const key = atob("MTIzZDZjZWRmNjI2ZHk1NDIzM2FhMXc2"); -const apiUrls = [ - atob("aHR0cHM6Ly9zaG93Ym94LnNoZWd1Lm5ldC9hcGkvYXBpX2NsaWVudC9pbmRleC8="), - atob("aHR0cHM6Ly9tYnBhcGkuc2hlZ3UubmV0L2FwaS9hcGlfY2xpZW50L2luZGV4Lw=="), -]; -const appKey = atob("bW92aWVib3g="); -const appId = atob("Y29tLnRkby5zaG93Ym94"); - -// cryptography stuff -const crypto = { - encrypt(str: string) { - return CryptoJS.TripleDES.encrypt(str, CryptoJS.enc.Utf8.parse(key), { - iv: CryptoJS.enc.Utf8.parse(iv), - }).toString(); - }, - getVerify(str: string, str2: string, str3: string) { - if (str) { - return CryptoJS.MD5( - CryptoJS.MD5(str2).toString() + str3 + str - ).toString(); - } - return null; - }, -}; - -// get expire time -const expiry = () => Math.floor(Date.now() / 1000 + 60 * 60 * 12); - -// sending requests -const get = (data: object, altApi = false) => { - const defaultData = { - childmode: "0", - app_version: "11.5", - appid: appId, - lang: "en", - expired_date: `${expiry()}`, - platform: "android", - channel: "Website", - }; - const encryptedData = crypto.encrypt( - JSON.stringify({ - ...defaultData, - ...data, - }) - ); - const appKeyHash = CryptoJS.MD5(appKey).toString(); - const verify = crypto.getVerify(encryptedData, appKey, key); - const body = JSON.stringify({ - app_key: appKeyHash, - verify, - encrypt_data: encryptedData, - }); - const b64Body = btoa(body); - - const formatted = new URLSearchParams(); - formatted.append("data", b64Body); - formatted.append("appid", "27"); - formatted.append("platform", "android"); - formatted.append("version", "129"); - formatted.append("medium", "Website"); - - const requestUrl = altApi ? apiUrls[1] : apiUrls[0]; - return fetch(`${conf().CORS_PROXY_URL}${requestUrl}`, { - method: "POST", - headers: { - Platform: "android", - "Content-Type": "application/x-www-form-urlencoded", - }, - body: `${formatted.toString()}&token${nanoid()}`, - }); -}; - -export const superStreamScraper: MWMediaProvider = { - id: "superstream", - enabled: true, - type: [MWMediaType.MOVIE, MWMediaType.SERIES], - displayName: "SuperStream", - - async getMediaFromPortable( - media: MWPortableMedia - ): Promise { - let apiQuery: any; - if (media.mediaType === MWMediaType.SERIES) { - apiQuery = { - module: "TV_detail_1", - display_all: "1", - tid: media.mediaId, - }; - } else { - apiQuery = { - module: "Movie_detail", - mid: media.mediaId, - }; - } - const detailRes = (await get(apiQuery, true).then((r) => r.json())).data; - - return { - ...media, - title: detailRes.title, - year: detailRes.year, - seasonCount: detailRes?.season?.length, - } as MWProviderMediaResult; - }, - - async searchForMedia(query: MWQuery): Promise { - const apiQuery = { - module: "Search3", - page: "1", - type: "all", - keyword: query.searchQuery, - pagelimit: "20", - }; - const searchRes = (await get(apiQuery, true).then((r) => r.json())).data; - - const movieResults: MWProviderMediaResult[] = (searchRes || []) - .filter((item: any) => item.box_type === 1) - .map((item: any) => ({ - title: item.title, - year: item.year, - mediaId: item.id, - })); - const seriesResults: MWProviderMediaResult[] = (searchRes || []) - .filter((item: any) => item.box_type === 2) - .map((item: any) => ({ - title: item.title, - year: item.year, - mediaId: item.id, - seasonId: "1", - episodeId: "1", - })); - - if (query.type === MWMediaType.MOVIE) { - return movieResults; - } - if (query.type === MWMediaType.SERIES) { - return seriesResults; - } - throw new Error("Invalid media type used."); - }, - - async getStream(media: MWPortableMedia): Promise { - if (media.mediaType === MWMediaType.MOVIE) { - const apiQuery = { - uid: "", - module: "Movie_downloadurl_v3", - mid: media.mediaId, - oss: "1", - group: "", - }; - const mediaRes = (await get(apiQuery).then((r) => r.json())).data; - const hdQuality = - mediaRes.list.find( - (quality: any) => quality.quality === "1080p" && quality.path - ) ?? - mediaRes.list.find( - (quality: any) => quality.quality === "720p" && quality.path - ) ?? - mediaRes.list.find( - (quality: any) => quality.quality === "480p" && quality.path - ) ?? - mediaRes.list.find( - (quality: any) => quality.quality === "360p" && quality.path - ); - - if (!hdQuality) throw new Error("No quality could be found."); - - const subtitleApiQuery = { - fid: hdQuality.fid, - uid: "", - module: "Movie_srt_list_v2", - mid: media.mediaId, - }; - const subtitleRes = (await get(subtitleApiQuery).then((r) => r.json())) - .data; - const mappedCaptions = await Promise.all( - subtitleRes.list.map(async (subtitle: any) => { - const captionBlob = await fetch( - `${conf().CORS_PROXY_URL}${subtitle.subtitles[0].file_path}` - ).then((captionRes) => captionRes.blob()); // cross-origin bypass - const captionUrl = await toWebVTT(captionBlob); // convert to vtt so it's playable - return { - id: subtitle.language, - url: captionUrl, - label: subtitle.language, - }; - }) - ); - - return { url: hdQuality.path, type: "mp4", captions: mappedCaptions }; - } - - const apiQuery = { - uid: "", - module: "TV_downloadurl_v3", - episode: media.episodeId, - tid: media.mediaId, - season: media.seasonId, - oss: "1", - group: "", - }; - const mediaRes = (await get(apiQuery).then((r) => r.json())).data; - const hdQuality = - mediaRes.list.find( - (quality: any) => quality.quality === "1080p" && quality.path - ) ?? - mediaRes.list.find( - (quality: any) => quality.quality === "720p" && quality.path - ) ?? - mediaRes.list.find( - (quality: any) => quality.quality === "480p" && quality.path - ) ?? - mediaRes.list.find( - (quality: any) => quality.quality === "360p" && quality.path - ); - - if (!hdQuality) throw new Error("No quality could be found."); - - const subtitleApiQuery = { - fid: hdQuality.fid, - uid: "", - module: "TV_srt_list_v2", - episode: media.episodeId, - tid: media.mediaId, - season: media.seasonId, - }; - const subtitleRes = (await get(subtitleApiQuery).then((r) => r.json())) - .data; - const mappedCaptions = await Promise.all( - subtitleRes.list.map(async (subtitle: any) => { - const captionBlob = await fetch( - `${conf().CORS_PROXY_URL}${subtitle.subtitles[0].file_path}` - ).then((captionRes) => captionRes.blob()); // cross-origin bypass - const captionUrl = await toWebVTT(captionBlob); // convert to vtt so it's playable - return { - id: subtitle.language, - url: captionUrl, - label: subtitle.language, - }; - }) - ); - - return { url: hdQuality.path, type: "mp4", captions: mappedCaptions }; - }, - async getSeasonDataFromMedia( - media: MWPortableMedia - ): Promise { - const apiQuery = { - module: "TV_detail_1", - display_all: "1", - tid: media.mediaId, - }; - const detailRes = (await get(apiQuery, true).then((r) => r.json())).data; - const firstSearchResult = ( - await fetch( - `https://api.themoviedb.org/3/search/tv?api_key=${ - conf().TMDB_API_KEY - }&language=en-US&page=1&query=${detailRes.title}&include_adult=false` - ).then((r) => r.json()) - ).results[0]; - const showDetails = await fetch( - `https://api.themoviedb.org/3/tv/${firstSearchResult.id}?api_key=${ - conf().TMDB_API_KEY - }` - ).then((r) => r.json()); - - return { - seasons: showDetails.seasons.map((season: any) => ({ - sort: season.season_number, - id: season.season_number.toString(), - type: season.season_number === 0 ? "special" : "season", - episodes: Array.from({ length: season.episode_count }).map( - (_, epNum) => ({ - title: `Episode ${epNum + 1}`, - sort: epNum + 1, - id: (epNum + 1).toString(), - episodeNumber: epNum + 1, - }) - ), - })), - }; - }, -}; diff --git a/src/providers/list/theflix/index.ts b/src/providers/list/theflix/index.ts deleted file mode 100644 index 01ac7092..00000000 --- a/src/providers/list/theflix/index.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { - MWMediaProvider, - MWMediaType, - MWPortableMedia, - MWMediaStream, - MWQuery, - MWMediaSeasons, - MWProviderMediaResult, -} from "@/providers/types"; - -import { - searchTheFlix, - getDataFromSearch, - turnDataIntoMedia, -} from "@/providers/list/theflix/search"; - -import { getDataFromPortableSearch } from "@/providers/list/theflix/portableToMedia"; -import { conf } from "@/setup/config"; - -export const theFlixScraper: MWMediaProvider = { - id: "theflix", - enabled: false, - type: [MWMediaType.MOVIE, MWMediaType.SERIES], - displayName: "theflix", - - async getMediaFromPortable( - media: MWPortableMedia - ): Promise { - const data: any = await getDataFromPortableSearch(media); - - return { - ...media, - year: new Date(data.releaseDate).getFullYear().toString(), - title: data.name, - }; - }, - - async searchForMedia(query: MWQuery): Promise { - const searchRes = await searchTheFlix(query); - const searchData = await getDataFromSearch(searchRes, 10); - - const results: MWProviderMediaResult[] = []; - for (const item of searchData) { - results.push(turnDataIntoMedia(item)); - } - - return results; - }, - - async getStream(media: MWPortableMedia): Promise { - let url = ""; - - if (media.mediaType === MWMediaType.MOVIE) { - url = `${conf().CORS_PROXY_URL}https://theflix.to/movie/${ - media.mediaId - }?movieInfo=${media.mediaId}`; - } else if (media.mediaType === MWMediaType.SERIES) { - url = `${conf().CORS_PROXY_URL}https://theflix.to/tv-show/${ - media.mediaId - }/season-${media.seasonId}/episode-${media.episodeId}`; - } - - const res = await fetch(url).then((d) => d.text()); - - const prop: HTMLElement | undefined = Array.from( - new DOMParser() - .parseFromString(res, "text/html") - .querySelectorAll("script") - ).find((e) => e.textContent?.includes("theflixvd.b-cdn")); - - if (!prop || !prop.textContent) { - throw new Error("Could not find stream"); - } - - const data = JSON.parse(prop.textContent); - - return { url: data.props.pageProps.videoUrl, type: "mp4", captions: [] }; - }, - - async getSeasonDataFromMedia( - media: MWPortableMedia - ): Promise { - const url = `${conf().CORS_PROXY_URL}https://theflix.to/tv-show/${ - media.mediaId - }/season-${media.seasonId}/episode-${media.episodeId}`; - const res = await fetch(url).then((d) => d.text()); - - const node: Element = Array.from( - new DOMParser() - .parseFromString(res, "text/html") - .querySelectorAll(`script[id="__NEXT_DATA__"]`) - )[0]; - - let data = JSON.parse(node.innerHTML).props.pageProps.selectedTv.seasons; - - data = data.filter((season: any) => season.releaseDate != null); - data = data.map((season: any) => { - const episodes = season.episodes.filter( - (episode: any) => episode.releaseDate != null - ); - return { ...season, episodes }; - }); - - return { - seasons: data.map((d: any) => ({ - sort: d.seasonNumber === 0 ? 999 : d.seasonNumber, - id: d.seasonNumber.toString(), - type: d.seasonNumber === 0 ? "special" : "season", - title: d.name, - episodes: d.episodes.map((e: any) => ({ - title: e.name, - sort: e.episodeNumber, - id: e.episodeNumber.toString(), - episodeNumber: e.episodeNumber, - })), - })), - }; - }, -}; diff --git a/src/providers/list/theflix/portableToMedia.ts b/src/providers/list/theflix/portableToMedia.ts deleted file mode 100644 index 4f42dd47..00000000 --- a/src/providers/list/theflix/portableToMedia.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { conf } from "@/setup/config"; -import { MWMediaType, MWPortableMedia } from "@/providers/types"; - -const getTheFlixUrl = (media: MWPortableMedia, params?: URLSearchParams) => { - if (media.mediaType === MWMediaType.MOVIE) { - return `https://theflix.to/movie/${media.mediaId}?${params}`; - } - if (media.mediaType === MWMediaType.SERIES) { - return `https://theflix.to/tv-show/${media.mediaId}/season-${media.seasonId}/episode-${media.episodeId}`; - } - - return ""; -}; - -export async function getDataFromPortableSearch( - media: MWPortableMedia -): Promise { - const params = new URLSearchParams(); - params.append("movieInfo", media.mediaId); - - const res = await fetch( - conf().CORS_PROXY_URL + getTheFlixUrl(media, params) - ).then((d) => d.text()); - - const node: Element = Array.from( - new DOMParser() - .parseFromString(res, "text/html") - .querySelectorAll(`script[id="__NEXT_DATA__"]`) - )[0]; - - if (media.mediaType === MWMediaType.MOVIE) { - return JSON.parse(node.innerHTML).props.pageProps.movie; - } - // must be series here - return JSON.parse(node.innerHTML).props.pageProps.selectedTv; -} diff --git a/src/providers/list/theflix/search.ts b/src/providers/list/theflix/search.ts deleted file mode 100644 index aa575194..00000000 --- a/src/providers/list/theflix/search.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { conf } from "@/setup/config"; -import { MWMediaType, MWProviderMediaResult, MWQuery } from "@/providers"; - -const getTheFlixUrl = (type: "tv-shows" | "movies", params: URLSearchParams) => - `https://theflix.to/${type}/trending?${params}`; - -export function searchTheFlix(query: MWQuery): Promise { - const params = new URLSearchParams(); - params.append("search", query.searchQuery); - return fetch( - conf().CORS_PROXY_URL + - getTheFlixUrl( - query.type === MWMediaType.MOVIE ? "movies" : "tv-shows", - params - ) - ).then((d) => d.text()); -} - -export function getDataFromSearch(page: string, limit = 10): any[] { - const node: Element = Array.from( - new DOMParser() - .parseFromString(page, "text/html") - .querySelectorAll(`script[id="__NEXT_DATA__"]`) - )[0]; - const data = JSON.parse(node.innerHTML); - return data.props.pageProps.mainList.docs - .filter((d: any) => d.available) - .slice(0, limit); -} - -export function turnDataIntoMedia(data: any): MWProviderMediaResult { - return { - mediaId: `${data.id}-${data.name - .replace(/[^a-z0-9]+|\s+/gim, " ") - .trim() - .replace(/\s+/g, "-") - .toLowerCase()}`, - title: data.name, - year: new Date(data.releaseDate).getFullYear().toString(), - seasonCount: data.numberOfSeasons, - episodeId: data.lastReleasedEpisode - ? data.lastReleasedEpisode.episodeNumber.toString() - : null, - seasonId: data.lastReleasedEpisode - ? data.lastReleasedEpisode.seasonNumber.toString() - : null, - }; -} diff --git a/src/providers/list/xemovie/index.ts b/src/providers/list/xemovie/index.ts deleted file mode 100644 index 82df6848..00000000 --- a/src/providers/list/xemovie/index.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { - MWMediaProvider, - MWMediaType, - MWPortableMedia, - MWMediaStream, - MWQuery, - MWProviderMediaResult, - MWMediaCaption, -} from "@/providers/types"; - -import { conf } from "@/setup/config"; - -export const xemovieScraper: MWMediaProvider = { - id: "xemovie", - enabled: false, - type: [MWMediaType.MOVIE], - displayName: "xemovie", - - async getMediaFromPortable( - media: MWPortableMedia - ): Promise { - const res = await fetch( - `${conf().CORS_PROXY_URL}https://xemovie.co/movies/${media.mediaId}/watch` - ).then((d) => d.text()); - - const DOM = new DOMParser().parseFromString(res, "text/html"); - - const title = - DOM.querySelector(".text-primary.text-lg.font-extrabold")?.textContent || - ""; - const year = - DOM.querySelector("div.justify-between:nth-child(3) > div:nth-child(2)") - ?.textContent || ""; - - return { - ...media, - title, - year, - } as MWProviderMediaResult; - }, - - async searchForMedia(query: MWQuery): Promise { - const term = query.searchQuery.toLowerCase(); - - const searchUrl = `${ - conf().CORS_PROXY_URL - }https://xemovie.co/search?q=${encodeURIComponent(term)}`; - const searchRes = await fetch(searchUrl).then((d) => d.text()); - - const parser = new DOMParser(); - const doc = parser.parseFromString(searchRes, "text/html"); - - const movieContainer = doc - .querySelectorAll(".py-10")[0] - .querySelector(".grid"); - if (!movieContainer) return []; - const movieNodes = Array.from(movieContainer.querySelectorAll("a")).filter( - (link) => !link.className - ); - - const results: MWProviderMediaResult[] = movieNodes - .map((node) => { - const parent = node.parentElement; - if (!parent) return; - - const aElement = parent.querySelector("a"); - if (!aElement) return; - - return { - title: parent.querySelector("div > div > a > h6")?.textContent, - year: parent.querySelector("div.float-right")?.textContent, - mediaId: aElement.href.split("/").pop() || "", - }; - }) - .filter((d): d is MWProviderMediaResult => !!d); - - return results; - }, - - async getStream(media: MWPortableMedia): Promise { - if (media.mediaType !== MWMediaType.MOVIE) - throw new Error("Incorrect type"); - - const url = `${conf().CORS_PROXY_URL}https://xemovie.co/movies/${ - media.mediaId - }/watch`; - - let streamUrl = ""; - const subtitles: MWMediaCaption[] = []; - - const res = await fetch(url).then((d) => d.text()); - const scripts = Array.from( - new DOMParser() - .parseFromString(res, "text/html") - .querySelectorAll("script") - ); - - for (const script of scripts) { - if (!script.textContent) continue; - - if (script.textContent.match(/https:\/\/[a-z][0-9]\.xemovie\.com/)) { - const data = JSON.parse( - JSON.stringify( - eval( - `(${ - script.textContent.replace("const data = ", "").split("};")[0] - }})` - ) - ) - ); - streamUrl = data.playlist[0].file; - - for (const [ - index, - subtitleTrack, - ] of data.playlist[0].tracks.entries()) { - const subtitleBlob = URL.createObjectURL( - await fetch(`${conf().CORS_PROXY_URL}${subtitleTrack.file}`).then( - (captionRes) => captionRes.blob() - ) - ); // do this so no need for CORS errors - - subtitles.push({ - id: index, - url: subtitleBlob, - label: subtitleTrack.label, - }); - } - } - } - - const streamType = streamUrl.split(".").at(-1); - if (streamType !== "mp4" && streamType !== "m3u8") - throw new Error("Unsupported stream type"); - - return { - url: streamUrl, - type: streamType, - captions: subtitles, - } as MWMediaStream; - }, -}; diff --git a/src/providers/methods/contentCache.ts b/src/providers/methods/contentCache.ts deleted file mode 100644 index c2e43a7b..00000000 --- a/src/providers/methods/contentCache.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { SimpleCache } from "@/utils/cache"; -import { MWPortableMedia, MWMedia } from "@/providers"; - -// cache -const contentCache = new SimpleCache(); -contentCache.setCompare( - (a, b) => a.mediaId === b.mediaId && a.providerId === b.providerId -); -contentCache.initialize(); - -export default contentCache; diff --git a/src/providers/methods/helpers.ts b/src/providers/methods/helpers.ts deleted file mode 100644 index 4ab75794..00000000 --- a/src/providers/methods/helpers.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { MWMediaType, MWMediaProviderMetadata } from "@/providers"; -import { MWMedia, MWMediaEpisode, MWMediaSeason } from "@/providers/types"; -import { mediaProviders, mediaProvidersUnchecked } from "./providers"; - -/* - ** Fetch all enabled providers for a specific type - */ -export function GetProvidersForType(type: MWMediaType) { - return mediaProviders.filter((v) => v.type.includes(type)); -} - -/* - ** Get a provider by a id - */ -export function getProviderFromId(id: string) { - return mediaProviders.find((v) => v.id === id); -} - -/* - ** Get a provider metadata - */ -export function getProviderMetadata(id: string): MWMediaProviderMetadata { - const provider = mediaProvidersUnchecked.find((v) => v.id === id); - - if (!provider) { - return { - exists: false, - type: [], - enabled: false, - id, - }; - } - - return { - exists: true, - type: provider.type, - enabled: provider.enabled, - id, - provider, - }; -} - -/* - ** get episode and season from media - */ -export function getEpisodeFromMedia( - media: MWMedia -): { season: MWMediaSeason; episode: MWMediaEpisode } | null { - if ( - media.seasonId === undefined || - media.episodeId === undefined || - media.seriesData === undefined - ) { - return null; - } - - const season = media.seriesData.seasons.find((v) => v.id === media.seasonId); - if (!season) return null; - const episode = season?.episodes.find((v) => v.id === media.episodeId); - if (!episode) return null; - return { - season, - episode, - }; -} diff --git a/src/providers/methods/providers.ts b/src/providers/methods/providers.ts deleted file mode 100644 index c7d68875..00000000 --- a/src/providers/methods/providers.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { theFlixScraper } from "@/providers/list/theflix"; -import { gDrivePlayerScraper } from "@/providers/list/gdriveplayer"; -import { MWWrappedMediaProvider, WrapProvider } from "@/providers/wrapper"; -import { gomostreamScraper } from "@/providers/list/gomostream"; -import { xemovieScraper } from "@/providers/list/xemovie"; -import { flixhqProvider } from "@/providers/list/flixhq"; -import { superStreamScraper } from "@/providers/list/superstream"; - -export const mediaProvidersUnchecked: MWWrappedMediaProvider[] = [ - WrapProvider(superStreamScraper), - WrapProvider(theFlixScraper), - WrapProvider(gDrivePlayerScraper), - WrapProvider(gomostreamScraper), - WrapProvider(xemovieScraper), - WrapProvider(flixhqProvider), -]; - -export const mediaProviders: MWWrappedMediaProvider[] = - mediaProvidersUnchecked.filter((v) => v.enabled); diff --git a/src/providers/methods/search.ts b/src/providers/methods/search.ts deleted file mode 100644 index 856fc0fa..00000000 --- a/src/providers/methods/search.ts +++ /dev/null @@ -1,105 +0,0 @@ -import Fuse from "fuse.js"; -import { - MWMassProviderOutput, - MWMedia, - MWQuery, - convertMediaToPortable, -} from "@/providers"; -import { SimpleCache } from "@/utils/cache"; -import { GetProvidersForType } from "./helpers"; -import contentCache from "./contentCache"; - -// cache -const resultCache = new SimpleCache(); -resultCache.setCompare( - (a, b) => a.searchQuery === b.searchQuery && a.type === b.type -); -resultCache.initialize(); - -/* - ** actually call all providers with the search query - */ -async function callProviders(query: MWQuery): Promise { - const allQueries = GetProvidersForType(query.type).map< - Promise<{ media: MWMedia[]; success: boolean; id: string }> - >(async (provider) => { - try { - return { - media: await provider.searchForMedia(query), - success: true, - id: provider.id, - }; - } catch (err) { - console.error(`Failed running provider ${provider.id}`, err, query); - return { - media: [], - success: false, - id: provider.id, - }; - } - }); - const allResults = await Promise.all(allQueries); - const providerResults = allResults.map((provider) => ({ - success: provider.success, - id: provider.id, - })); - const output: MWMassProviderOutput = { - results: allResults.flatMap((results) => results.media), - providers: providerResults, - stats: { - total: providerResults.length, - failed: providerResults.filter((v) => !v.success).length, - succeeded: providerResults.filter((v) => v.success).length, - }, - }; - - // save in cache if all successfull - if (output.stats.failed === 0) { - resultCache.set(query, output, 60 * 60); // cache for an hour - } - - output.results.forEach((result: MWMedia) => { - contentCache.set(convertMediaToPortable(result), result, 60 * 60); - }); - - return output; -} - -/* - ** sort results based on query - */ -function sortResults( - query: MWQuery, - providerResults: MWMassProviderOutput -): MWMassProviderOutput { - const results: MWMassProviderOutput = { ...providerResults }; - const fuse = new Fuse(results.results, { - threshold: 0.3, - keys: ["title"], - fieldNormWeight: 0.5, - }); - results.results = fuse.search(query.searchQuery).map((v) => v.item); - return results; -} - -/* - ** Call search on all providers that matches query type - */ -export async function SearchProviders( - inputQuery: MWQuery -): Promise { - // input normalisation - const query = { ...inputQuery }; - query.searchQuery = query.searchQuery.toLowerCase().trim(); - - // consult cache first - let output = resultCache.get(query); - if (!output) output = await callProviders(query); - - // sort results - output = sortResults(query, output); - - if (output.stats.total === output.stats.failed) - throw new Error("All Scrapers failed"); - return output; -} diff --git a/src/providers/methods/seasons.ts b/src/providers/methods/seasons.ts deleted file mode 100644 index a9e996be..00000000 --- a/src/providers/methods/seasons.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { SimpleCache } from "@/utils/cache"; -import { MWPortableMedia } from "@/providers"; -import { - MWMediaSeasons, - MWMediaType, - MWMediaProviderSeries, -} from "@/providers/types"; -import { getProviderFromId } from "./helpers"; - -// cache -const seasonCache = new SimpleCache(); -seasonCache.setCompare( - (a, b) => a.mediaId === b.mediaId && a.providerId === b.providerId -); -seasonCache.initialize(); - -/* - ** get season data from a (portable) media object, seasons and episodes will be sorted - */ -export async function getSeasonDataFromMedia( - media: MWPortableMedia -): Promise { - const provider = getProviderFromId(media.providerId) as MWMediaProviderSeries; - if (!provider) { - return { - seasons: [], - }; - } - - if ( - !provider.type.includes(MWMediaType.SERIES) && - !provider.type.includes(MWMediaType.ANIME) - ) { - return { - seasons: [], - }; - } - - if (seasonCache.has(media)) { - return seasonCache.get(media) as MWMediaSeasons; - } - - const seasonData = await provider.getSeasonDataFromMedia(media); - seasonData.seasons.sort((a, b) => a.sort - b.sort); - seasonData.seasons.forEach((s) => s.episodes.sort((a, b) => a.sort - b.sort)); - - // cache it - seasonCache.set(media, seasonData, 60 * 60); // cache it for an hour - return seasonData; -} diff --git a/src/providers/types.ts b/src/providers/types.ts deleted file mode 100644 index 37861633..00000000 --- a/src/providers/types.ts +++ /dev/null @@ -1,97 +0,0 @@ -export enum MWMediaType { - MOVIE = "movie", - SERIES = "series", - ANIME = "anime", -} - -export interface MWPortableMedia { - mediaId: string; - mediaType: MWMediaType; - providerId: string; - seasonId?: string; - episodeId?: string; -} - -export type MWMediaStreamType = "m3u8" | "mp4"; -export interface MWMediaCaption { - id: string; - url: string; - label: string; -} -export interface MWMediaStream { - url: string; - type: MWMediaStreamType; - captions: MWMediaCaption[]; -} - -export interface MWMediaMeta extends MWPortableMedia { - title: string; - year: string; - seasonCount?: number; -} - -export interface MWMediaEpisode { - sort: number; - id: string; - title: string; -} -export interface MWMediaSeason { - sort: number; - id: string; - title?: string; - type: "season" | "special"; - episodes: MWMediaEpisode[]; -} -export interface MWMediaSeasons { - seasons: MWMediaSeason[]; -} - -export interface MWMedia extends MWMediaMeta { - seriesData?: MWMediaSeasons; -} - -export type MWProviderMediaResult = Omit; - -export interface MWQuery { - searchQuery: string; - type: MWMediaType; -} - -export interface MWMediaProviderBase { - id: string; // id of provider, must be unique - enabled: boolean; - type: MWMediaType[]; - displayName: string; - - getMediaFromPortable(media: MWPortableMedia): Promise; - searchForMedia(query: MWQuery): Promise; - getStream(media: MWPortableMedia): Promise; - getSeasonDataFromMedia?: (media: MWPortableMedia) => Promise; -} - -export type MWMediaProviderSeries = MWMediaProviderBase & { - getSeasonDataFromMedia: (media: MWPortableMedia) => Promise; -}; - -export type MWMediaProvider = MWMediaProviderBase; - -export interface MWMediaProviderMetadata { - exists: boolean; - id?: string; - enabled: boolean; - type: MWMediaType[]; - provider?: MWMediaProvider; -} - -export interface MWMassProviderOutput { - providers: { - id: string; - success: boolean; - }[]; - results: MWMedia[]; - stats: { - total: number; - failed: number; - succeeded: number; - }; -} diff --git a/src/providers/wrapper.ts b/src/providers/wrapper.ts deleted file mode 100644 index 35727d97..00000000 --- a/src/providers/wrapper.ts +++ /dev/null @@ -1,48 +0,0 @@ -import contentCache from "./methods/contentCache"; -import { - MWMedia, - MWMediaProvider, - MWMediaStream, - MWPortableMedia, - MWQuery, -} from "./types"; - -export interface MWWrappedMediaProvider extends MWMediaProvider { - getMediaFromPortable(media: MWPortableMedia): Promise; - searchForMedia(query: MWQuery): Promise; - getStream(media: MWPortableMedia): Promise; -} - -export function WrapProvider( - provider: MWMediaProvider -): MWWrappedMediaProvider { - return { - ...provider, - - async getMediaFromPortable(media: MWPortableMedia): Promise { - // consult cache first - const output = contentCache.get(media); - if (output) { - output.seasonId = media.seasonId; - output.episodeId = media.episodeId; - return output; - } - - const mediaObject = { - ...(await provider.getMediaFromPortable(media)), - providerId: provider.id, - mediaType: media.mediaType, - }; - contentCache.set(media, mediaObject, 60 * 60); - return mediaObject; - }, - - async searchForMedia(query: MWQuery): Promise { - return (await provider.searchForMedia(query)).map((m) => ({ - ...m, - providerId: provider.id, - mediaType: query.type, - })); - }, - }; -} From 094f9208a89601dcef5705a99a3137df5aad8781 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Wed, 11 Jan 2023 23:41:27 +0100 Subject: [PATCH 026/135] add quality to streams --- src/backend/embeds/testEmbedScraper.ts | 3 ++- src/backend/helpers/streams.ts | 6 ++++++ src/backend/providers/testProvider.ts | 3 ++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/backend/embeds/testEmbedScraper.ts b/src/backend/embeds/testEmbedScraper.ts index 8498c0a2..b97c7f05 100644 --- a/src/backend/embeds/testEmbedScraper.ts +++ b/src/backend/embeds/testEmbedScraper.ts @@ -1,6 +1,6 @@ import { MWEmbedType } from "../helpers/embed"; import { registerEmbedScraper } from "../helpers/register"; -import { MWStreamType } from "../helpers/streams"; +import { MWStreamQuality, MWStreamType } from "../helpers/streams"; registerEmbedScraper({ id: "testembed", @@ -15,6 +15,7 @@ registerEmbedScraper({ return { streamUrl: "hello-world", type: MWStreamType.MP4, + quality: MWStreamQuality.Q1080P, }; }, }); diff --git a/src/backend/helpers/streams.ts b/src/backend/helpers/streams.ts index 6eb7257b..abcd9adc 100644 --- a/src/backend/helpers/streams.ts +++ b/src/backend/helpers/streams.ts @@ -3,7 +3,13 @@ export enum MWStreamType { HLS = "hls", } +export enum MWStreamQuality { + Q360P = "360p", + Q1080P = "1080p", +} + export type MWStream = { streamUrl: string; type: MWStreamType; + quality: MWStreamQuality; }; diff --git a/src/backend/providers/testProvider.ts b/src/backend/providers/testProvider.ts index 455325ca..21376d29 100644 --- a/src/backend/providers/testProvider.ts +++ b/src/backend/providers/testProvider.ts @@ -1,6 +1,6 @@ import { MWEmbedType } from "../helpers/embed"; import { registerProvider } from "../helpers/register"; -import { MWStreamType } from "../helpers/streams"; +import { MWStreamQuality, MWStreamType } from "../helpers/streams"; import { MWMediaType } from "../metadata/types"; registerProvider({ @@ -20,6 +20,7 @@ registerProvider({ stream: { streamUrl: "hello-world", type: MWStreamType.HLS, + quality: MWStreamQuality.Q1080P, }, embeds: [ { From a9ac3e64dbd3f30e558afa448a4ff74d8d97291f Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Thu, 12 Jan 2023 22:04:28 +0100 Subject: [PATCH 027/135] add provider scrape hookiboi --- src/backend/embeds/testEmbedScraper.ts | 13 +- src/backend/embeds/testEmbedScraperTwo.ts | 24 + src/backend/helpers/embed.ts | 1 + src/backend/helpers/register.ts | 17 +- src/backend/helpers/run.ts | 52 +++ src/backend/helpers/scrape.ts | 117 +++++ src/backend/index.ts | 4 +- src/backend/metadata/search.ts | 3 +- src/backend/metadata/types.ts | 5 + src/backend/providers/testProvider.ts | 33 +- src/backend/providers/testProviderTwo.ts | 37 ++ src/components/SearchBar.tsx | 2 +- src/components/layout/Seasons.tsx | 155 +++---- src/components/media/MediaCard.tsx | 5 +- src/components/media/WatchedEpisodeButton.tsx | 29 +- src/components/media/WatchedMediaCard.tsx | 4 +- src/hooks/usePortableMedia.ts | 30 -- src/hooks/useScrape.ts | 59 +++ src/hooks/useSearchQuery.ts | 2 +- src/setup/App.tsx | 2 +- src/state/bookmark/context.tsx | 8 +- src/state/watched/context.tsx | 129 +++--- src/state/watched/store.ts | 140 +++--- src/views/MediaView.tsx | 419 +++++++++--------- src/views/TestView.tsx | 90 ++-- src/views/notfound/NotFoundChecks.tsx | 23 +- src/views/search/HomeView.tsx | 9 +- src/views/search/SearchResultsPartial.tsx | 2 +- src/views/search/SearchResultsView.tsx | 6 +- 29 files changed, 847 insertions(+), 573 deletions(-) create mode 100644 src/backend/embeds/testEmbedScraperTwo.ts create mode 100644 src/backend/helpers/run.ts create mode 100644 src/backend/helpers/scrape.ts create mode 100644 src/backend/providers/testProviderTwo.ts delete mode 100644 src/hooks/usePortableMedia.ts create mode 100644 src/hooks/useScrape.ts diff --git a/src/backend/embeds/testEmbedScraper.ts b/src/backend/embeds/testEmbedScraper.ts index b97c7f05..8da3786f 100644 --- a/src/backend/embeds/testEmbedScraper.ts +++ b/src/backend/embeds/testEmbedScraper.ts @@ -2,16 +2,25 @@ import { MWEmbedType } from "../helpers/embed"; import { registerEmbedScraper } from "../helpers/register"; import { MWStreamQuality, MWStreamType } from "../helpers/streams"; +const timeout = (time: number) => + new Promise((resolve) => { + setTimeout(() => resolve(), time); + }); + registerEmbedScraper({ id: "testembed", rank: 23, for: MWEmbedType.OPENLOAD, - async getStream({ progress, url }) { - console.log("scraping url: ", url); + async getStream({ progress }) { + await timeout(1000); progress(25); + await timeout(1000); progress(50); + await timeout(1000); progress(75); + throw new Error("failed to load or something"); + await timeout(1000); return { streamUrl: "hello-world", type: MWStreamType.MP4, diff --git a/src/backend/embeds/testEmbedScraperTwo.ts b/src/backend/embeds/testEmbedScraperTwo.ts new file mode 100644 index 00000000..7d409d98 --- /dev/null +++ b/src/backend/embeds/testEmbedScraperTwo.ts @@ -0,0 +1,24 @@ +import { MWEmbedType } from "../helpers/embed"; +import { registerEmbedScraper } from "../helpers/register"; +import { MWStreamQuality, MWStreamType } from "../helpers/streams"; + +const timeout = (time: number) => + new Promise((resolve) => { + setTimeout(() => resolve(), time); + }); + +registerEmbedScraper({ + id: "testembedtwo", + rank: 19, + for: MWEmbedType.ANOTHER, + + async getStream({ progress }) { + progress(75); + await timeout(1000); + return { + streamUrl: "hello-world-5", + type: MWStreamType.MP4, + quality: MWStreamQuality.Q1080P, + }; + }, +}); diff --git a/src/backend/helpers/embed.ts b/src/backend/helpers/embed.ts index 20cd3b29..88c0420d 100644 --- a/src/backend/helpers/embed.ts +++ b/src/backend/helpers/embed.ts @@ -2,6 +2,7 @@ import { MWStream } from "./streams"; export enum MWEmbedType { OPENLOAD = "openload", + ANOTHER = "another", } export type MWEmbed = { diff --git a/src/backend/helpers/register.ts b/src/backend/helpers/register.ts index 001aed16..9d3f76c2 100644 --- a/src/backend/helpers/register.ts +++ b/src/backend/helpers/register.ts @@ -1,4 +1,4 @@ -import { MWEmbedScraper } from "./embed"; +import { MWEmbedScraper, MWEmbedType } from "./embed"; import { MWProvider } from "./provider"; let providers: MWProvider[] = []; @@ -15,8 +15,8 @@ export function registerEmbedScraper(embed: MWEmbedScraper) { export function initializeScraperStore() { // sort by ranking - providers = providers.sort((a, b) => a.rank - b.rank); - embeds = embeds.sort((a, b) => a.rank - b.rank); + providers = providers.sort((a, b) => b.rank - a.rank); + embeds = embeds.sort((a, b) => b.rank - a.rank); // check for invalid ranks let lastRank: null | number = null; @@ -50,6 +50,11 @@ export function initializeScraperStore() { const embedIds = embeds.map((v) => v.id); if (embedIds.length > 0 && new Set(embedIds).size !== embedIds.length) throw new Error("Duplicate IDS in embed scrapers"); + + // check for duplicate embed types + const embedTypes = embeds.map((v) => v.for); + if (embedTypes.length > 0 && new Set(embedTypes).size !== embedTypes.length) + throw new Error("Duplicate types in embed scrapers"); } export function getProviders(): MWProvider[] { @@ -59,3 +64,9 @@ export function getProviders(): MWProvider[] { export function getEmbeds(): MWEmbedScraper[] { return embeds; } + +export function getEmbedScraperByType( + type: MWEmbedType +): MWEmbedScraper | null { + return getEmbeds().find((v) => v.for === type) ?? null; +} diff --git a/src/backend/helpers/run.ts b/src/backend/helpers/run.ts new file mode 100644 index 00000000..6b3a548d --- /dev/null +++ b/src/backend/helpers/run.ts @@ -0,0 +1,52 @@ +import { MWEmbed, MWEmbedContext, MWEmbedScraper } from "./embed"; +import { + MWProvider, + MWProviderContext, + MWProviderScrapeResult, +} from "./provider"; +import { getEmbedScraperByType } from "./register"; +import { MWStream } from "./streams"; + +function sortProviderResult( + ctx: MWProviderScrapeResult +): MWProviderScrapeResult { + ctx.embeds = ctx.embeds + .map<[MWEmbed, MWEmbedScraper | null]>((v) => [ + v, + v.type ? getEmbedScraperByType(v.type) : null, + ]) + .sort(([, a], [, b]) => (b?.rank ?? 0) - (a?.rank ?? 0)) + .map((v) => v[0]); + return ctx; +} + +export async function runProvider( + provider: MWProvider, + ctx: MWProviderContext +): Promise { + try { + const data = await provider.scrape(ctx); + return sortProviderResult(data); + } catch (err) { + console.error("Failed to run provider", { + id: provider.id, + ctx: { ...ctx }, + }); + throw err; + } +} + +export async function runEmbedScraper( + scraper: MWEmbedScraper, + ctx: MWEmbedContext +): Promise { + try { + return await scraper.getStream(ctx); + } catch (err) { + console.error("Failed to run embed scraper", { + id: scraper.id, + ctx: { ...ctx }, + }); + throw err; + } +} diff --git a/src/backend/helpers/scrape.ts b/src/backend/helpers/scrape.ts new file mode 100644 index 00000000..fe0dc61b --- /dev/null +++ b/src/backend/helpers/scrape.ts @@ -0,0 +1,117 @@ +import { MWProviderScrapeResult } from "./provider"; +import { getEmbedScraperByType, getProviders } from "./register"; +import { runEmbedScraper, runProvider } from "./run"; +import { MWStream } from "./streams"; + +interface MWProgressData { + type: "embed" | "provider"; + id: string; + percentage: number; + errored: boolean; +} +interface MWNextData { + id: string; + type: "embed" | "provider"; +} + +export interface MWProviderRunContext { + tmdb: string; + imdb: string; + onProgress?: (data: MWProgressData) => void; + onNext?: (data: MWNextData) => void; +} + +async function findBestEmbedStream( + result: MWProviderScrapeResult, + ctx: MWProviderRunContext +): Promise { + if (result.stream) return result.stream; + + for (const embed of result.embeds) { + if (!embed.type) continue; + const scraper = getEmbedScraperByType(embed.type); + if (!scraper) throw new Error("Type for embed not found"); + + ctx.onNext?.({ id: scraper.id, type: "embed" }); + + let stream: MWStream; + try { + stream = await runEmbedScraper(scraper, { + url: embed.url, + progress(num) { + ctx.onProgress?.({ + errored: false, + id: scraper.id, + percentage: num, + type: "embed", + }); + }, + }); + } catch { + ctx.onProgress?.({ + errored: true, + id: scraper.id, + percentage: 100, + type: "embed", + }); + continue; + } + + ctx.onProgress?.({ + errored: false, + id: scraper.id, + percentage: 100, + type: "embed", + }); + + return stream; + } + + return null; +} + +export async function findBestStream( + ctx: MWProviderRunContext +): Promise { + const providers = getProviders(); + + for (const provider of providers) { + ctx.onNext?.({ id: provider.id, type: "provider" }); + let result: MWProviderScrapeResult; + try { + result = await runProvider(provider, { + imdbId: ctx.imdb, + tmdbId: ctx.tmdb, + progress(num) { + ctx.onProgress?.({ + percentage: num, + errored: false, + id: provider.id, + type: "provider", + }); + }, + }); + } catch (err) { + ctx.onProgress?.({ + percentage: 100, + errored: true, + id: provider.id, + type: "provider", + }); + continue; + } + + ctx.onProgress?.({ + errored: false, + id: provider.id, + percentage: 100, + type: "provider", + }); + + const stream = await findBestEmbedStream(result, ctx); + if (!stream) continue; + return stream; + } + + return null; +} diff --git a/src/backend/index.ts b/src/backend/index.ts index 34983fad..1384e109 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -1,7 +1,6 @@ import { initializeScraperStore } from "./helpers/register"; // TODO backend system: -// - run providers/embedscrapers in webworkers for multithreading and isolation // - caption support // - hooks to run all providers one by one // - move over old providers to new system @@ -10,8 +9,11 @@ import { initializeScraperStore } from "./helpers/register"; // providers // -- nothing here yet import "./providers/testProvider"; +import "./providers/testProviderTwo"; // embeds // -- nothing here yet +import "./embeds/testEmbedScraper"; +import "./embeds/testEmbedScraperTwo"; initializeScraperStore(); diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index fa26b35d..0b0964a7 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -1,5 +1,4 @@ -import { MWMediaType, MWQuery } from "@/providers"; -import { MWMediaMeta } from "./types"; +import { MWMediaMeta, MWMediaType, MWQuery } from "./types"; const JW_API_BASE = "https://apis.justwatch.com"; diff --git a/src/backend/metadata/types.ts b/src/backend/metadata/types.ts index a74e0520..afabb970 100644 --- a/src/backend/metadata/types.ts +++ b/src/backend/metadata/types.ts @@ -11,3 +11,8 @@ export type MWMediaMeta = { poster?: string; type: MWMediaType; }; + +export interface MWQuery { + searchQuery: string; + type: MWMediaType; +} diff --git a/src/backend/providers/testProvider.ts b/src/backend/providers/testProvider.ts index 21376d29..c00615c8 100644 --- a/src/backend/providers/testProvider.ts +++ b/src/backend/providers/testProvider.ts @@ -1,32 +1,35 @@ -import { MWEmbedType } from "../helpers/embed"; import { registerProvider } from "../helpers/register"; -import { MWStreamQuality, MWStreamType } from "../helpers/streams"; import { MWMediaType } from "../metadata/types"; +const timeout = (time: number) => + new Promise((resolve) => { + setTimeout(() => resolve(), time); + }); + registerProvider({ id: "testprov", rank: 42, type: [MWMediaType.MOVIE], - async scrape({ progress, imdbId, tmdbId }) { - console.log("scraping provider for: ", imdbId, tmdbId); + async scrape({ progress }) { + await timeout(1000); progress(25); + await timeout(1000); progress(50); + await timeout(1000); progress(75); + await timeout(1000); - // providers can optionally provide a stream themselves, - // incase they host their own streams instead of using embeds return { - stream: { - streamUrl: "hello-world", - type: MWStreamType.HLS, - quality: MWStreamQuality.Q1080P, - }, embeds: [ - { - type: MWEmbedType.OPENLOAD, - url: "https://google.com", - }, + // { + // type: MWEmbedType.OPENLOAD, + // url: "https://google.com", + // }, + // { + // type: MWEmbedType.ANOTHER, + // url: "https://google.com", + // }, ], }; }, diff --git a/src/backend/providers/testProviderTwo.ts b/src/backend/providers/testProviderTwo.ts new file mode 100644 index 00000000..f3a39fbf --- /dev/null +++ b/src/backend/providers/testProviderTwo.ts @@ -0,0 +1,37 @@ +import { MWEmbedType } from "../helpers/embed"; +import { registerProvider } from "../helpers/register"; +import { MWMediaType } from "../metadata/types"; + +const timeout = (time: number) => + new Promise((resolve) => { + setTimeout(() => resolve(), time); + }); + +registerProvider({ + id: "testprov2", + rank: 40, + type: [MWMediaType.MOVIE], + + async scrape({ progress }) { + await timeout(1000); + progress(25); + await timeout(1000); + progress(50); + await timeout(1000); + progress(75); + await timeout(1000); + + return { + embeds: [ + { + type: MWEmbedType.OPENLOAD, + url: "https://google.com", + }, + { + type: MWEmbedType.ANOTHER, + url: "https://google.com", + }, + ], + }; + }, +}); diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index df844c83..8e97138d 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -1,6 +1,6 @@ +import { MWMediaType, MWQuery } from "@/backend/metadata/types"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { MWMediaType, MWQuery } from "@/providers"; import { DropdownButton } from "./buttons/DropdownButton"; import { Icon, Icons } from "./Icon"; import { TextInputControl } from "./text-inputs/TextInputControl"; diff --git a/src/components/layout/Seasons.tsx b/src/components/layout/Seasons.tsx index f2416372..1f31a1b9 100644 --- a/src/components/layout/Seasons.tsx +++ b/src/components/layout/Seasons.tsx @@ -7,17 +7,9 @@ import { Icons } from "@/components/Icon"; import { WatchedEpisode } from "@/components/media/WatchedEpisodeButton"; import { useLoading } from "@/hooks/useLoading"; import { serializePortableMedia } from "@/hooks/usePortableMedia"; -import { - convertMediaToPortable, - MWMedia, - MWMediaSeasons, - MWMediaSeason, - MWPortableMedia, -} from "@/providers"; -import { getSeasonDataFromMedia } from "@/providers/methods/seasons"; export interface SeasonsProps { - media: MWMedia; + media: any; } export function LoadingSeasons(props: { error?: boolean }) { @@ -45,80 +37,73 @@ export function LoadingSeasons(props: { error?: boolean }) { } export function Seasons(props: SeasonsProps) { - const { t } = useTranslation(); - - const [searchSeasons, loading, error, success] = useLoading( - (portableMedia: MWPortableMedia) => getSeasonDataFromMedia(portableMedia) - ); - const history = useHistory(); - const [seasons, setSeasons] = useState({ seasons: [] }); - const seasonSelected = props.media.seasonId as string; - const episodeSelected = props.media.episodeId as string; - - useEffect(() => { - (async () => { - const seasonData = await searchSeasons(props.media); - setSeasons(seasonData); - })(); - }, [searchSeasons, props.media]); - - function navigateToSeasonAndEpisode(seasonId: string, episodeId: string) { - const newMedia: MWMedia = { ...props.media }; - newMedia.episodeId = episodeId; - newMedia.seasonId = seasonId; - history.replace( - `/media/${newMedia.mediaType}/${serializePortableMedia( - convertMediaToPortable(newMedia) - )}` - ); - } - - const mapSeason = (season: MWMediaSeason) => ({ - id: season.id, - name: season.title || `${t("seasons.season", { season: season.sort })}`, - }); - - const options = seasons.seasons.map(mapSeason); - - const foundSeason = seasons.seasons.find( - (season) => season.id === seasonSelected - ); - const selectedItem = foundSeason ? mapSeason(foundSeason) : null; - - return ( - <> - {loading ? : null} - {error ? : null} - {success && seasons.seasons.length ? ( - <> - - navigateToSeasonAndEpisode( - seasonItem.id, - seasons.seasons.find((s) => s.id === seasonItem.id)?.episodes[0] - .id as string - ) - } - /> - {seasons.seasons - .find((s) => s.id === seasonSelected) - ?.episodes.map((v) => ( - navigateToSeasonAndEpisode(seasonSelected, v.id)} - /> - ))} - - ) : null} - - ); + // const { t } = useTranslation(); + // const [searchSeasons, loading, error, success] = useLoading( + // (portableMedia: MWPortableMedia) => getSeasonDataFromMedia(portableMedia) + // ); + // const history = useHistory(); + // const [seasons, setSeasons] = useState({ seasons: [] }); + // const seasonSelected = props.media.seasonId as string; + // const episodeSelected = props.media.episodeId as string; + // useEffect(() => { + // (async () => { + // const seasonData = await searchSeasons(props.media); + // setSeasons(seasonData); + // })(); + // }, [searchSeasons, props.media]); + // function navigateToSeasonAndEpisode(seasonId: string, episodeId: string) { + // const newMedia: MWMedia = { ...props.media }; + // newMedia.episodeId = episodeId; + // newMedia.seasonId = seasonId; + // history.replace( + // `/media/${newMedia.mediaType}/${serializePortableMedia( + // convertMediaToPortable(newMedia) + // )}` + // ); + // } + // const mapSeason = (season: MWMediaSeason) => ({ + // id: season.id, + // name: season.title || `${t("seasons.season", { season: season.sort })}`, + // }); + // const options = seasons.seasons.map(mapSeason); + // const foundSeason = seasons.seasons.find( + // (season) => season.id === seasonSelected + // ); + // const selectedItem = foundSeason ? mapSeason(foundSeason) : null; + // return ( + // <> + // {loading ? : null} + // {error ? : null} + // {success && seasons.seasons.length ? ( + // <> + // + // navigateToSeasonAndEpisode( + // seasonItem.id, + // seasons.seasons.find((s) => s.id === seasonItem.id)?.episodes[0] + // .id as string + // ) + // } + // /> + // {seasons.seasons + // .find((s) => s.id === seasonSelected) + // ?.episodes.map((v) => ( + // navigateToSeasonAndEpisode(seasonSelected, v.id)} + // /> + // ))} + // + // ) : null} + // + // ); } diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 34fb09b4..ffc7a49b 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -1,10 +1,9 @@ import { Link } from "react-router-dom"; import { DotList } from "@/components/text/DotList"; -import { MWSearchResult } from "@/backend/metadata/search"; -import { MWMediaType } from "@/providers"; +import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types"; export interface MediaCardProps { - media: MWSearchResult; + media: MWMediaMeta; linkable?: boolean; } diff --git a/src/components/media/WatchedEpisodeButton.tsx b/src/components/media/WatchedEpisodeButton.tsx index aa699859..779f2aef 100644 --- a/src/components/media/WatchedEpisodeButton.tsx +++ b/src/components/media/WatchedEpisodeButton.tsx @@ -1,25 +1,24 @@ -import { getEpisodeFromMedia, MWMedia } from "@/providers"; +import { MWMediaMeta } from "@/backend/metadata/types"; import { useWatchedContext, getWatchedFromPortable } from "@/state/watched"; import { Episode } from "./EpisodeButton"; export interface WatchedEpisodeProps { - media: MWMedia; + media: MWMediaMeta; onClick?: () => void; active?: boolean; } export function WatchedEpisode(props: WatchedEpisodeProps) { - const { watched } = useWatchedContext(); - const foundWatched = getWatchedFromPortable(watched.items, props.media); - const episode = getEpisodeFromMedia(props.media); - const watchedPercentage = (foundWatched && foundWatched.percentage) || 0; - - return ( - - ); + // const { watched } = useWatchedContext(); + // const foundWatched = getWatchedFromPortable(watched.items, props.media); + // // const episode = getEpisodeFromMedia(props.media); + // const watchedPercentage = (foundWatched && foundWatched.percentage) || 0; + // return ( + // + // ); } diff --git a/src/components/media/WatchedMediaCard.tsx b/src/components/media/WatchedMediaCard.tsx index f1d37374..6dcf7064 100644 --- a/src/components/media/WatchedMediaCard.tsx +++ b/src/components/media/WatchedMediaCard.tsx @@ -1,8 +1,8 @@ -import { MWSearchResult } from "@/backend/metadata/search"; +import { MWMediaMeta } from "@/backend/metadata/types"; import { MediaCard } from "./MediaCard"; export interface WatchedMediaCardProps { - media: MWSearchResult; + media: MWMediaMeta; } export function WatchedMediaCard(props: WatchedMediaCardProps) { diff --git a/src/hooks/usePortableMedia.ts b/src/hooks/usePortableMedia.ts deleted file mode 100644 index 81744fb9..00000000 --- a/src/hooks/usePortableMedia.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useEffect, useState } from "react"; -import { useParams } from "react-router-dom"; -import { MWPortableMedia } from "@/providers"; - -export function deserializePortableMedia(media: string): MWPortableMedia { - return JSON.parse(atob(decodeURIComponent(media))); -} - -export function serializePortableMedia(media: MWPortableMedia): string { - const data = encodeURIComponent(btoa(JSON.stringify(media))); - return data; -} - -export function usePortableMedia(): MWPortableMedia | undefined { - const { media } = useParams<{ media: string }>(); - const [mediaObject, setMediaObject] = useState( - undefined - ); - - useEffect(() => { - try { - setMediaObject(deserializePortableMedia(media)); - } catch (err) { - console.error("Failed to deserialize portable media", err); - setMediaObject(undefined); - } - }, [media, setMediaObject]); - - return mediaObject; -} diff --git a/src/hooks/useScrape.ts b/src/hooks/useScrape.ts new file mode 100644 index 00000000..2104e5f6 --- /dev/null +++ b/src/hooks/useScrape.ts @@ -0,0 +1,59 @@ +import { findBestStream } from "@/backend/helpers/scrape"; +import { MWStream } from "@/backend/helpers/streams"; +import { useEffect, useState } from "react"; + +interface ScrapeEventLog { + type: "provider" | "embed"; + errored: boolean; + percentage: number; + id: string; +} + +export function useScrape() { + const [eventLog, setEventLog] = useState([]); + const [stream, setStream] = useState(null); + const [pending, setPending] = useState(true); + + useEffect(() => { + setPending(true); + setStream(null); + setEventLog([]); + (async () => { + // TODO has test inputs + const scrapedStream = await findBestStream({ + imdb: "test1", + tmdb: "test2", + onNext(ctx) { + setEventLog((arr) => [ + ...arr, + { + errored: false, + id: ctx.id, + type: ctx.type, + percentage: 0, + }, + ]); + }, + onProgress(ctx) { + setEventLog((arr) => { + const item = arr.reverse().find((v) => v.id === ctx.id); + if (item) { + item.errored = ctx.errored; + item.percentage = ctx.percentage; + } + return [...arr]; + }); + }, + }); + + setPending(false); + setStream(scrapedStream); + })(); + }, []); + + return { + stream, + pending, + eventLog, + }; +} diff --git a/src/hooks/useSearchQuery.ts b/src/hooks/useSearchQuery.ts index 7f83f6aa..b20c78ab 100644 --- a/src/hooks/useSearchQuery.ts +++ b/src/hooks/useSearchQuery.ts @@ -1,6 +1,6 @@ +import { MWMediaType, MWQuery } from "@/backend/metadata/types"; import React, { useRef, useState } from "react"; import { generatePath, useHistory, useRouteMatch } from "react-router-dom"; -import { MWMediaType, MWQuery } from "@/providers"; export function useSearchQuery(): [ MWQuery, diff --git a/src/setup/App.tsx b/src/setup/App.tsx index 091aa967..ebe4f016 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -1,5 +1,4 @@ import { Redirect, Route, Switch } from "react-router-dom"; -import { MWMediaType } from "@/providers"; import { BookmarkContextProvider } from "@/state/bookmark"; import { WatchedContextProvider } from "@/state/watched"; @@ -7,6 +6,7 @@ import { NotFoundPage } from "@/views/notfound/NotFoundView"; import { MediaView } from "@/views/MediaView"; import { SearchView } from "@/views/search/SearchView"; import { TestView } from "@/views/TestView"; +import { MWMediaType } from "@/backend/metadata/types"; function App() { return ( diff --git a/src/state/bookmark/context.tsx b/src/state/bookmark/context.tsx index c8eb2dca..2217908f 100644 --- a/src/state/bookmark/context.tsx +++ b/src/state/bookmark/context.tsx @@ -1,3 +1,4 @@ +import { MWMediaMeta } from "@/backend/metadata/types"; import { createContext, ReactNode, @@ -6,7 +7,6 @@ import { useMemo, useState, } from "react"; -import { getProviderMetadata, MWMediaMeta } from "@/providers"; import { BookmarkStore } from "./store"; interface BookmarkStoreData { @@ -64,7 +64,7 @@ export function BookmarkContextProvider(props: { children: ReactNode }) { const contextValue = useMemo( () => ({ - setItemBookmark(media: MWMediaMeta, bookmarked: boolean) { + setItemBookmark(media: any, bookmarked: boolean) { setBookmarked((data: BookmarkStoreData) => { if (bookmarked) { const itemIndex = getBookmarkIndexFromMedia(data.bookmarks, media); @@ -90,9 +90,7 @@ export function BookmarkContextProvider(props: { children: ReactNode }) { }); }, getFilteredBookmarks() { - return bookmarkStorage.bookmarks.filter( - (bookmark) => getProviderMetadata(bookmark.providerId)?.enabled - ); + return []; }, bookmarkStore: bookmarkStorage, }), diff --git a/src/state/watched/context.tsx b/src/state/watched/context.tsx index 789d2308..4fa68f3e 100644 --- a/src/state/watched/context.tsx +++ b/src/state/watched/context.tsx @@ -1,3 +1,4 @@ +import { MWMediaMeta } from "@/backend/metadata/types"; import React, { createContext, ReactNode, @@ -6,7 +7,6 @@ import React, { useMemo, useState, } from "react"; -import { MWMediaMeta, getProviderMetadata, MWMediaType } from "@/providers"; import { VideoProgressStore } from "./store"; interface WatchedStoreItem extends MWMediaMeta { @@ -28,13 +28,7 @@ export function getWatchedFromPortable( items: WatchedStoreItem[], media: MWMediaMeta ): WatchedStoreItem | undefined { - return items.find( - (v) => - v.mediaId === media.mediaId && - v.providerId === media.providerId && - v.episodeId === media.episodeId && - v.seasonId === media.seasonId - ); + return undefined; } const WatchedContext = createContext({ @@ -73,76 +67,73 @@ export function WatchedContextProvider(props: { children: ReactNode }) { progress: number, total: number ): void { - setWatched((data: WatchedStoreData) => { - let item = getWatchedFromPortable(data.items, media); - if (!item) { - item = { - mediaId: media.mediaId, - mediaType: media.mediaType, - providerId: media.providerId, - title: media.title, - year: media.year, - percentage: 0, - progress: 0, - episodeId: media.episodeId, - seasonId: media.seasonId, - }; - data.items.push(item); - } - - // update actual item - item.progress = progress; - item.percentage = Math.round((progress / total) * 100); - - return data; - }); + // setWatched((data: WatchedStoreData) => { + // let item = getWatchedFromPortable(data.items, media); + // if (!item) { + // item = { + // mediaId: media.mediaId, + // mediaType: media.mediaType, + // providerId: media.providerId, + // title: media.title, + // year: media.year, + // percentage: 0, + // progress: 0, + // episodeId: media.episodeId, + // seasonId: media.seasonId, + // }; + // data.items.push(item); + // } + // // update actual item + // item.progress = progress; + // item.percentage = Math.round((progress / total) * 100); + // return data; + // }); }, getFilteredWatched() { // remove disabled providers - let filtered = watched.items.filter( - (item) => getProviderMetadata(item.providerId)?.enabled - ); - - // get highest episode number for every anime/season - const highestEpisode: Record = {}; - const highestWatchedItem: Record = {}; - filtered = filtered.filter((item) => { - if ( - [MWMediaType.ANIME, MWMediaType.SERIES].includes(item.mediaType) - ) { - const key = `${item.mediaType}-${item.mediaId}`; - const current: [number, number] = [ - item.episodeId ? parseInt(item.episodeId, 10) : -1, - item.seasonId ? parseInt(item.seasonId, 10) : -1, - ]; - let existing = highestEpisode[key]; - if (!existing) { - existing = current; - highestEpisode[key] = current; - highestWatchedItem[key] = item; - } - - if ( - current[0] > existing[0] || - (current[0] === existing[0] && current[1] > existing[1]) - ) { - highestEpisode[key] = current; - highestWatchedItem[key] = item; - } - return false; - } - return true; - }); - - return [...filtered, ...Object.values(highestWatchedItem)]; + // let filtered = watched.items.filter( + // (item) => getProviderMetadata(item.providerId)?.enabled + // ); + // // get highest episode number for every anime/season + // const highestEpisode: Record = {}; + // const highestWatchedItem: Record = {}; + // filtered = filtered.filter((item) => { + // if ( + // [MWMediaType.ANIME, MWMediaType.SERIES].includes(item.mediaType) + // ) { + // const key = `${item.mediaType}-${item.mediaId}`; + // const current: [number, number] = [ + // item.episodeId ? parseInt(item.episodeId, 10) : -1, + // item.seasonId ? parseInt(item.seasonId, 10) : -1, + // ]; + // let existing = highestEpisode[key]; + // if (!existing) { + // existing = current; + // highestEpisode[key] = current; + // highestWatchedItem[key] = item; + // } + // if ( + // current[0] > existing[0] || + // (current[0] === existing[0] && current[1] > existing[1]) + // ) { + // highestEpisode[key] = current; + // highestWatchedItem[key] = item; + // } + // return false; + // } + // return true; + // }); + // return [...filtered, ...Object.values(highestWatchedItem)]; }, watched, }), - [watched, setWatched] + [ + /*watched, setWatched*/ + ] ); return ( - + {props.children} ); diff --git a/src/state/watched/store.ts b/src/state/watched/store.ts index 065de4ec..eab57080 100644 --- a/src/state/watched/store.ts +++ b/src/state/watched/store.ts @@ -1,6 +1,4 @@ -import { MWMediaType } from "@/providers"; import { versionedStoreBuilder } from "@/utils/storage"; -import { WatchedStoreData } from "./context"; export const VideoProgressStore = versionedStoreBuilder() .setKey("video-progress") @@ -9,79 +7,79 @@ export const VideoProgressStore = versionedStoreBuilder() }) .addVersion({ version: 1, - migrate(data: any) { - const output: WatchedStoreData = { items: [] }; + migrate() { + // const output: WatchedStoreData = { items: [] }; - if (!data || data.constructor !== Object) return output; + // if (!data || data.constructor !== Object) return output; - Object.keys(data).forEach((scraperId) => { - if (scraperId === "--version") return; - if (scraperId === "save") return; + // Object.keys(data).forEach((scraperId) => { + // if (scraperId === "--version") return; + // if (scraperId === "save") return; - if ( - data[scraperId].movie && - data[scraperId].movie.constructor === Object - ) { - Object.keys(data[scraperId].movie).forEach((movieId) => { - try { - output.items.push({ - mediaId: movieId.includes("player.php") - ? movieId.split("player.php%3Fimdb%3D")[1] - : movieId, - mediaType: MWMediaType.MOVIE, - providerId: scraperId, - title: data[scraperId].movie[movieId].full.meta.title, - year: data[scraperId].movie[movieId].full.meta.year, - progress: data[scraperId].movie[movieId].full.currentlyAt, - percentage: Math.round( - (data[scraperId].movie[movieId].full.currentlyAt / - data[scraperId].movie[movieId].full.totalDuration) * - 100 - ), - }); - } catch (err) { - console.error( - `Failed to migrate movie: ${scraperId}/${movieId}`, - data[scraperId].movie[movieId] - ); - } - }); - } + // if ( + // data[scraperId].movie && + // data[scraperId].movie.constructor === Object + // ) { + // Object.keys(data[scraperId].movie).forEach((movieId) => { + // try { + // output.items.push({ + // mediaId: movieId.includes("player.php") + // ? movieId.split("player.php%3Fimdb%3D")[1] + // : movieId, + // mediaType: MWMediaType.MOVIE, + // providerId: scraperId, + // title: data[scraperId].movie[movieId].full.meta.title, + // year: data[scraperId].movie[movieId].full.meta.year, + // progress: data[scraperId].movie[movieId].full.currentlyAt, + // percentage: Math.round( + // (data[scraperId].movie[movieId].full.currentlyAt / + // data[scraperId].movie[movieId].full.totalDuration) * + // 100 + // ), + // }); + // } catch (err) { + // console.error( + // `Failed to migrate movie: ${scraperId}/${movieId}`, + // data[scraperId].movie[movieId] + // ); + // } + // }); + // } - if ( - data[scraperId].show && - data[scraperId].show.constructor === Object - ) { - Object.keys(data[scraperId].show).forEach((showId) => { - if (data[scraperId].show[showId].constructor !== Object) return; - Object.keys(data[scraperId].show[showId]).forEach((episodeId) => { - try { - output.items.push({ - mediaId: showId, - mediaType: MWMediaType.SERIES, - providerId: scraperId, - title: data[scraperId].show[showId][episodeId].meta.title, - year: data[scraperId].show[showId][episodeId].meta.year, - percentage: Math.round( - (data[scraperId].show[showId][episodeId].currentlyAt / - data[scraperId].show[showId][episodeId].totalDuration) * - 100 - ), - progress: data[scraperId].show[showId][episodeId].currentlyAt, - episodeId: - data[scraperId].show[showId][episodeId].show.episode, - seasonId: data[scraperId].show[showId][episodeId].show.season, - }); - } catch (err) { - console.error( - `Failed to migrate series: ${scraperId}/${showId}/${episodeId}`, - data[scraperId].show[showId][episodeId] - ); - } - }); - }); - } - }); + // if ( + // data[scraperId].show && + // data[scraperId].show.constructor === Object + // ) { + // Object.keys(data[scraperId].show).forEach((showId) => { + // if (data[scraperId].show[showId].constructor !== Object) return; + // Object.keys(data[scraperId].show[showId]).forEach((episodeId) => { + // try { + // output.items.push({ + // mediaId: showId, + // mediaType: MWMediaType.SERIES, + // providerId: scraperId, + // title: data[scraperId].show[showId][episodeId].meta.title, + // year: data[scraperId].show[showId][episodeId].meta.year, + // percentage: Math.round( + // (data[scraperId].show[showId][episodeId].currentlyAt / + // data[scraperId].show[showId][episodeId].totalDuration) * + // 100 + // ), + // progress: data[scraperId].show[showId][episodeId].currentlyAt, + // episodeId: + // data[scraperId].show[showId][episodeId].show.episode, + // seasonId: data[scraperId].show[showId][episodeId].show.season, + // }); + // } catch (err) { + // console.error( + // `Failed to migrate series: ${scraperId}/${showId}/${episodeId}`, + // data[scraperId].show[showId][episodeId] + // ); + // } + // }); + // }); + // } + // }); return output; }, diff --git a/src/views/MediaView.tsx b/src/views/MediaView.tsx index 77d54d2c..5cea1a35 100644 --- a/src/views/MediaView.tsx +++ b/src/views/MediaView.tsx @@ -1,220 +1,215 @@ -import { ReactElement, useCallback, useEffect, useState } from "react"; -import { useHistory } from "react-router-dom"; +// import { ReactElement, useCallback, useEffect, useState } from "react"; +// import { useHistory } from "react-router-dom"; +// import { useTranslation } from "react-i18next"; +// import { IconPatch } from "@/components/buttons/IconPatch"; +// import { Icons } from "@/components/Icon"; +// import { Navigation } from "@/components/layout/Navigation"; +// import { Paper } from "@/components/layout/Paper"; +// import { LoadingSeasons, Seasons } from "@/components/layout/Seasons"; +// import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer"; +// import { ArrowLink } from "@/components/text/ArrowLink"; +// import { DotList } from "@/components/text/DotList"; +// import { Title } from "@/components/text/Title"; +// import { useLoading } from "@/hooks/useLoading"; +// import { usePortableMedia } from "@/hooks/usePortableMedia"; +// import { +// getIfBookmarkedFromPortable, +// useBookmarkContext, +// } from "@/state/bookmark"; +// import { getWatchedFromPortable, useWatchedContext } from "@/state/watched"; +// import { SourceControl } from "@/components/video/controls/SourceControl"; +// import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl"; +// import { Loading } from "@/components/layout/Loading"; +// import { NotFoundChecks } from "./notfound/NotFoundChecks"; + import { useTranslation } from "react-i18next"; -import { IconPatch } from "@/components/buttons/IconPatch"; -import { Icons } from "@/components/Icon"; +import { useHistory } from "react-router-dom"; import { Navigation } from "@/components/layout/Navigation"; -import { Paper } from "@/components/layout/Paper"; -import { LoadingSeasons, Seasons } from "@/components/layout/Seasons"; -import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer"; import { ArrowLink } from "@/components/text/ArrowLink"; -import { DotList } from "@/components/text/DotList"; -import { Title } from "@/components/text/Title"; -import { useLoading } from "@/hooks/useLoading"; -import { usePortableMedia } from "@/hooks/usePortableMedia"; -import { - MWPortableMedia, - getStream, - MWMediaStream, - MWMedia, - convertPortableToMedia, - getProviderFromId, - MWMediaProvider, - MWMediaType, -} from "@/providers"; -import { - getIfBookmarkedFromPortable, - useBookmarkContext, -} from "@/state/bookmark"; -import { getWatchedFromPortable, useWatchedContext } from "@/state/watched"; -import { SourceControl } from "@/components/video/controls/SourceControl"; -import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl"; -import { Loading } from "@/components/layout/Loading"; -import { NotFoundChecks } from "./notfound/NotFoundChecks"; - -interface StyledMediaViewProps { - media: MWMedia; - stream: MWMediaStream; -} - -export function SkeletonVideoPlayer(props: { error?: boolean }) { - return ( -
- {props.error ? ( -
- -

Couldn't get your stream

-
- ) : ( -
- -

Getting your stream...

-
- )} -
- ); -} - -function StyledMediaView(props: StyledMediaViewProps) { - const reactHistory = useHistory(); - const watchedStore = useWatchedContext(); - const startAtTime: number | undefined = getWatchedFromPortable( - watchedStore.watched.items, - props.media - )?.progress; - - const updateProgress = useCallback( - (time: number, duration: number) => { - // Don't update stored progress if less than 30s into the video - if (time <= 30) return; - watchedStore.updateProgress(props.media, time, duration); - }, - [props, watchedStore] - ); - - const goBack = useCallback(() => { - if (reactHistory.action !== "POP") reactHistory.goBack(); - else reactHistory.push("/"); - }, [reactHistory]); - - return ( -
- - - - -
- ); -} - -interface StyledMediaFooterProps { - media: MWMedia; - provider: MWMediaProvider; -} -function StyledMediaFooter(props: StyledMediaFooterProps) { - const { setItemBookmark, getFilteredBookmarks } = useBookmarkContext(); - const isBookmarked = getIfBookmarkedFromPortable( - getFilteredBookmarks(), - props.media - ); - - return ( - -
-
- {props.media.title} - -
-
- setItemBookmark(props.media, !isBookmarked)} - clickable - /> -
-
- {props.media.mediaType !== MWMediaType.MOVIE ? ( - - ) : null} -
- ); -} - -function LoadingMediaFooter(props: { error?: boolean }) { - const { t } = useTranslation(); - - return ( - -
-
-
-
- - -
- {props.error ? ( -
- -

{t("media.invalidUrl")}

-
- ) : ( - - )} -
-
- - ); -} - -function MediaViewContent(props: { portable: MWPortableMedia }) { - const mediaPortable = props.portable; - const [streamUrl, setStreamUrl] = useState(); - const [media, setMedia] = useState(); - const [fetchMedia, loadingPortable, errorPortable] = useLoading( - (portable: MWPortableMedia) => convertPortableToMedia(portable) - ); - const [fetchStream, loadingStream, errorStream] = useLoading( - (portable: MWPortableMedia) => getStream(portable) - ); - - useEffect(() => { - (async () => { - if (mediaPortable) { - setMedia(await fetchMedia(mediaPortable)); - } - })(); - }, [mediaPortable, setMedia, fetchMedia]); - - useEffect(() => { - (async () => { - if (mediaPortable) { - setStreamUrl(await fetchStream(mediaPortable)); - } - })(); - }, [mediaPortable, setStreamUrl, fetchStream]); - - let playerContent: ReactElement | null = null; - if (loadingStream) playerContent = ; - else if (errorStream) playerContent = ; - else if (media && streamUrl) - playerContent = ; - - let footerContent: ReactElement | null = null; - if (loadingPortable) footerContent = ; - else if (errorPortable) footerContent = ; - else if (mediaPortable && media) - footerContent = ( - - ); - - return ( - <> - {playerContent} - {footerContent} - - ); -} +// interface StyledMediaViewProps { +// media: MWMedia; +// stream: MWMediaStream; +// } + +// export function SkeletonVideoPlayer(props: { error?: boolean }) { +// return ( +//
+// {props.error ? ( +//
+// +//

Couldn't get your stream

+//
+// ) : ( +//
+// +//

Getting your stream...

+//
+// )} +//
+// ); +// } + +// function StyledMediaView(props: StyledMediaViewProps) { +// const reactHistory = useHistory(); +// const watchedStore = useWatchedContext(); +// const startAtTime: number | undefined = getWatchedFromPortable( +// watchedStore.watched.items, +// props.media +// )?.progress; + +// const updateProgress = useCallback( +// (time: number, duration: number) => { +// // Don't update stored progress if less than 30s into the video +// if (time <= 30) return; +// watchedStore.updateProgress(props.media, time, duration); +// }, +// [props, watchedStore] +// ); + +// const goBack = useCallback(() => { +// if (reactHistory.action !== "POP") reactHistory.goBack(); +// else reactHistory.push("/"); +// }, [reactHistory]); + +// return ( +//
+// +// +// +// +//
+// ); +// } + +// interface StyledMediaFooterProps { +// media: MWMedia; +// provider: MWMediaProvider; +// } + +// function StyledMediaFooter(props: StyledMediaFooterProps) { +// const { setItemBookmark, getFilteredBookmarks } = useBookmarkContext(); +// const isBookmarked = getIfBookmarkedFromPortable( +// getFilteredBookmarks(), +// props.media +// ); + +// return ( +// +//
+//
+// {props.media.title} +// +//
+//
+// setItemBookmark(props.media, !isBookmarked)} +// clickable +// /> +//
+//
+// {props.media.mediaType !== MWMediaType.MOVIE ? ( +// +// ) : null} +//
+// ); +// } + +// function LoadingMediaFooter(props: { error?: boolean }) { +// const { t } = useTranslation(); + +// return ( +// +//
+//
+//
+//
+// +// +//
+// {props.error ? ( +//
+// +//

{t("media.invalidUrl")}

+//
+// ) : ( +// +// )} +//
+//
+// +// ); +// } + +// function MediaViewContent(props: { portable: MWPortableMedia }) { +// const mediaPortable = props.portable; +// const [streamUrl, setStreamUrl] = useState(); +// const [media, setMedia] = useState(); +// const [fetchMedia, loadingPortable, errorPortable] = useLoading( +// (portable: MWPortableMedia) => convertPortableToMedia(portable) +// ); +// const [fetchStream, loadingStream, errorStream] = useLoading( +// (portable: MWPortableMedia) => getStream(portable) +// ); + +// useEffect(() => { +// (async () => { +// if (mediaPortable) { +// setMedia(await fetchMedia(mediaPortable)); +// } +// })(); +// }, [mediaPortable, setMedia, fetchMedia]); + +// useEffect(() => { +// (async () => { +// if (mediaPortable) { +// setStreamUrl(await fetchStream(mediaPortable)); +// } +// })(); +// }, [mediaPortable, setStreamUrl, fetchStream]); + +// let playerContent: ReactElement | null = null; +// if (loadingStream) playerContent = ; +// else if (errorStream) playerContent = ; +// else if (media && streamUrl) +// playerContent = ; + +// let footerContent: ReactElement | null = null; +// if (loadingPortable) footerContent = ; +// else if (errorPortable) footerContent = ; +// else if (mediaPortable && media) +// footerContent = ( +// +// ); + +// return ( +// <> +// {playerContent} +// {footerContent} +// +// ); +// } export function MediaView() { const { t } = useTranslation(); - const mediaPortable: MWPortableMedia | undefined = usePortableMedia(); + // const mediaPortable: MWPortableMedia | undefined = usePortableMedia(); const reactHistory = useHistory(); return ( @@ -230,11 +225,11 @@ export function MediaView() { linkText={t("media.arrowText")} /> - + {/*
-
+
*/}
); } diff --git a/src/views/TestView.tsx b/src/views/TestView.tsx index 5314996d..dd45fdb0 100644 --- a/src/views/TestView.tsx +++ b/src/views/TestView.tsx @@ -1,9 +1,10 @@ -import { searchForMedia } from "@/backend/metadata/search"; -import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl"; -import { SourceControl } from "@/components/video/controls/SourceControl"; -import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer"; -import { MWMediaType } from "@/providers"; -import { useCallback, useState } from "react"; +// import { searchForMedia } from "@/backend/metadata/search"; +// import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl"; +// import { SourceControl } from "@/components/video/controls/SourceControl"; +// import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer"; +import { useScrape } from "@/hooks/useScrape"; +// import { MWMediaType } from "@/providers"; +// import { useCallback, useState } from "react"; // test videos: https://gist.github.com/jsturgis/3b19447b304616f18657 @@ -24,37 +25,58 @@ import { useCallback, useState } from "react"; // - devices: ipadOS // - features: HLS, error handling, preload interactions +// export function TestView() { +// const [show, setShow] = useState(true); +// const handleClick = useCallback(() => { +// setShow((v) => !v); +// }, [setShow]); + +// if (!show) { +// return

Click me to show

; +// } + +// async function search() { +// const test = await searchForMedia({ +// searchQuery: "tron", +// type: MWMediaType.MOVIE, +// }); +// console.log(test); +// } + +// return ( +//
+// +// +// console.log(a, b)} +// /> +// +//

search()}>click me to search

+//
+// ); +// } + export function TestView() { - const [show, setShow] = useState(true); - const handleClick = useCallback(() => { - setShow((v) => !v); - }, [setShow]); - - if (!show) { - return

Click me to show

; - } - - async function search() { - const test = await searchForMedia({ - searchQuery: "tron", - type: MWMediaType.MOVIE, - }); - console.log(test); - } + const { eventLog, pending, stream } = useScrape(); return ( -
- - - console.log(a, b)} - /> - -

search()}>click me to search

+
+

pending: {pending}

+

+ stream: {stream?.streamUrl} - {stream?.type} - {stream?.quality} +

+
+ {eventLog.map((v) => ( +
+

+ {v.percentage}% - {v.type} - {v.errored ? "ERROR" : "pending"} +

+
+ ))}
); } diff --git a/src/views/notfound/NotFoundChecks.tsx b/src/views/notfound/NotFoundChecks.tsx index 0981e180..a8fe9e4a 100644 --- a/src/views/notfound/NotFoundChecks.tsx +++ b/src/views/notfound/NotFoundChecks.tsx @@ -1,9 +1,8 @@ import { ReactElement } from "react"; -import { getProviderMetadata, MWPortableMedia } from "@/providers"; -import { NotFoundMedia, NotFoundProvider } from "./NotFoundView"; +// import { NotFoundMedia, NotFoundProvider } from "./NotFoundView"; export interface NotFoundChecksProps { - portable: MWPortableMedia | undefined; + // portable: MWPortableMedia | undefined; children?: ReactElement; } @@ -13,17 +12,17 @@ export interface NotFoundChecksProps { export function NotFoundChecks( props: NotFoundChecksProps ): ReactElement | null { - const providerMeta = props.portable - ? getProviderMetadata(props.portable.providerId) - : undefined; + // const providerMeta = props.portable + // ? getProviderMetadata(props.portable.providerId) + // : undefined; - if (!providerMeta || !providerMeta.exists) { - return ; - } + // if (!providerMeta || !providerMeta.exists) { + // return ; + // } - if (!providerMeta.enabled) { - return ; - } + // if (!providerMeta.enabled) { + // return ; + // } return props.children || null; } diff --git a/src/views/search/HomeView.tsx b/src/views/search/HomeView.tsx index 16f68e7c..7a94cdcb 100644 --- a/src/views/search/HomeView.tsx +++ b/src/views/search/HomeView.tsx @@ -2,7 +2,6 @@ import { useTranslation } from "react-i18next"; import { Icons } from "@/components/Icon"; import { SectionHeading } from "@/components/layout/SectionHeading"; import { MediaGrid } from "@/components/media/MediaGrid"; -import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; import { getIfBookmarkedFromPortable, useBookmarkContext, @@ -22,12 +21,12 @@ function Bookmarks() { icon={Icons.BOOKMARK} > - {bookmarks.map((v) => ( + {/* {bookmarks.map((v) => ( - ))} + ))} */} ); @@ -51,13 +50,13 @@ function Watched() { icon={Icons.CLOCK} > - {watchedItems.map((v) => ( + {/* {watchedItems.map((v) => ( - ))} + ))} */} ); diff --git a/src/views/search/SearchResultsPartial.tsx b/src/views/search/SearchResultsPartial.tsx index 59281093..d7859612 100644 --- a/src/views/search/SearchResultsPartial.tsx +++ b/src/views/search/SearchResultsPartial.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from "react"; import { useDebounce } from "@/hooks/useDebounce"; -import { MWQuery } from "@/providers"; +import { MWQuery } from "@/backend/metadata/types"; import { HomeView } from "./HomeView"; import { SearchLoadingView } from "./SearchLoadingView"; import { SearchResultsView } from "./SearchResultsView"; diff --git a/src/views/search/SearchResultsView.tsx b/src/views/search/SearchResultsView.tsx index ad611516..60a61115 100644 --- a/src/views/search/SearchResultsView.tsx +++ b/src/views/search/SearchResultsView.tsx @@ -6,8 +6,8 @@ import { SectionHeading } from "@/components/layout/SectionHeading"; import { MediaGrid } from "@/components/media/MediaGrid"; import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; import { useLoading } from "@/hooks/useLoading"; -import { MWQuery } from "@/providers"; -import { MWSearchResult, searchForMedia } from "@/backend/metadata/search"; +import { searchForMedia } from "@/backend/metadata/search"; +import { MWMediaMeta, MWQuery } from "@/backend/metadata/types"; import { SearchLoadingView } from "./SearchLoadingView"; function SearchSuffix(props: { failed?: boolean; results?: number }) { @@ -46,7 +46,7 @@ function SearchSuffix(props: { failed?: boolean; results?: number }) { export function SearchResultsView({ searchQuery }: { searchQuery: MWQuery }) { const { t } = useTranslation(); - const [results, setResults] = useState([]); + const [results, setResults] = useState([]); const [runSearchQuery, loading, error] = useLoading((query: MWQuery) => searchForMedia(query) ); From 6589e095ec75e1855867fbb541c9633dff3992ab Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Thu, 12 Jan 2023 22:36:28 +0100 Subject: [PATCH 028/135] cleanup unused code --- src/state/watched/store.ts | 78 +------- src/views/MediaView.tsx | 241 ++----------------------- src/views/notfound/NotFoundChecks.tsx | 17 +- src/views/search/SearchResultsView.tsx | 2 + 4 files changed, 23 insertions(+), 315 deletions(-) diff --git a/src/state/watched/store.ts b/src/state/watched/store.ts index eab57080..4ea10100 100644 --- a/src/state/watched/store.ts +++ b/src/state/watched/store.ts @@ -8,80 +8,10 @@ export const VideoProgressStore = versionedStoreBuilder() .addVersion({ version: 1, migrate() { - // const output: WatchedStoreData = { items: [] }; - - // if (!data || data.constructor !== Object) return output; - - // Object.keys(data).forEach((scraperId) => { - // if (scraperId === "--version") return; - // if (scraperId === "save") return; - - // if ( - // data[scraperId].movie && - // data[scraperId].movie.constructor === Object - // ) { - // Object.keys(data[scraperId].movie).forEach((movieId) => { - // try { - // output.items.push({ - // mediaId: movieId.includes("player.php") - // ? movieId.split("player.php%3Fimdb%3D")[1] - // : movieId, - // mediaType: MWMediaType.MOVIE, - // providerId: scraperId, - // title: data[scraperId].movie[movieId].full.meta.title, - // year: data[scraperId].movie[movieId].full.meta.year, - // progress: data[scraperId].movie[movieId].full.currentlyAt, - // percentage: Math.round( - // (data[scraperId].movie[movieId].full.currentlyAt / - // data[scraperId].movie[movieId].full.totalDuration) * - // 100 - // ), - // }); - // } catch (err) { - // console.error( - // `Failed to migrate movie: ${scraperId}/${movieId}`, - // data[scraperId].movie[movieId] - // ); - // } - // }); - // } - - // if ( - // data[scraperId].show && - // data[scraperId].show.constructor === Object - // ) { - // Object.keys(data[scraperId].show).forEach((showId) => { - // if (data[scraperId].show[showId].constructor !== Object) return; - // Object.keys(data[scraperId].show[showId]).forEach((episodeId) => { - // try { - // output.items.push({ - // mediaId: showId, - // mediaType: MWMediaType.SERIES, - // providerId: scraperId, - // title: data[scraperId].show[showId][episodeId].meta.title, - // year: data[scraperId].show[showId][episodeId].meta.year, - // percentage: Math.round( - // (data[scraperId].show[showId][episodeId].currentlyAt / - // data[scraperId].show[showId][episodeId].totalDuration) * - // 100 - // ), - // progress: data[scraperId].show[showId][episodeId].currentlyAt, - // episodeId: - // data[scraperId].show[showId][episodeId].show.episode, - // seasonId: data[scraperId].show[showId][episodeId].show.season, - // }); - // } catch (err) { - // console.error( - // `Failed to migrate series: ${scraperId}/${showId}/${episodeId}`, - // data[scraperId].show[showId][episodeId] - // ); - // } - // }); - // }); - // } - // }); - - return output; + // TODO add migration back + return { + items: [], + }; }, }) .addVersion({ diff --git a/src/views/MediaView.tsx b/src/views/MediaView.tsx index 5cea1a35..e9bedc53 100644 --- a/src/views/MediaView.tsx +++ b/src/views/MediaView.tsx @@ -1,235 +1,22 @@ -// import { ReactElement, useCallback, useEffect, useState } from "react"; -// import { useHistory } from "react-router-dom"; -// import { useTranslation } from "react-i18next"; -// import { IconPatch } from "@/components/buttons/IconPatch"; -// import { Icons } from "@/components/Icon"; -// import { Navigation } from "@/components/layout/Navigation"; -// import { Paper } from "@/components/layout/Paper"; -// import { LoadingSeasons, Seasons } from "@/components/layout/Seasons"; -// import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer"; -// import { ArrowLink } from "@/components/text/ArrowLink"; -// import { DotList } from "@/components/text/DotList"; -// import { Title } from "@/components/text/Title"; -// import { useLoading } from "@/hooks/useLoading"; -// import { usePortableMedia } from "@/hooks/usePortableMedia"; -// import { -// getIfBookmarkedFromPortable, -// useBookmarkContext, -// } from "@/state/bookmark"; -// import { getWatchedFromPortable, useWatchedContext } from "@/state/watched"; -// import { SourceControl } from "@/components/video/controls/SourceControl"; -// import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl"; -// import { Loading } from "@/components/layout/Loading"; -// import { NotFoundChecks } from "./notfound/NotFoundChecks"; - -import { useTranslation } from "react-i18next"; import { useHistory } from "react-router-dom"; -import { Navigation } from "@/components/layout/Navigation"; -import { ArrowLink } from "@/components/text/ArrowLink"; - -// interface StyledMediaViewProps { -// media: MWMedia; -// stream: MWMediaStream; -// } - -// export function SkeletonVideoPlayer(props: { error?: boolean }) { -// return ( -//
-// {props.error ? ( -//
-// -//

Couldn't get your stream

-//
-// ) : ( -//
-// -//

Getting your stream...

-//
-// )} -//
-// ); -// } - -// function StyledMediaView(props: StyledMediaViewProps) { -// const reactHistory = useHistory(); -// const watchedStore = useWatchedContext(); -// const startAtTime: number | undefined = getWatchedFromPortable( -// watchedStore.watched.items, -// props.media -// )?.progress; - -// const updateProgress = useCallback( -// (time: number, duration: number) => { -// // Don't update stored progress if less than 30s into the video -// if (time <= 30) return; -// watchedStore.updateProgress(props.media, time, duration); -// }, -// [props, watchedStore] -// ); - -// const goBack = useCallback(() => { -// if (reactHistory.action !== "POP") reactHistory.goBack(); -// else reactHistory.push("/"); -// }, [reactHistory]); - -// return ( -//
-// -// -// -// -//
-// ); -// } - -// interface StyledMediaFooterProps { -// media: MWMedia; -// provider: MWMediaProvider; -// } - -// function StyledMediaFooter(props: StyledMediaFooterProps) { -// const { setItemBookmark, getFilteredBookmarks } = useBookmarkContext(); -// const isBookmarked = getIfBookmarkedFromPortable( -// getFilteredBookmarks(), -// props.media -// ); - -// return ( -// -//
-//
-// {props.media.title} -// -//
-//
-// setItemBookmark(props.media, !isBookmarked)} -// clickable -// /> -//
-//
-// {props.media.mediaType !== MWMediaType.MOVIE ? ( -// -// ) : null} -//
-// ); -// } - -// function LoadingMediaFooter(props: { error?: boolean }) { -// const { t } = useTranslation(); - -// return ( -// -//
-//
-//
-//
-// -// -//
-// {props.error ? ( -//
-// -//

{t("media.invalidUrl")}

-//
-// ) : ( -// -// )} -//
-//
-// -// ); -// } - -// function MediaViewContent(props: { portable: MWPortableMedia }) { -// const mediaPortable = props.portable; -// const [streamUrl, setStreamUrl] = useState(); -// const [media, setMedia] = useState(); -// const [fetchMedia, loadingPortable, errorPortable] = useLoading( -// (portable: MWPortableMedia) => convertPortableToMedia(portable) -// ); -// const [fetchStream, loadingStream, errorStream] = useLoading( -// (portable: MWPortableMedia) => getStream(portable) -// ); - -// useEffect(() => { -// (async () => { -// if (mediaPortable) { -// setMedia(await fetchMedia(mediaPortable)); -// } -// })(); -// }, [mediaPortable, setMedia, fetchMedia]); - -// useEffect(() => { -// (async () => { -// if (mediaPortable) { -// setStreamUrl(await fetchStream(mediaPortable)); -// } -// })(); -// }, [mediaPortable, setStreamUrl, fetchStream]); - -// let playerContent: ReactElement | null = null; -// if (loadingStream) playerContent = ; -// else if (errorStream) playerContent = ; -// else if (media && streamUrl) -// playerContent = ; - -// let footerContent: ReactElement | null = null; -// if (loadingPortable) footerContent = ; -// else if (errorPortable) footerContent = ; -// else if (mediaPortable && media) -// footerContent = ( -// -// ); - -// return ( -// <> -// {playerContent} -// {footerContent} -// -// ); -// } +import { useCallback } from "react"; +import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer"; export function MediaView() { - const { t } = useTranslation(); - // const mediaPortable: MWPortableMedia | undefined = usePortableMedia(); const reactHistory = useHistory(); + const goBack = useCallback(() => { + if (reactHistory.action !== "POP") reactHistory.goBack(); + else reactHistory.push("/"); + }, [reactHistory]); + + // TODO fetch meta + // TODO call useScrape + // TODO not found checks + // TODO watched store + // TODO scrape loading state + // TODO error page with video header return ( -
- - - reactHistory.action !== "POP" - ? reactHistory.goBack() - : reactHistory.push("/") - } - direction="left" - linkText={t("media.arrowText")} - /> - - {/* -
- -
-
*/} -
+ ); } diff --git a/src/views/notfound/NotFoundChecks.tsx b/src/views/notfound/NotFoundChecks.tsx index a8fe9e4a..cffd9b85 100644 --- a/src/views/notfound/NotFoundChecks.tsx +++ b/src/views/notfound/NotFoundChecks.tsx @@ -1,28 +1,17 @@ import { ReactElement } from "react"; -// import { NotFoundMedia, NotFoundProvider } from "./NotFoundView"; export interface NotFoundChecksProps { - // portable: MWPortableMedia | undefined; + id: string; children?: ReactElement; } /* - ** Component that only renders children if the passed-in portable is fully correct + ** Component that only renders children if the passed in data is fully correct */ export function NotFoundChecks( props: NotFoundChecksProps ): ReactElement | null { - // const providerMeta = props.portable - // ? getProviderMetadata(props.portable.providerId) - // : undefined; - - // if (!providerMeta || !providerMeta.exists) { - // return ; - // } - - // if (!providerMeta.enabled) { - // return ; - // } + // TODO do notfound check return props.children || null; } diff --git a/src/views/search/SearchResultsView.tsx b/src/views/search/SearchResultsView.tsx index 60a61115..5c67e30e 100644 --- a/src/views/search/SearchResultsView.tsx +++ b/src/views/search/SearchResultsView.tsx @@ -65,6 +65,8 @@ export function SearchResultsView({ searchQuery }: { searchQuery: MWQuery }) { if (error) return ; if (!results) return null; + // TODO on click go to the right page with id instead of portable + return (
{results.length > 0 ? ( From a64841507fdfe8a173c8695be76927c5a34a6e40 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sat, 14 Jan 2023 00:12:56 +0100 Subject: [PATCH 029/135] port providers, media watch page + make search work again Co-authored-by: James Hawkins --- src/backend/embeds/.gitkeep | 1 + src/backend/embeds/testEmbedScraper.ts | 30 - src/backend/embeds/testEmbedScraperTwo.ts | 24 - src/backend/helpers/provider.ts | 4 +- src/backend/helpers/run.ts | 2 +- src/backend/helpers/scrape.ts | 7 +- src/backend/helpers/streams.ts | 2 + src/backend/index.ts | 5 +- src/backend/metadata/getmeta.ts | 61 ++ src/backend/metadata/justwatch.ts | 42 ++ src/backend/metadata/search.ts | 30 +- .../providers/{testProvider.ts => flixhq.ts} | 1 + src/backend/providers/gdriveplayer.ts | 96 +++ src/backend/providers/superstream/LICENSE | 680 ++++++++++++++++++ .../providers/superstream/superstream.ts | 205 ++++++ src/backend/providers/testProviderTwo.ts | 37 - src/components/media/MediaCard.tsx | 11 +- src/components/video/VideoContext.tsx | 7 +- src/components/video/VideoPlayer.tsx | 2 +- .../video/controls/SourceControl.tsx | 3 +- src/components/video/hooks/controlVideo.ts | 9 +- src/hooks/useScrape.ts | 9 +- src/setup/App.tsx | 5 +- src/state/bookmark/context.tsx | 20 +- src/state/watched/context.tsx | 64 +- src/views/MediaView.tsx | 76 +- src/views/TestView.tsx | 82 --- src/views/search/HomeView.tsx | 56 +- src/views/search/SearchResultsView.tsx | 2 - 29 files changed, 1263 insertions(+), 310 deletions(-) create mode 100644 src/backend/embeds/.gitkeep delete mode 100644 src/backend/embeds/testEmbedScraper.ts delete mode 100644 src/backend/embeds/testEmbedScraperTwo.ts create mode 100644 src/backend/metadata/getmeta.ts create mode 100644 src/backend/metadata/justwatch.ts rename src/backend/providers/{testProvider.ts => flixhq.ts} (97%) create mode 100644 src/backend/providers/gdriveplayer.ts create mode 100644 src/backend/providers/superstream/LICENSE create mode 100644 src/backend/providers/superstream/superstream.ts delete mode 100644 src/backend/providers/testProviderTwo.ts delete mode 100644 src/views/TestView.tsx diff --git a/src/backend/embeds/.gitkeep b/src/backend/embeds/.gitkeep new file mode 100644 index 00000000..f42d5aa9 --- /dev/null +++ b/src/backend/embeds/.gitkeep @@ -0,0 +1 @@ +embed scrapers go here diff --git a/src/backend/embeds/testEmbedScraper.ts b/src/backend/embeds/testEmbedScraper.ts deleted file mode 100644 index 8da3786f..00000000 --- a/src/backend/embeds/testEmbedScraper.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { MWEmbedType } from "../helpers/embed"; -import { registerEmbedScraper } from "../helpers/register"; -import { MWStreamQuality, MWStreamType } from "../helpers/streams"; - -const timeout = (time: number) => - new Promise((resolve) => { - setTimeout(() => resolve(), time); - }); - -registerEmbedScraper({ - id: "testembed", - rank: 23, - for: MWEmbedType.OPENLOAD, - - async getStream({ progress }) { - await timeout(1000); - progress(25); - await timeout(1000); - progress(50); - await timeout(1000); - progress(75); - throw new Error("failed to load or something"); - await timeout(1000); - return { - streamUrl: "hello-world", - type: MWStreamType.MP4, - quality: MWStreamQuality.Q1080P, - }; - }, -}); diff --git a/src/backend/embeds/testEmbedScraperTwo.ts b/src/backend/embeds/testEmbedScraperTwo.ts deleted file mode 100644 index 7d409d98..00000000 --- a/src/backend/embeds/testEmbedScraperTwo.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { MWEmbedType } from "../helpers/embed"; -import { registerEmbedScraper } from "../helpers/register"; -import { MWStreamQuality, MWStreamType } from "../helpers/streams"; - -const timeout = (time: number) => - new Promise((resolve) => { - setTimeout(() => resolve(), time); - }); - -registerEmbedScraper({ - id: "testembedtwo", - rank: 19, - for: MWEmbedType.ANOTHER, - - async getStream({ progress }) { - progress(75); - await timeout(1000); - return { - streamUrl: "hello-world-5", - type: MWStreamType.MP4, - quality: MWStreamQuality.Q1080P, - }; - }, -}); diff --git a/src/backend/helpers/provider.ts b/src/backend/helpers/provider.ts index 38e4bbfa..545a39e6 100644 --- a/src/backend/helpers/provider.ts +++ b/src/backend/helpers/provider.ts @@ -1,3 +1,4 @@ +import { DetailedMeta } from "../metadata/getmeta"; import { MWMediaType } from "../metadata/types"; import { MWEmbed } from "./embed"; import { MWStream } from "./streams"; @@ -9,8 +10,7 @@ export type MWProviderScrapeResult = { export type MWProviderContext = { progress(percentage: number): void; - imdbId: string; - tmdbId: string; + media: DetailedMeta; }; export type MWProvider = { diff --git a/src/backend/helpers/run.ts b/src/backend/helpers/run.ts index 6b3a548d..f2f9bc9c 100644 --- a/src/backend/helpers/run.ts +++ b/src/backend/helpers/run.ts @@ -28,7 +28,7 @@ export async function runProvider( const data = await provider.scrape(ctx); return sortProviderResult(data); } catch (err) { - console.error("Failed to run provider", { + console.error("Failed to run provider", err, { id: provider.id, ctx: { ...ctx }, }); diff --git a/src/backend/helpers/scrape.ts b/src/backend/helpers/scrape.ts index fe0dc61b..0683a2e6 100644 --- a/src/backend/helpers/scrape.ts +++ b/src/backend/helpers/scrape.ts @@ -2,6 +2,7 @@ import { MWProviderScrapeResult } from "./provider"; import { getEmbedScraperByType, getProviders } from "./register"; import { runEmbedScraper, runProvider } from "./run"; import { MWStream } from "./streams"; +import { DetailedMeta } from "../metadata/getmeta"; interface MWProgressData { type: "embed" | "provider"; @@ -15,8 +16,7 @@ interface MWNextData { } export interface MWProviderRunContext { - tmdb: string; - imdb: string; + media: DetailedMeta; onProgress?: (data: MWProgressData) => void; onNext?: (data: MWNextData) => void; } @@ -80,8 +80,7 @@ export async function findBestStream( let result: MWProviderScrapeResult; try { result = await runProvider(provider, { - imdbId: ctx.imdb, - tmdbId: ctx.tmdb, + media: ctx.media, progress(num) { ctx.onProgress?.({ percentage: num, diff --git a/src/backend/helpers/streams.ts b/src/backend/helpers/streams.ts index abcd9adc..628bad4a 100644 --- a/src/backend/helpers/streams.ts +++ b/src/backend/helpers/streams.ts @@ -5,7 +5,9 @@ export enum MWStreamType { export enum MWStreamQuality { Q360P = "360p", + Q720P = "720p", Q1080P = "1080p", + QUNKNOWN = "unknown", } export type MWStream = { diff --git a/src/backend/index.ts b/src/backend/index.ts index 1384e109..47587b3a 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -8,12 +8,9 @@ import { initializeScraperStore } from "./helpers/register"; // providers // -- nothing here yet -import "./providers/testProvider"; -import "./providers/testProviderTwo"; +import "./providers/gdriveplayer"; // embeds // -- nothing here yet -import "./embeds/testEmbedScraper"; -import "./embeds/testEmbedScraperTwo"; initializeScraperStore(); diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts new file mode 100644 index 00000000..e8306664 --- /dev/null +++ b/src/backend/metadata/getmeta.ts @@ -0,0 +1,61 @@ +import { formatJWMeta, JWMediaResult } from "./justwatch"; +import { MWMediaMeta, MWMediaType } from "./types"; + +const JW_API_BASE = "https://apis.justwatch.com"; + +// http://localhost:5173/#/media/movie-439596/ + +type JWExternalIdType = + | "eidr" + | "imdb_latest" + | "imdb" + | "tmdb_latest" + | "tmdb" + | "tms"; + +interface JWExternalId { + provider: JWExternalIdType; + external_id: string; +} + +interface JWDetailedMeta extends JWMediaResult { + external_ids: JWExternalId[]; +} + +export interface DetailedMeta { + meta: MWMediaMeta; + tmdbId: string; + imdbId: string; +} + +export async function getMetaFromId( + type: MWMediaType, + id: string +): Promise { + let queryType = ""; + if (type === MWMediaType.MOVIE) queryType = "movie"; + else if (type === MWMediaType.SERIES) queryType = "show"; + else if (type === MWMediaType.ANIME) + throw new Error("Anime search type is not supported"); + + const data = await fetch( + `${JW_API_BASE}/content/titles/${queryType}/${encodeURIComponent( + id + )}/locale/en_US` + ).then((res) => res.json() as Promise); + + const imdbId = data.external_ids.find( + (v) => v.provider === "imdb_latest" + )?.external_id; + const tmdbId = data.external_ids.find( + (v) => v.provider === "tmdb_latest" + )?.external_id; + + if (!imdbId || !tmdbId) throw new Error("not enough info"); + + return { + meta: formatJWMeta(data), + imdbId, + tmdbId, + }; +} diff --git a/src/backend/metadata/justwatch.ts b/src/backend/metadata/justwatch.ts new file mode 100644 index 00000000..50712bac --- /dev/null +++ b/src/backend/metadata/justwatch.ts @@ -0,0 +1,42 @@ +import { MWMediaType } from "./types"; + +export const JW_API_BASE = "https://apis.justwatch.com"; + +export type JWContentTypes = "movie" | "show"; + +export type JWMediaResult = { + title: string; + poster?: string; + id: number; + original_release_year: number; + jw_entity_id: string; + object_type: JWContentTypes; +}; + +export function mediaTypeToJW(type: MWMediaType): JWContentTypes { + if (type === MWMediaType.MOVIE) return "movie"; + if (type === MWMediaType.SERIES) return "show"; + throw new Error("unsupported type"); +} + +export function JWMediaToMediaType(type: string): MWMediaType { + if (type === "movie") return MWMediaType.MOVIE; + if (type === "show") return MWMediaType.SERIES; + throw new Error("unsupported type"); +} + +export function formatJWMeta(media: JWMediaResult) { + const type = JWMediaToMediaType(media.object_type); + return { + title: media.title, + id: media.id.toString(), + year: media.original_release_year.toString(), + poster: media.poster + ? `https://images.justwatch.com${media.poster.replace( + "{profile}", + "s166" + )}` + : undefined, + type, + }; +} diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 0b0964a7..7b3c486e 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -1,9 +1,11 @@ +import { + formatJWMeta, + JWContentTypes, + JWMediaResult, + JW_API_BASE, +} from "./justwatch"; import { MWMediaMeta, MWMediaType, MWQuery } from "./types"; -const JW_API_BASE = "https://apis.justwatch.com"; - -type JWContentTypes = "movie" | "show"; - type JWSearchQuery = { content_types: JWContentTypes[]; page: number; @@ -11,14 +13,6 @@ type JWSearchQuery = { query: string; }; -type JWSearchResults = { - title: string; - poster?: string; - id: number; - original_release_year: number; - jw_entity_id: string; -}; - type JWPage = { items: T[]; page: number; @@ -46,15 +40,7 @@ export async function searchForMedia({ `${JW_API_BASE}/content/titles/en_US/popular?body=${encodeURIComponent( JSON.stringify(body) )}` - ).then((res) => res.json() as Promise>); + ).then((res) => res.json() as Promise>); - return data.items.map((v) => ({ - title: v.title, - id: v.id.toString(), - year: v.original_release_year.toString(), - poster: v.poster - ? `https://images.justwatch.com${v.poster.replace("{profile}", "s166")}` - : undefined, - type, - })); + return data.items.map((v) => formatJWMeta(v)); } diff --git a/src/backend/providers/testProvider.ts b/src/backend/providers/flixhq.ts similarity index 97% rename from src/backend/providers/testProvider.ts rename to src/backend/providers/flixhq.ts index c00615c8..e5b5d1fe 100644 --- a/src/backend/providers/testProvider.ts +++ b/src/backend/providers/flixhq.ts @@ -10,6 +10,7 @@ registerProvider({ id: "testprov", rank: 42, type: [MWMediaType.MOVIE], + disabled: true, async scrape({ progress }) { await timeout(1000); diff --git a/src/backend/providers/gdriveplayer.ts b/src/backend/providers/gdriveplayer.ts new file mode 100644 index 00000000..492257a3 --- /dev/null +++ b/src/backend/providers/gdriveplayer.ts @@ -0,0 +1,96 @@ +import { conf } from "@/setup/config"; +import { registerProvider } from "@/backend/helpers/register"; +import { MWMediaType } from "@/backend/metadata/types"; +import { MWStreamQuality } from "@/backend/helpers/streams"; + +import { unpack } from "unpacker"; +import CryptoJS from "crypto-js"; + +const format = { + stringify: (cipher: any) => { + const ct = cipher.ciphertext.toString(CryptoJS.enc.Base64); + const iv = cipher.iv.toString() || ""; + const salt = cipher.salt.toString() || ""; + return JSON.stringify({ + ct, + iv, + salt, + }); + }, + parse: (jsonStr: string) => { + const json = JSON.parse(jsonStr); + const ciphertext = CryptoJS.enc.Base64.parse(json.ct); + const iv = CryptoJS.enc.Hex.parse(json.iv) || ""; + const salt = CryptoJS.enc.Hex.parse(json.s) || ""; + + const cipher = CryptoJS.lib.CipherParams.create({ + ciphertext, + iv, + salt, + }); + return cipher; + }, +}; + +registerProvider({ + id: "gdriveplayer", + rank: 69, + type: [MWMediaType.MOVIE], + + async scrape({ media: { imdbId } }) { + const streamRes = await fetch( + `${ + conf().CORS_PROXY_URL + }https://database.gdriveplayer.us/player.php?imdb=${imdbId}` + ).then((d) => d.text()); + const page = new DOMParser().parseFromString(streamRes, "text/html"); + + const script: HTMLElement | undefined = Array.from( + page.querySelectorAll("script") + ).find((e) => e.textContent?.includes("eval")); + + if (!script || !script.textContent) { + throw new Error("Could not find stream"); + } + + /// NOTE: this code requires re-write, it's not safe + const data = unpack(script.textContent) + .split("var data=\\'")[1] + .split("\\'")[0] + .replace(/\\/g, ""); + const decryptedData = unpack( + CryptoJS.AES.decrypt( + data, + "alsfheafsjklNIWORNiolNIOWNKLNXakjsfwnBdwjbwfkjbJjkopfjweopjASoiwnrflakefneiofrt", + { format } + ).toString(CryptoJS.enc.Utf8) + ); + // eslint-disable-next-line + const sources = JSON.parse( + JSON.stringify( + eval( + decryptedData + .split("sources:")[1] + .split(",image")[0] + .replace(/\\/g, "") + .replace(/document\.referrer/g, '""') + ) + ) + ); + const source = sources[sources.length - 1]; + /// END + + let quality; + if (source.label === "720p") quality = MWStreamQuality.Q720P; + else quality = MWStreamQuality.QUNKNOWN; + + return { + stream: { + streamUrl: `https:${source.file}`, + type: source.type, + quality, + }, + embeds: [], + }; + }, +}); diff --git a/src/backend/providers/superstream/LICENSE b/src/backend/providers/superstream/LICENSE new file mode 100644 index 00000000..3f5347b0 --- /dev/null +++ b/src/backend/providers/superstream/LICENSE @@ -0,0 +1,680 @@ +Credit goes to @ImZaw and @Blatzar from https://github.com/recloudstream/cloudstream +All files in the current directory (src/providers/list/superstream) are derived from https://github.com/recloudstream/cloudstream-extensions/blob/master/SuperStream/src/main/kotlin/com/lagradost/SuperStream.kt +Below is the license associated with the source of the derived work. + + + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/src/backend/providers/superstream/superstream.ts b/src/backend/providers/superstream/superstream.ts new file mode 100644 index 00000000..86987ecd --- /dev/null +++ b/src/backend/providers/superstream/superstream.ts @@ -0,0 +1,205 @@ +import { registerProvider } from "@/backend/helpers/register"; +import { MWMediaType } from "@/backend/metadata/types"; +import { conf } from "@/setup/config"; + +import { customAlphabet } from "nanoid"; +// import toWebVTT from "srt-webvtt"; +import CryptoJS from "crypto-js"; + +const nanoid = customAlphabet("0123456789abcdef", 32); + +// CONSTANTS, read below (taken from og) +// We do not want content scanners to notice this scraping going on so we've hidden all constants +// The source has its origins in China so I added some extra security with banned words +// Mayhaps a tiny bit unethical, but this source is just too good :) +// If you are copying this code please use precautions so they do not change their api. +const iv = atob("d0VpcGhUbiE="); +const key = atob("MTIzZDZjZWRmNjI2ZHk1NDIzM2FhMXc2"); +const apiUrls = [ + atob("aHR0cHM6Ly9zaG93Ym94LnNoZWd1Lm5ldC9hcGkvYXBpX2NsaWVudC9pbmRleC8="), + atob("aHR0cHM6Ly9tYnBhcGkuc2hlZ3UubmV0L2FwaS9hcGlfY2xpZW50L2luZGV4Lw=="), +]; +const appKey = atob("bW92aWVib3g="); +const appId = atob("Y29tLnRkby5zaG93Ym94"); + +// cryptography stuff +const crypto = { + encrypt(str: string) { + return CryptoJS.TripleDES.encrypt(str, CryptoJS.enc.Utf8.parse(key), { + iv: CryptoJS.enc.Utf8.parse(iv), + }).toString(); + }, + getVerify(str: string, str2: string, str3: string) { + if (str) { + return CryptoJS.MD5( + CryptoJS.MD5(str2).toString() + str3 + str + ).toString(); + } + return null; + }, +}; + +// get expire time +const expiry = () => Math.floor(Date.now() / 1000 + 60 * 60 * 12); + +// sending requests +const get = (data: object, altApi = false) => { + const defaultData = { + childmode: "0", + app_version: "11.5", + appid: appId, + lang: "en", + expired_date: `${expiry()}`, + platform: "android", + channel: "Website", + }; + const encryptedData = crypto.encrypt( + JSON.stringify({ + ...defaultData, + ...data, + }) + ); + const appKeyHash = CryptoJS.MD5(appKey).toString(); + const verify = crypto.getVerify(encryptedData, appKey, key); + const body = JSON.stringify({ + app_key: appKeyHash, + verify, + encrypt_data: encryptedData, + }); + const b64Body = btoa(body); + + const formatted = new URLSearchParams(); + formatted.append("data", b64Body); + formatted.append("appid", "27"); + formatted.append("platform", "android"); + formatted.append("version", "129"); + formatted.append("medium", "Website"); + + const requestUrl = altApi ? apiUrls[1] : apiUrls[0]; + return fetch(`${conf().CORS_PROXY_URL}${requestUrl}`, { + method: "POST", + headers: { + Platform: "android", + "Content-Type": "application/x-www-form-urlencoded", + }, + body: `${formatted.toString()}&token${nanoid()}`, + }); +}; + +registerProvider({ + id: "superstream", + rank: 50, + type: [MWMediaType.MOVIE, MWMediaType.SERIES], + disabled: true, + + async scrape({ + media: { + meta: { type }, + tmdbId, + }, + }) { + if (type === MWMediaType.MOVIE) { + const apiQuery = { + uid: "", + module: "Movie_downloadurl_v3", + mid: tmdbId, + oss: "1", + group: "", + }; + + const mediaRes = (await get(apiQuery).then((r) => r.json())).data; + const hdQuality = + mediaRes.list.find( + (quality: any) => quality.quality === "1080p" && quality.path + ) ?? + mediaRes.list.find( + (quality: any) => quality.quality === "720p" && quality.path + ) ?? + mediaRes.list.find( + (quality: any) => quality.quality === "480p" && quality.path + ) ?? + mediaRes.list.find( + (quality: any) => quality.quality === "360p" && quality.path + ); + + if (!hdQuality) throw new Error("No quality could be found."); + + // const subtitleApiQuery = { + // fid: hdQuality.fid, + // uid: "", + // module: "Movie_srt_list_v2", + // mid: tmdbId, + // }; + + // const subtitleRes = (await get(subtitleApiQuery).then((r) => r.json())) + // .data; + // const mappedCaptions = await Promise.all( + // subtitleRes.list.map(async (subtitle: any) => { + // const captionBlob = await fetch( + // `${conf().CORS_PROXY_URL}${subtitle.subtitles[0].file_path}` + // ).then((captionRes) => captionRes.blob()); // cross-origin bypass + // const captionUrl = await toWebVTT(captionBlob); // convert to vtt so it's playable + // return { + // id: subtitle.language, + // url: captionUrl, + // label: subtitle.language, + // }; + // }) + // ); + + return { embeds: [], stream: hdQuality.path }; + } + + // const apiQuery = { + // uid: "", + // module: "TV_downloadurl_v3", + // episode: media.episodeId, + // tid: media.mediaId, + // season: media.seasonId, + // oss: "1", + // group: "", + // }; + // const mediaRes = (await get(apiQuery).then((r) => r.json())).data; + // const hdQuality = + // mediaRes.list.find( + // (quality: any) => quality.quality === "1080p" && quality.path + // ) ?? + // mediaRes.list.find( + // (quality: any) => quality.quality === "720p" && quality.path + // ) ?? + // mediaRes.list.find( + // (quality: any) => quality.quality === "480p" && quality.path + // ) ?? + // mediaRes.list.find( + // (quality: any) => quality.quality === "360p" && quality.path + // ); + + // if (!hdQuality) throw new Error("No quality could be found."); + + // const subtitleApiQuery = { + // fid: hdQuality.fid, + // uid: "", + // module: "TV_srt_list_v2", + // episode: media.episodeId, + // tid: media.mediaId, + // season: media.seasonId, + // }; + // const subtitleRes = (await get(subtitleApiQuery).then((r) => r.json())) + // .data; + // const mappedCaptions = await Promise.all( + // subtitleRes.list.map(async (subtitle: any) => { + // const captionBlob = await fetch( + // `${conf().CORS_PROXY_URL}${subtitle.subtitles[0].file_path}` + // ).then((captionRes) => captionRes.blob()); // cross-origin bypass + // const captionUrl = await toWebVTT(captionBlob); // convert to vtt so it's playable + // return { + // id: subtitle.language, + // url: captionUrl, + // label: subtitle.language, + // }; + // }) + // ); + + return { embeds: [] }; + }, +}); diff --git a/src/backend/providers/testProviderTwo.ts b/src/backend/providers/testProviderTwo.ts deleted file mode 100644 index f3a39fbf..00000000 --- a/src/backend/providers/testProviderTwo.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { MWEmbedType } from "../helpers/embed"; -import { registerProvider } from "../helpers/register"; -import { MWMediaType } from "../metadata/types"; - -const timeout = (time: number) => - new Promise((resolve) => { - setTimeout(() => resolve(), time); - }); - -registerProvider({ - id: "testprov2", - rank: 40, - type: [MWMediaType.MOVIE], - - async scrape({ progress }) { - await timeout(1000); - progress(25); - await timeout(1000); - progress(50); - await timeout(1000); - progress(75); - await timeout(1000); - - return { - embeds: [ - { - type: MWEmbedType.OPENLOAD, - url: "https://google.com", - }, - { - type: MWEmbedType.ANOTHER, - url: "https://google.com", - }, - ], - }; - }, -}); diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index ffc7a49b..f4287571 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -1,6 +1,6 @@ import { Link } from "react-router-dom"; import { DotList } from "@/components/text/DotList"; -import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types"; +import { MWMediaMeta } from "@/backend/metadata/types"; export interface MediaCardProps { media: MWMediaMeta; @@ -37,14 +37,15 @@ function MediaCardContent({ media, linkable }: MediaCardProps) { } export function MediaCard(props: MediaCardProps) { - let link = "movie"; - if (props.media.type === MWMediaType.SERIES) link = "series"; - const content = ; if (!props.linkable) return {content}; return ( - + {content} ); diff --git a/src/components/video/VideoContext.tsx b/src/components/video/VideoContext.tsx index cbee333b..8cbc8e4a 100644 --- a/src/components/video/VideoContext.tsx +++ b/src/components/video/VideoContext.tsx @@ -1,3 +1,4 @@ +import { MWStreamType } from "@/backend/helpers/streams"; import React, { createContext, MutableRefObject, @@ -13,17 +14,17 @@ import { interface VideoPlayerContextType { source: string | null; - sourceType: "m3u8" | "mp4"; + sourceType: MWStreamType; state: PlayerContext; } const initial: VideoPlayerContextType = { source: null, - sourceType: "mp4", + sourceType: MWStreamType.MP4, state: initialPlayerState, }; type VideoPlayerContextAction = - | { type: "SET_SOURCE"; url: string; sourceType: "m3u8" | "mp4" } + | { type: "SET_SOURCE"; url: string; sourceType: MWStreamType } | { type: "UPDATE_PLAYER"; state: PlayerContext; diff --git a/src/components/video/VideoPlayer.tsx b/src/components/video/VideoPlayer.tsx index 0f914693..02d71440 100644 --- a/src/components/video/VideoPlayer.tsx +++ b/src/components/video/VideoPlayer.tsx @@ -39,7 +39,7 @@ export function VideoPlayer(props: VideoPlayerProps) { return (
({ ...s, leftControlHovering: hovering })); }, - initPlayer(sourceUrl: string, sourceType: "m3u8" | "mp4") { + initPlayer(sourceUrl: string, sourceType: MWStreamType) { this.setVolume(getStoredVolume()); - if (sourceType === "m3u8") { + if (sourceType === MWStreamType.HLS) { if (player.canPlayType("application/vnd.apple.mpegurl")) { player.src = sourceUrl; } else { @@ -125,7 +126,7 @@ export function populateControls( hls.attachMedia(player); hls.loadSource(sourceUrl); } - } else if (sourceType === "mp4") { + } else if (sourceType === MWStreamType.MP4) { player.src = sourceUrl; } }, diff --git a/src/hooks/useScrape.ts b/src/hooks/useScrape.ts index 2104e5f6..1a24eb4f 100644 --- a/src/hooks/useScrape.ts +++ b/src/hooks/useScrape.ts @@ -1,5 +1,6 @@ import { findBestStream } from "@/backend/helpers/scrape"; import { MWStream } from "@/backend/helpers/streams"; +import { DetailedMeta } from "@/backend/metadata/getmeta"; import { useEffect, useState } from "react"; interface ScrapeEventLog { @@ -9,7 +10,7 @@ interface ScrapeEventLog { id: string; } -export function useScrape() { +export function useScrape(meta: DetailedMeta) { const [eventLog, setEventLog] = useState([]); const [stream, setStream] = useState(null); const [pending, setPending] = useState(true); @@ -19,10 +20,8 @@ export function useScrape() { setStream(null); setEventLog([]); (async () => { - // TODO has test inputs const scrapedStream = await findBestStream({ - imdb: "test1", - tmdb: "test2", + media: meta, onNext(ctx) { setEventLog((arr) => [ ...arr, @@ -49,7 +48,7 @@ export function useScrape() { setPending(false); setStream(scrapedStream); })(); - }, []); + }, [meta]); return { stream, diff --git a/src/setup/App.tsx b/src/setup/App.tsx index ebe4f016..629f7c04 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -5,7 +5,6 @@ import { WatchedContextProvider } from "@/state/watched"; import { NotFoundPage } from "@/views/notfound/NotFoundView"; import { MediaView } from "@/views/MediaView"; import { SearchView } from "@/views/search/SearchView"; -import { TestView } from "@/views/TestView"; import { MWMediaType } from "@/backend/metadata/types"; function App() { @@ -16,10 +15,8 @@ function App() { - - + - diff --git a/src/state/bookmark/context.tsx b/src/state/bookmark/context.tsx index 2217908f..977b4596 100644 --- a/src/state/bookmark/context.tsx +++ b/src/state/bookmark/context.tsx @@ -31,13 +31,7 @@ function getBookmarkIndexFromMedia( bookmarks: MWMediaMeta[], media: MWMediaMeta ): number { - const a = bookmarks.findIndex( - (v) => - v.mediaId === media.mediaId && - v.providerId === media.providerId && - v.episodeId === media.episodeId && - v.seasonId === media.seasonId - ); + const a = bookmarks.findIndex((v) => v.id === media.id); return a; } @@ -64,19 +58,19 @@ export function BookmarkContextProvider(props: { children: ReactNode }) { const contextValue = useMemo( () => ({ - setItemBookmark(media: any, bookmarked: boolean) { + setItemBookmark(media: MWMediaMeta, bookmarked: boolean) { setBookmarked((data: BookmarkStoreData) => { if (bookmarked) { const itemIndex = getBookmarkIndexFromMedia(data.bookmarks, media); if (itemIndex === -1) { const item = { - mediaId: media.mediaId, - mediaType: media.mediaType, - providerId: media.providerId, + id: media.id, + type: media.type, + // providerId: media.providerId, title: media.title, year: media.year, - episodeId: media.episodeId, - seasonId: media.seasonId, + // episodeId: media.episodeId, + // seasonId: media.seasonId, }; data.bookmarks.push(item); } diff --git a/src/state/watched/context.tsx b/src/state/watched/context.tsx index 4fa68f3e..c434801e 100644 --- a/src/state/watched/context.tsx +++ b/src/state/watched/context.tsx @@ -1,4 +1,4 @@ -import { MWMediaMeta } from "@/backend/metadata/types"; +import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types"; import React, { createContext, ReactNode, @@ -94,42 +94,40 @@ export function WatchedContextProvider(props: { children: ReactNode }) { // let filtered = watched.items.filter( // (item) => getProviderMetadata(item.providerId)?.enabled // ); + let filtered = watched.items; + // // get highest episode number for every anime/season - // const highestEpisode: Record = {}; - // const highestWatchedItem: Record = {}; - // filtered = filtered.filter((item) => { - // if ( - // [MWMediaType.ANIME, MWMediaType.SERIES].includes(item.mediaType) - // ) { - // const key = `${item.mediaType}-${item.mediaId}`; - // const current: [number, number] = [ - // item.episodeId ? parseInt(item.episodeId, 10) : -1, - // item.seasonId ? parseInt(item.seasonId, 10) : -1, - // ]; - // let existing = highestEpisode[key]; - // if (!existing) { - // existing = current; - // highestEpisode[key] = current; - // highestWatchedItem[key] = item; - // } - // if ( - // current[0] > existing[0] || - // (current[0] === existing[0] && current[1] > existing[1]) - // ) { - // highestEpisode[key] = current; - // highestWatchedItem[key] = item; - // } - // return false; - // } - // return true; - // }); - // return [...filtered, ...Object.values(highestWatchedItem)]; + const highestEpisode: Record = {}; + const highestWatchedItem: Record = {}; + filtered = filtered.filter((item) => { + if ([MWMediaType.ANIME, MWMediaType.SERIES].includes(item.type)) { + const key = `${item.type}-${item.id}`; + const current: [number, number] = [ + item.episodeId ? parseInt(item.episodeId, 10) : -1, + item.seasonId ? parseInt(item.seasonId, 10) : -1, + ]; + let existing = highestEpisode[key]; + if (!existing) { + existing = current; + highestEpisode[key] = current; + highestWatchedItem[key] = item; + } + if ( + current[0] > existing[0] || + (current[0] === existing[0] && current[1] > existing[1]) + ) { + highestEpisode[key] = current; + highestWatchedItem[key] = item; + } + return false; + } + return true; + }); + return [...filtered, ...Object.values(highestWatchedItem)]; }, watched, }), - [ - /*watched, setWatched*/ - ] + [watched] ); return ( diff --git a/src/views/MediaView.tsx b/src/views/MediaView.tsx index e9bedc53..26637a46 100644 --- a/src/views/MediaView.tsx +++ b/src/views/MediaView.tsx @@ -1,22 +1,88 @@ -import { useHistory } from "react-router-dom"; -import { useCallback } from "react"; +import { useHistory, useParams } from "react-router-dom"; +import { useCallback, useEffect, useState } from "react"; import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer"; +import { MWStream } from "@/backend/helpers/streams"; +import { useScrape } from "@/hooks/useScrape"; +import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader"; +import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta"; +import { JWMediaToMediaType } from "@/backend/metadata/justwatch"; +import { SourceControl } from "@/components/video/controls/SourceControl"; + +function MediaViewLoading() { + return

Loading meta...

; +} + +interface MediaViewScrapingProps { + onStream(stream: MWStream): void; + onGoBack(): void; + meta: DetailedMeta; +} +function MediaViewScraping(props: MediaViewScrapingProps) { + const { eventLog, pending, stream } = useScrape(props.meta); + + useEffect(() => { + if (stream) { + props.onStream(stream); + } + }, [stream, props]); + + return ( +
+ +

pending: {pending.toString()}

+

+ stream: {stream?.streamUrl} - {stream?.type} - {stream?.quality} +

+
+ {eventLog.map((v) => ( +
+

+ {v.percentage}% - {v.type} - {v.errored ? "ERROR" : "pending"} +

+
+ ))} +
+ ); +} export function MediaView() { const reactHistory = useHistory(); + const params = useParams<{ media: string }>(); const goBack = useCallback(() => { if (reactHistory.action !== "POP") reactHistory.goBack(); else reactHistory.push("/"); }, [reactHistory]); - // TODO fetch meta - // TODO call useScrape + const [meta, setMeta] = useState(null); + const [stream, setStream] = useState(null); + + useEffect(() => { + (async () => { + const [t, id] = params.media.split("-", 2); + const type = JWMediaToMediaType(t); + const fetchedMeta = await getMetaFromId(type, id); + setMeta(fetchedMeta); + })(); + }, [setMeta, params]); + // TODO not found checks // TODO watched store // TODO scrape loading state // TODO error page with video header + if (!meta) return ; + if (!stream) + return ( + + ); return ( - +
+ + + +
); } diff --git a/src/views/TestView.tsx b/src/views/TestView.tsx deleted file mode 100644 index dd45fdb0..00000000 --- a/src/views/TestView.tsx +++ /dev/null @@ -1,82 +0,0 @@ -// import { searchForMedia } from "@/backend/metadata/search"; -// import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl"; -// import { SourceControl } from "@/components/video/controls/SourceControl"; -// import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer"; -import { useScrape } from "@/hooks/useScrape"; -// import { MWMediaType } from "@/providers"; -// import { useCallback, useState } from "react"; - -// test videos: https://gist.github.com/jsturgis/3b19447b304616f18657 - -// TODO video todos: -// - error handling -// - captions -// - mobile UI -// - safari fullscreen will make video overlap player controls -// - safari progress bar is fucked (video doesnt change time but video.currentTime does change) - -// TODO optional todos: -// - shortcuts when player is active -// - improve seekables (if possible) - -// TODO stuff to test: -// - browser: firefox, chrome, edge, safari desktop -// - phones: android firefox, android chrome, iphone safari -// - devices: ipadOS -// - features: HLS, error handling, preload interactions - -// export function TestView() { -// const [show, setShow] = useState(true); -// const handleClick = useCallback(() => { -// setShow((v) => !v); -// }, [setShow]); - -// if (!show) { -// return

Click me to show

; -// } - -// async function search() { -// const test = await searchForMedia({ -// searchQuery: "tron", -// type: MWMediaType.MOVIE, -// }); -// console.log(test); -// } - -// return ( -//
-// -// -// console.log(a, b)} -// /> -// -//

search()}>click me to search

-//
-// ); -// } - -export function TestView() { - const { eventLog, pending, stream } = useScrape(); - - return ( -
-

pending: {pending}

-

- stream: {stream?.streamUrl} - {stream?.type} - {stream?.quality} -

-
- {eventLog.map((v) => ( -
-

- {v.percentage}% - {v.type} - {v.errored ? "ERROR" : "pending"} -

-
- ))} -
- ); -} diff --git a/src/views/search/HomeView.tsx b/src/views/search/HomeView.tsx index 7a94cdcb..ac745ede 100644 --- a/src/views/search/HomeView.tsx +++ b/src/views/search/HomeView.tsx @@ -32,41 +32,41 @@ function Bookmarks() { ); } -function Watched() { - const { t } = useTranslation(); - const { getFilteredBookmarks } = useBookmarkContext(); - const { getFilteredWatched } = useWatchedContext(); +// function Watched() { +// const { t } = useTranslation(); +// const { getFilteredBookmarks } = useBookmarkContext(); +// const { getFilteredWatched } = useWatchedContext(); - const bookmarks = getFilteredBookmarks(); - const watchedItems = getFilteredWatched().filter( - (v) => !getIfBookmarkedFromPortable(bookmarks, v) - ); +// const bookmarks = getFilteredBookmarks(); +// const watchedItems = getFilteredWatched().filter( +// (v) => !getIfBookmarkedFromPortable(bookmarks, v) +// ); - if (watchedItems.length === 0) return null; +// if (watchedItems.length === 0) return null; - return ( - - - {/* {watchedItems.map((v) => ( - - ))} */} - - - ); -} +// return ( +// +// +// {/* {watchedItems.map((v) => ( +// +// ))} */} +// +// +// ); +// } export function HomeView() { return (
- - + {/* */} + {/* */}
); } diff --git a/src/views/search/SearchResultsView.tsx b/src/views/search/SearchResultsView.tsx index 5c67e30e..60a61115 100644 --- a/src/views/search/SearchResultsView.tsx +++ b/src/views/search/SearchResultsView.tsx @@ -65,8 +65,6 @@ export function SearchResultsView({ searchQuery }: { searchQuery: MWQuery }) { if (error) return ; if (!results) return null; - // TODO on click go to the right page with id instead of portable - return (
{results.length > 0 ? ( From 4d07751a4a45fa8fc259d6c2fc0e911154fced8b Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sat, 14 Jan 2023 00:27:40 +0100 Subject: [PATCH 030/135] first load spinner --- src/components/video/controls/LoadingControl.tsx | 4 +++- src/components/video/controls/MiddlePauseControl.tsx | 1 + src/components/video/hooks/useVideoPlayer.ts | 11 +++++++++++ src/views/search/SearchResultsView.tsx | 2 ++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/components/video/controls/LoadingControl.tsx b/src/components/video/controls/LoadingControl.tsx index eeea8d8f..8b38cef9 100644 --- a/src/components/video/controls/LoadingControl.tsx +++ b/src/components/video/controls/LoadingControl.tsx @@ -4,7 +4,9 @@ import { useVideoPlayerState } from "../VideoContext"; export function LoadingControl() { const { videoState } = useVideoPlayerState(); - if (!videoState.isLoading) return null; + const isLoading = videoState.isFirstLoading || videoState.isLoading; + + if (!isLoading) return null; return ; } diff --git a/src/components/video/controls/MiddlePauseControl.tsx b/src/components/video/controls/MiddlePauseControl.tsx index 9bbbe08c..b934db0a 100644 --- a/src/components/video/controls/MiddlePauseControl.tsx +++ b/src/components/video/controls/MiddlePauseControl.tsx @@ -12,6 +12,7 @@ export function MiddlePauseControl() { if (videoState.hasPlayedOnce) return null; if (videoState.isPlaying) return null; + if (videoState.isFirstLoading) return null; return (
{ + update((s) => ({ + ...s, + isFirstLoading: false, + })); + }; player.addEventListener("pause", pause); player.addEventListener("playing", playing); @@ -133,6 +142,7 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { player.addEventListener("volumechange", volumechange); player.addEventListener("progress", progress); player.addEventListener("waiting", waiting); + player.addEventListener("canplay", canplay); return () => { player.removeEventListener("pause", pause); @@ -145,6 +155,7 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { player.removeEventListener("volumechange", volumechange); player.removeEventListener("progress", progress); player.removeEventListener("waiting", waiting); + player.removeEventListener("canplay", canplay); }; } diff --git a/src/views/search/SearchResultsView.tsx b/src/views/search/SearchResultsView.tsx index 60a61115..20764b22 100644 --- a/src/views/search/SearchResultsView.tsx +++ b/src/views/search/SearchResultsView.tsx @@ -52,6 +52,8 @@ export function SearchResultsView({ searchQuery }: { searchQuery: MWQuery }) { ); useEffect(() => { + // TODO use cache + // TODO run immediately without debounce on mount async function runSearch(query: MWQuery) { const searchResults = await runSearchQuery(query); if (!searchResults) return; From 5967c83d28dbcb9baf49ea0c4ce828fc42b01d5b Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sat, 14 Jan 2023 00:30:50 +0100 Subject: [PATCH 031/135] add deleted todos back --- src/index.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/index.tsx b/src/index.tsx index fbb6e122..1b20371c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -16,6 +16,23 @@ if (key) { (window as any).initMW(conf().BASE_PROXY_URL, key); } +// TODO video todos: +// - error handling +// - captions +// - mobile UI +// - safari fullscreen will make video overlap player controls +// - safari progress bar is fucked (video doesnt change time but video.currentTime does change) + +// TODO optional todos: +// - shortcuts when player is active +// - improve seekables (if possible) + +// TODO stuff to test: +// - browser: firefox, chrome, edge, safari desktop +// - phones: android firefox, android chrome, iphone safari +// - devices: ipadOS +// - features: HLS, error handling, preload interactions + ReactDOM.render( From cf83df64bbdc3a0689af29201994e562e8a1ec71 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sat, 14 Jan 2023 00:34:13 +0100 Subject: [PATCH 032/135] add some todos --- src/backend/index.ts | 1 + src/views/MediaView.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backend/index.ts b/src/backend/index.ts index 47587b3a..ee267e67 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -5,6 +5,7 @@ import { initializeScraperStore } from "./helpers/register"; // - hooks to run all providers one by one // - move over old providers to new system // - implement jons providers/embedscrapers +// - show/episode support // providers // -- nothing here yet diff --git a/src/views/MediaView.tsx b/src/views/MediaView.tsx index 26637a46..926f4c54 100644 --- a/src/views/MediaView.tsx +++ b/src/views/MediaView.tsx @@ -60,6 +60,7 @@ export function MediaView() { const [stream, setStream] = useState(null); useEffect(() => { + // TODO handle errors (async () => { const [t, id] = params.media.split("-", 2); const type = JWMediaToMediaType(t); @@ -68,7 +69,6 @@ export function MediaView() { })(); }, [setMeta, params]); - // TODO not found checks // TODO watched store // TODO scrape loading state // TODO error page with video header From 2f1058cb9c4728e2118a3143f1f1100d9803ec08 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sat, 14 Jan 2023 01:37:47 +0100 Subject: [PATCH 033/135] loading screen usescrape --- src/backend/providers/gdriveplayer.ts | 4 +- src/components/Icon.tsx | 2 + src/components/media/MediaCard.tsx | 7 +-- src/hooks/useScrape.ts | 2 +- src/setup/App.tsx | 2 +- src/views/media/MediaScrapeLog.tsx | 78 +++++++++++++++++++++++++++ src/views/{ => media}/MediaView.tsx | 52 ++++++++++-------- 7 files changed, 119 insertions(+), 28 deletions(-) create mode 100644 src/views/media/MediaScrapeLog.tsx rename src/views/{ => media}/MediaView.tsx (62%) diff --git a/src/backend/providers/gdriveplayer.ts b/src/backend/providers/gdriveplayer.ts index 492257a3..60c7ac4f 100644 --- a/src/backend/providers/gdriveplayer.ts +++ b/src/backend/providers/gdriveplayer.ts @@ -37,12 +37,14 @@ registerProvider({ rank: 69, type: [MWMediaType.MOVIE], - async scrape({ media: { imdbId } }) { + async scrape({ progress, media: { imdbId } }) { + progress(10); const streamRes = await fetch( `${ conf().CORS_PROXY_URL }https://database.gdriveplayer.us/player.php?imdb=${imdbId}` ).then((d) => d.text()); + progress(90); const page = new DOMParser().parseFromString(streamRes, "text/html"); const script: HTMLElement | undefined = Array.from( diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 18398ace..7866066f 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -22,6 +22,7 @@ export enum Icons { COMPRESS = "compress", VOLUME = "volume", VOLUME_X = "volume_x", + X = "x", } export interface IconProps { @@ -51,6 +52,7 @@ const iconList: Record = { compress: ``, volume: ``, volume_x: ``, + x: ``, }; export const Icon = memo((props: IconProps) => { diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index f4287571..7b63bf7e 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -1,6 +1,7 @@ import { Link } from "react-router-dom"; import { DotList } from "@/components/text/DotList"; import { MWMediaMeta } from "@/backend/metadata/types"; +import { mediaTypeToJW } from "@/backend/metadata/justwatch"; export interface MediaCardProps { media: MWMediaMeta; @@ -42,9 +43,9 @@ export function MediaCard(props: MediaCardProps) { if (!props.linkable) return {content}; return ( {content} diff --git a/src/hooks/useScrape.ts b/src/hooks/useScrape.ts index 1a24eb4f..ca0004ed 100644 --- a/src/hooks/useScrape.ts +++ b/src/hooks/useScrape.ts @@ -3,7 +3,7 @@ import { MWStream } from "@/backend/helpers/streams"; import { DetailedMeta } from "@/backend/metadata/getmeta"; import { useEffect, useState } from "react"; -interface ScrapeEventLog { +export interface ScrapeEventLog { type: "provider" | "embed"; errored: boolean; percentage: number; diff --git a/src/setup/App.tsx b/src/setup/App.tsx index 629f7c04..563a8643 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -3,7 +3,7 @@ import { BookmarkContextProvider } from "@/state/bookmark"; import { WatchedContextProvider } from "@/state/watched"; import { NotFoundPage } from "@/views/notfound/NotFoundView"; -import { MediaView } from "@/views/MediaView"; +import { MediaView } from "@/views/media/MediaView"; import { SearchView } from "@/views/search/SearchView"; import { MWMediaType } from "@/backend/metadata/types"; diff --git a/src/views/media/MediaScrapeLog.tsx b/src/views/media/MediaScrapeLog.tsx new file mode 100644 index 00000000..20f8bb50 --- /dev/null +++ b/src/views/media/MediaScrapeLog.tsx @@ -0,0 +1,78 @@ +import { Icon, Icons } from "@/components/Icon"; +import { ScrapeEventLog } from "@/hooks/useScrape"; + +interface MediaScrapeLogProps { + events: ScrapeEventLog[]; +} + +interface MediaScrapePillProps { + event: ScrapeEventLog; +} + +function MediaScrapePillSkeleton() { + return
; +} + +function MediaScrapePill({ event }: MediaScrapePillProps) { + return ( +
+
+ {!event.errored ? ( + + + + ) : ( + + )} +
+
+

+ {event.id} +

+
+
+ ); +} + +export function MediaScrapeLog(props: MediaScrapeLogProps) { + return ( +
+
+
+
+ + {props.events.map((v) => ( + + ))} + +
+
+
+
+
+
+ ); +} diff --git a/src/views/MediaView.tsx b/src/views/media/MediaView.tsx similarity index 62% rename from src/views/MediaView.tsx rename to src/views/media/MediaView.tsx index 926f4c54..f9484c1a 100644 --- a/src/views/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -7,9 +7,21 @@ import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader"; import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta"; import { JWMediaToMediaType } from "@/backend/metadata/justwatch"; import { SourceControl } from "@/components/video/controls/SourceControl"; +import { Loading } from "@/components/layout/Loading"; +import { MediaScrapeLog } from "./MediaScrapeLog"; -function MediaViewLoading() { - return

Loading meta...

; +function MediaViewLoading(props: { onGoBack(): void }) { + return ( +
+
+ +
+
+ +

Finding the best video for you

+
+
+ ); } interface MediaViewScrapingProps { @@ -18,7 +30,7 @@ interface MediaViewScrapingProps { meta: DetailedMeta; } function MediaViewScraping(props: MediaViewScrapingProps) { - const { eventLog, pending, stream } = useScrape(props.meta); + const { eventLog, stream } = useScrape(props.meta); useEffect(() => { if (stream) { @@ -26,24 +38,21 @@ function MediaViewScraping(props: MediaViewScrapingProps) { } }, [stream, props]); + // TODO error screen if no streams found + return ( -
- -

pending: {pending.toString()}

-

- stream: {stream?.streamUrl} - {stream?.type} - {stream?.quality} -

-
- {eventLog.map((v) => ( -
-

- {v.percentage}% - {v.type} - {v.errored ? "ERROR" : "pending"} -

-
- ))} +
+
+ +
+
+ +

Finding the best video for you

+ +
); } @@ -70,10 +79,9 @@ export function MediaView() { }, [setMeta, params]); // TODO watched store - // TODO scrape loading state // TODO error page with video header - if (!meta) return ; + if (!meta) return ; if (!stream) return ( From d161c948cd2967d6d759889f75304d8a9b78c121 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sat, 14 Jan 2023 16:03:59 +0100 Subject: [PATCH 034/135] better progress indicator --- src/views/media/MediaScrapeLog.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/views/media/MediaScrapeLog.tsx b/src/views/media/MediaScrapeLog.tsx index 20f8bb50..5d7ed3b1 100644 --- a/src/views/media/MediaScrapeLog.tsx +++ b/src/views/media/MediaScrapeLog.tsx @@ -16,11 +16,17 @@ function MediaScrapePillSkeleton() { function MediaScrapePill({ event }: MediaScrapePillProps) { return (
-
+
{!event.errored ? ( + Date: Sat, 14 Jan 2023 16:14:54 +0100 Subject: [PATCH 035/135] fix debounce on first render --- src/hooks/useSearchQuery.ts | 29 +++++++++----------------- src/views/search/SearchResultsView.tsx | 1 - 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/hooks/useSearchQuery.ts b/src/hooks/useSearchQuery.ts index b20c78ab..372c70fc 100644 --- a/src/hooks/useSearchQuery.ts +++ b/src/hooks/useSearchQuery.ts @@ -1,19 +1,23 @@ import { MWMediaType, MWQuery } from "@/backend/metadata/types"; -import React, { useRef, useState } from "react"; +import { useState } from "react"; import { generatePath, useHistory, useRouteMatch } from "react-router-dom"; +function getInitialValue(params: { type: string; query: string }) { + const type = + Object.values(MWMediaType).find((v) => params.type === v) || + MWMediaType.MOVIE; + const searchQuery = params.query || ""; + return { type, searchQuery }; +} + export function useSearchQuery(): [ MWQuery, (inp: Partial, force: boolean) => void, () => void ] { const history = useHistory(); - const isFirstRender = useRef(true); const { path, params } = useRouteMatch<{ type: string; query: string }>(); - const [search, setSearch] = useState({ - searchQuery: "", - type: MWMediaType.MOVIE, - }); + const [search, setSearch] = useState(getInitialValue(params)); const updateParams = (inp: Partial, force: boolean) => { const copySearch: MWQuery = { ...search }; @@ -38,18 +42,5 @@ export function useSearchQuery(): [ ); }; - // only run on first load of the page - React.useEffect(() => { - if (isFirstRender.current === false) { - return; - } - isFirstRender.current = false; - const type = - Object.values(MWMediaType).find((v) => params.type === v) || - MWMediaType.MOVIE; - const searchQuery = params.query || ""; - setSearch({ type, searchQuery }); - }, [setSearch, params, isFirstRender]); - return [search, updateParams, onUnFocus]; } diff --git a/src/views/search/SearchResultsView.tsx b/src/views/search/SearchResultsView.tsx index 20764b22..8638ec71 100644 --- a/src/views/search/SearchResultsView.tsx +++ b/src/views/search/SearchResultsView.tsx @@ -53,7 +53,6 @@ export function SearchResultsView({ searchQuery }: { searchQuery: MWQuery }) { useEffect(() => { // TODO use cache - // TODO run immediately without debounce on mount async function runSearch(query: MWQuery) { const searchResults = await runSearchQuery(query); if (!searchResults) return; From 52b063b10a5f3795e8ad9c53b12cd738e475365b Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sun, 15 Jan 2023 16:01:07 +0100 Subject: [PATCH 036/135] bunch of todos --- package.json | 1 + public/locales/en-GB/translation.json | 4 +- src/backend/helpers/embed.ts | 2 +- src/backend/helpers/fetch.ts | 35 ++++++ src/backend/helpers/provider.ts | 15 ++- src/backend/helpers/scrape.ts | 84 +++++++++++--- src/backend/index.ts | 3 - src/backend/metadata/getmeta.ts | 42 ++++--- src/backend/metadata/justwatch.ts | 6 +- src/backend/metadata/search.ts | 44 +++++--- src/backend/providers/gdriveplayer.ts | 23 ++-- src/components/layout/ErrorBoundary.tsx | 85 ++++++++++----- .../video/parts/VideoPlayerHeader.tsx | 2 +- src/hooks/useGoBack.ts | 12 ++ src/hooks/useScrape.ts | 20 +++- src/index.tsx | 3 + src/views/media/MediaErrorView.tsx | 49 +++++++++ src/views/media/MediaScrapeLog.tsx | 2 +- src/views/media/MediaView.tsx | 103 +++++++++++++----- src/views/notfound/NotFoundChecks.tsx | 17 --- src/views/notfound/NotFoundView.tsx | 17 ++- src/views/search/SearchResultsView.tsx | 1 - yarn.lock | 24 ++++ 23 files changed, 446 insertions(+), 148 deletions(-) create mode 100644 src/backend/helpers/fetch.ts create mode 100644 src/hooks/useGoBack.ts create mode 100644 src/views/media/MediaErrorView.tsx delete mode 100644 src/views/notfound/NotFoundChecks.tsx diff --git a/package.json b/package.json index 02c93cc2..e7947361 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "json5": "^2.2.0", "lodash.throttle": "^4.1.1", "nanoid": "^4.0.0", + "ofetch": "^1.0.0", "react": "^17.0.2", "react-dom": "^17.0.2", "react-i18next": "^12.1.1", diff --git a/public/locales/en-GB/translation.json b/public/locales/en-GB/translation.json index f50d3c0b..f83d56f7 100644 --- a/public/locales/en-GB/translation.json +++ b/public/locales/en-GB/translation.json @@ -4,15 +4,13 @@ }, "search": { "loading": "Fetching your favourite shows...", - "providersFailed": "{{fails}}/{{total}} providers failed!", "allResults": "That's all we have!", "noResults": "We couldn't find anything!", - "allFailed": "All providers have failed!", + "allFailed": "Failed to find media, try again!", "headingTitle": "Search results", "headingLink": "Back to home", "bookmarks": "Bookmarks", "continueWatching": "Continue Watching", - "tagline": "Because watching legally is boring", "title": "What do you want to watch?", "placeholder": "What do you want to watch?" }, diff --git a/src/backend/helpers/embed.ts b/src/backend/helpers/embed.ts index 88c0420d..9f99b28a 100644 --- a/src/backend/helpers/embed.ts +++ b/src/backend/helpers/embed.ts @@ -2,7 +2,6 @@ import { MWStream } from "./streams"; export enum MWEmbedType { OPENLOAD = "openload", - ANOTHER = "another", } export type MWEmbed = { @@ -17,6 +16,7 @@ export type MWEmbedContext = { export type MWEmbedScraper = { id: string; + displayName: string; for: MWEmbedType; rank: number; disabled?: boolean; diff --git a/src/backend/helpers/fetch.ts b/src/backend/helpers/fetch.ts new file mode 100644 index 00000000..9804ff40 --- /dev/null +++ b/src/backend/helpers/fetch.ts @@ -0,0 +1,35 @@ +import { conf } from "@/setup/config"; +import { ofetch } from "ofetch"; + +type P = Parameters>; +type R = ReturnType>; + +const baseFetch = ofetch.create({ + retry: 0, +}); + +export function makeUrl(url: string, data: Record) { + let parsedUrl: string = url; + Object.entries(data).forEach(([k, v]) => { + parsedUrl = parsedUrl.replace(`{${k}}`, encodeURIComponent(v)); + }); + return parsedUrl; +} + +export function mwFetch(url: string, ops: P[1]): R { + return baseFetch(url, ops); +} + +export function proxiedFetch(url: string, ops: P[1]): R { + const parsedUrl = new URL(url); + Object.entries(ops?.params ?? {}).forEach(([k, v]) => { + parsedUrl.searchParams.set(k, v); + }); + return baseFetch(conf().BASE_PROXY_URL, { + ...ops, + baseURL: undefined, + params: { + destination: parsedUrl.toString(), + }, + }); +} diff --git a/src/backend/helpers/provider.ts b/src/backend/helpers/provider.ts index 545a39e6..348152b3 100644 --- a/src/backend/helpers/provider.ts +++ b/src/backend/helpers/provider.ts @@ -8,13 +8,26 @@ export type MWProviderScrapeResult = { embeds: MWEmbed[]; }; -export type MWProviderContext = { +type MWProviderBase = { progress(percentage: number): void; media: DetailedMeta; }; +type MWProviderTypeSpecific = + | { + type: MWMediaType.MOVIE | MWMediaType.ANIME; + episode?: undefined; + season?: undefined; + } + | { + type: MWMediaType.SERIES; + episode: number; + season: number; + }; +export type MWProviderContext = MWProviderTypeSpecific & MWProviderBase; export type MWProvider = { id: string; + displayName: string; rank: number; disabled?: boolean; type: MWMediaType[]; diff --git a/src/backend/helpers/scrape.ts b/src/backend/helpers/scrape.ts index 0683a2e6..3ad57843 100644 --- a/src/backend/helpers/scrape.ts +++ b/src/backend/helpers/scrape.ts @@ -1,38 +1,60 @@ -import { MWProviderScrapeResult } from "./provider"; +import { MWProviderContext, MWProviderScrapeResult } from "./provider"; import { getEmbedScraperByType, getProviders } from "./register"; import { runEmbedScraper, runProvider } from "./run"; import { MWStream } from "./streams"; import { DetailedMeta } from "../metadata/getmeta"; +import { MWMediaType } from "../metadata/types"; interface MWProgressData { type: "embed" | "provider"; id: string; + eventId: string; percentage: number; errored: boolean; } interface MWNextData { id: string; + eventId: string; type: "embed" | "provider"; } -export interface MWProviderRunContext { +type MWProviderRunContextBase = { media: DetailedMeta; onProgress?: (data: MWProgressData) => void; onNext?: (data: MWNextData) => void; -} +}; +type MWProviderRunContextTypeSpecific = + | { + type: MWMediaType.MOVIE | MWMediaType.ANIME; + episode: undefined; + season: undefined; + } + | { + type: MWMediaType.SERIES; + episode: number; + season: number; + }; + +export type MWProviderRunContext = MWProviderRunContextBase & + MWProviderRunContextTypeSpecific; async function findBestEmbedStream( result: MWProviderScrapeResult, + providerId: string, ctx: MWProviderRunContext ): Promise { if (result.stream) return result.stream; + let embedNum = 0; for (const embed of result.embeds) { + embedNum += 1; if (!embed.type) continue; const scraper = getEmbedScraperByType(embed.type); if (!scraper) throw new Error("Type for embed not found"); - ctx.onNext?.({ id: scraper.id, type: "embed" }); + const eventId = [providerId, scraper.id, embedNum].join("|"); + + ctx.onNext?.({ id: scraper.id, type: "embed", eventId }); let stream: MWStream; try { @@ -41,6 +63,7 @@ async function findBestEmbedStream( progress(num) { ctx.onProgress?.({ errored: false, + eventId, id: scraper.id, percentage: num, type: "embed", @@ -50,6 +73,7 @@ async function findBestEmbedStream( } catch { ctx.onProgress?.({ errored: true, + eventId, id: scraper.id, percentage: 100, type: "embed", @@ -59,6 +83,7 @@ async function findBestEmbedStream( ctx.onProgress?.({ errored: false, + eventId, id: scraper.id, percentage: 100, type: "embed", @@ -76,24 +101,48 @@ export async function findBestStream( const providers = getProviders(); for (const provider of providers) { - ctx.onNext?.({ id: provider.id, type: "provider" }); + const eventId = provider.id; + ctx.onNext?.({ id: provider.id, type: "provider", eventId }); let result: MWProviderScrapeResult; try { - result = await runProvider(provider, { - media: ctx.media, - progress(num) { - ctx.onProgress?.({ - percentage: num, - errored: false, - id: provider.id, - type: "provider", - }); - }, - }); + let context: MWProviderContext; + if (ctx.type === MWMediaType.SERIES) { + context = { + media: ctx.media, + type: ctx.type, + episode: ctx.episode, + season: ctx.season, + progress(num) { + ctx.onProgress?.({ + percentage: num, + eventId, + errored: false, + id: provider.id, + type: "provider", + }); + }, + }; + } else { + context = { + media: ctx.media, + type: ctx.type, + progress(num) { + ctx.onProgress?.({ + percentage: num, + eventId, + errored: false, + id: provider.id, + type: "provider", + }); + }, + }; + } + result = await runProvider(provider, context); } catch (err) { ctx.onProgress?.({ percentage: 100, errored: true, + eventId, id: provider.id, type: "provider", }); @@ -103,11 +152,12 @@ export async function findBestStream( ctx.onProgress?.({ errored: false, id: provider.id, + eventId, percentage: 100, type: "provider", }); - const stream = await findBestEmbedStream(result, ctx); + const stream = await findBestEmbedStream(result, provider.id, ctx); if (!stream) continue; return stream; } diff --git a/src/backend/index.ts b/src/backend/index.ts index ee267e67..6924b860 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -2,13 +2,10 @@ import { initializeScraperStore } from "./helpers/register"; // TODO backend system: // - caption support -// - hooks to run all providers one by one // - move over old providers to new system // - implement jons providers/embedscrapers -// - show/episode support // providers -// -- nothing here yet import "./providers/gdriveplayer"; // embeds diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index e8306664..fc25fddf 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -1,10 +1,13 @@ -import { formatJWMeta, JWMediaResult } from "./justwatch"; +import { FetchError } from "ofetch"; +import { makeUrl, mwFetch } from "../helpers/fetch"; +import { + formatJWMeta, + JWMediaResult, + JW_API_BASE, + mediaTypeToJW, +} from "./justwatch"; import { MWMediaMeta, MWMediaType } from "./types"; -const JW_API_BASE = "https://apis.justwatch.com"; - -// http://localhost:5173/#/media/movie-439596/ - type JWExternalIdType = | "eidr" | "imdb_latest" @@ -31,18 +34,23 @@ export interface DetailedMeta { export async function getMetaFromId( type: MWMediaType, id: string -): Promise { - let queryType = ""; - if (type === MWMediaType.MOVIE) queryType = "movie"; - else if (type === MWMediaType.SERIES) queryType = "show"; - else if (type === MWMediaType.ANIME) - throw new Error("Anime search type is not supported"); - - const data = await fetch( - `${JW_API_BASE}/content/titles/${queryType}/${encodeURIComponent( - id - )}/locale/en_US` - ).then((res) => res.json() as Promise); +): Promise { + const queryType = mediaTypeToJW(type); + + let data: JWDetailedMeta; + try { + const url = makeUrl("/content/titles/{type}/{id}/locale/en_US", { + type: queryType, + id, + }); + data = await mwFetch(url, { baseURL: JW_API_BASE }); + } catch (err) { + if (err instanceof FetchError) { + // 400 and 404 are treated as not found + if (err.statusCode === 400 || err.statusCode === 404) return null; + } + throw err; + } const imdbId = data.external_ids.find( (v) => v.provider === "imdb_latest" diff --git a/src/backend/metadata/justwatch.ts b/src/backend/metadata/justwatch.ts index 50712bac..31aa78ed 100644 --- a/src/backend/metadata/justwatch.ts +++ b/src/backend/metadata/justwatch.ts @@ -1,6 +1,7 @@ import { MWMediaType } from "./types"; export const JW_API_BASE = "https://apis.justwatch.com"; +export const JW_IMAGE_BASE = "https://images.justwatch.com"; export type JWContentTypes = "movie" | "show"; @@ -32,10 +33,7 @@ export function formatJWMeta(media: JWMediaResult) { id: media.id.toString(), year: media.original_release_year.toString(), poster: media.poster - ? `https://images.justwatch.com${media.poster.replace( - "{profile}", - "s166" - )}` + ? `${JW_IMAGE_BASE}${media.poster.replace("{profile}", "s166")}` : undefined, type, }; diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 7b3c486e..4ad0434b 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -1,10 +1,19 @@ +import { SimpleCache } from "@/utils/cache"; +import { mwFetch } from "../helpers/fetch"; import { formatJWMeta, JWContentTypes, JWMediaResult, JW_API_BASE, + mediaTypeToJW, } from "./justwatch"; -import { MWMediaMeta, MWMediaType, MWQuery } from "./types"; +import { MWMediaMeta, MWQuery } from "./types"; + +const cache = new SimpleCache(); +cache.setCompare((a, b) => { + return a.type === b.type && a.searchQuery.trim() === b.searchQuery.trim(); +}); +cache.initialize(); type JWSearchQuery = { content_types: JWContentTypes[]; @@ -21,26 +30,29 @@ type JWPage = { total_results: number; }; -export async function searchForMedia({ - searchQuery, - type, -}: MWQuery): Promise { +export async function searchForMedia(query: MWQuery): Promise { + if (cache.has(query)) return cache.get(query) as MWMediaMeta[]; + const { searchQuery, type } = query; + + const contentType = mediaTypeToJW(type); const body: JWSearchQuery = { - content_types: [], + content_types: [contentType], page: 1, query: searchQuery, page_size: 40, }; - if (type === MWMediaType.MOVIE) body.content_types.push("movie"); - else if (type === MWMediaType.SERIES) body.content_types.push("show"); - else if (type === MWMediaType.ANIME) - throw new Error("Anime search type is not supported"); - const data = await fetch( - `${JW_API_BASE}/content/titles/en_US/popular?body=${encodeURIComponent( - JSON.stringify(body) - )}` - ).then((res) => res.json() as Promise>); + const data = await mwFetch>( + "/content/titles/en_US/popular", + { + baseURL: JW_API_BASE, + params: { + body: JSON.stringify(body), + }, + } + ); - return data.items.map((v) => formatJWMeta(v)); + const returnData = data.items.map((v) => formatJWMeta(v)); + cache.set(query, returnData, 3600); // cache for an hour + return returnData; } diff --git a/src/backend/providers/gdriveplayer.ts b/src/backend/providers/gdriveplayer.ts index 60c7ac4f..4adcb144 100644 --- a/src/backend/providers/gdriveplayer.ts +++ b/src/backend/providers/gdriveplayer.ts @@ -1,10 +1,10 @@ -import { conf } from "@/setup/config"; +import { unpack } from "unpacker"; +import CryptoJS from "crypto-js"; + import { registerProvider } from "@/backend/helpers/register"; import { MWMediaType } from "@/backend/metadata/types"; import { MWStreamQuality } from "@/backend/helpers/streams"; - -import { unpack } from "unpacker"; -import CryptoJS from "crypto-js"; +import { proxiedFetch } from "../helpers/fetch"; const format = { stringify: (cipher: any) => { @@ -34,16 +34,20 @@ const format = { registerProvider({ id: "gdriveplayer", + displayName: "gdriveplayer", rank: 69, type: [MWMediaType.MOVIE], async scrape({ progress, media: { imdbId } }) { progress(10); - const streamRes = await fetch( - `${ - conf().CORS_PROXY_URL - }https://database.gdriveplayer.us/player.php?imdb=${imdbId}` - ).then((d) => d.text()); + const streamRes = await proxiedFetch( + "https://database.gdriveplayer.us/player.php", + { + params: { + imdb: imdbId, + }, + } + ); progress(90); const page = new DOMParser().parseFromString(streamRes, "text/html"); @@ -67,6 +71,7 @@ registerProvider({ { format } ).toString(CryptoJS.enc.Utf8) ); + // eslint-disable-next-line const sources = JSON.parse( JSON.stringify( diff --git a/src/components/layout/ErrorBoundary.tsx b/src/components/layout/ErrorBoundary.tsx index 061ff5df..125a352b 100644 --- a/src/components/layout/ErrorBoundary.tsx +++ b/src/components/layout/ErrorBoundary.tsx @@ -5,6 +5,62 @@ import { Link } from "@/components/text/Link"; import { Title } from "@/components/text/Title"; import { conf } from "@/setup/config"; +interface ErrorShowcaseProps { + error: { + name: string; + description: string; + path: string; + }; +} + +export function ErrorShowcase(props: ErrorShowcaseProps) { + return ( +
+

+ {props.error.name} - {props.error.description} +

+

{props.error.path}

+
+ ); +} + +interface ErrorMessageProps { + error?: { + name: string; + description: string; + path: string; + }; + children?: React.ReactNode; +} + +export function ErrorMessage(props: ErrorMessageProps) { + return ( +
+
+ + Whoops, it broke + {props.children ? ( + props.children + ) : ( +

+ The app encountered an error and wasn't able to recover, please + report it to the{" "} + + Discord server + {" "} + or on{" "} + + GitHub + + . +

+ )} +
+ {props.error ? : null} +
+ ); +} + interface ErrorBoundaryState { hasError: boolean; error?: { @@ -50,33 +106,6 @@ export class ErrorBoundary extends Component< render() { if (!this.state.hasError) return this.props.children as any; - return ( -
-
- - Whoops, it broke -

- The app encountered an error and wasn't able to recover, please - report it to the{" "} - - Discord server - {" "} - or on{" "} - - GitHub - - . -

-
- {this.state.error ? ( -
-

- {this.state.error.name} - {this.state.error.description} -

-

{this.state.error.path}

-
- ) : null} -
- ); + return ; } } diff --git a/src/components/video/parts/VideoPlayerHeader.tsx b/src/components/video/parts/VideoPlayerHeader.tsx index 83138b19..2296caf7 100644 --- a/src/components/video/parts/VideoPlayerHeader.tsx +++ b/src/components/video/parts/VideoPlayerHeader.tsx @@ -7,7 +7,7 @@ interface VideoPlayerHeaderProps { } export function VideoPlayerHeader(props: VideoPlayerHeaderProps) { - const showDivider = props.title || props.onClick; + const showDivider = props.title && props.onClick; return (
diff --git a/src/hooks/useGoBack.ts b/src/hooks/useGoBack.ts new file mode 100644 index 00000000..3ecc29a6 --- /dev/null +++ b/src/hooks/useGoBack.ts @@ -0,0 +1,12 @@ +import { useCallback } from "react"; +import { useHistory } from "react-router-dom"; + +export function useGoBack() { + const reactHistory = useHistory(); + + const goBack = useCallback(() => { + if (reactHistory.action !== "POP") reactHistory.goBack(); + else reactHistory.push("/"); + }, [reactHistory]); + return goBack; +} diff --git a/src/hooks/useScrape.ts b/src/hooks/useScrape.ts index ca0004ed..413b638e 100644 --- a/src/hooks/useScrape.ts +++ b/src/hooks/useScrape.ts @@ -1,16 +1,30 @@ import { findBestStream } from "@/backend/helpers/scrape"; import { MWStream } from "@/backend/helpers/streams"; import { DetailedMeta } from "@/backend/metadata/getmeta"; +import { MWMediaType } from "@/backend/metadata/types"; import { useEffect, useState } from "react"; export interface ScrapeEventLog { type: "provider" | "embed"; errored: boolean; percentage: number; + eventId: string; id: string; } -export function useScrape(meta: DetailedMeta) { +export type SelectedMediaData = + | { + type: MWMediaType.SERIES; + episode: number; + season: number; + } + | { + type: MWMediaType.MOVIE | MWMediaType.ANIME; + episode: undefined; + season: undefined; + }; + +export function useScrape(meta: DetailedMeta, selected: SelectedMediaData) { const [eventLog, setEventLog] = useState([]); const [stream, setStream] = useState(null); const [pending, setPending] = useState(true); @@ -22,12 +36,14 @@ export function useScrape(meta: DetailedMeta) { (async () => { const scrapedStream = await findBestStream({ media: meta, + ...selected, onNext(ctx) { setEventLog((arr) => [ ...arr, { errored: false, id: ctx.id, + eventId: ctx.eventId, type: ctx.type, percentage: 0, }, @@ -48,7 +64,7 @@ export function useScrape(meta: DetailedMeta) { setPending(false); setStream(scrapedStream); })(); - }, [meta]); + }, [meta, selected]); return { stream, diff --git a/src/index.tsx b/src/index.tsx index 1b20371c..fce8fd42 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -33,6 +33,9 @@ if (key) { // - devices: ipadOS // - features: HLS, error handling, preload interactions +// TODO general todos: +// - localize everything + ReactDOM.render( diff --git a/src/views/media/MediaErrorView.tsx b/src/views/media/MediaErrorView.tsx new file mode 100644 index 00000000..586f123a --- /dev/null +++ b/src/views/media/MediaErrorView.tsx @@ -0,0 +1,49 @@ +import { ErrorMessage } from "@/components/layout/ErrorBoundary"; +import { Link } from "@/components/text/Link"; +import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader"; +import { useGoBack } from "@/hooks/useGoBack"; +import { conf } from "@/setup/config"; + +export function MediaFetchErrorView() { + const goBack = useGoBack(); + + return ( +
+
+ +
+ +

+ We failed to request the media you asked for, check your internet + connection and try again. +

+
+
+ ); +} + +export function MediaPlaybackErrorView(props: { title?: string }) { + const goBack = useGoBack(); + + return ( +
+
+ +
+ +

+ We encountered an error while playing the video you requested. If this + keeps happening please report the issue to the + + Discord server + {" "} + or on{" "} + + GitHub + + . +

+
+
+ ); +} diff --git a/src/views/media/MediaScrapeLog.tsx b/src/views/media/MediaScrapeLog.tsx index 5d7ed3b1..9f0b0a97 100644 --- a/src/views/media/MediaScrapeLog.tsx +++ b/src/views/media/MediaScrapeLog.tsx @@ -71,7 +71,7 @@ export function MediaScrapeLog(props: MediaScrapeLogProps) { > {props.events.map((v) => ( - + ))}
diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index f9484c1a..f0224953 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -1,14 +1,21 @@ -import { useHistory, useParams } from "react-router-dom"; -import { useCallback, useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import { useEffect, useState } from "react"; import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer"; import { MWStream } from "@/backend/helpers/streams"; -import { useScrape } from "@/hooks/useScrape"; +import { SelectedMediaData, useScrape } from "@/hooks/useScrape"; import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader"; import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta"; import { JWMediaToMediaType } from "@/backend/metadata/justwatch"; import { SourceControl } from "@/components/video/controls/SourceControl"; import { Loading } from "@/components/layout/Loading"; +import { useLoading } from "@/hooks/useLoading"; +import { MWMediaType } from "@/backend/metadata/types"; +import { useGoBack } from "@/hooks/useGoBack"; +import { IconPatch } from "@/components/buttons/IconPatch"; +import { Icons } from "@/components/Icon"; +import { MediaFetchErrorView } from "./MediaErrorView"; import { MediaScrapeLog } from "./MediaScrapeLog"; +import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView"; function MediaViewLoading(props: { onGoBack(): void }) { return ( @@ -28,9 +35,10 @@ interface MediaViewScrapingProps { onStream(stream: MWStream): void; onGoBack(): void; meta: DetailedMeta; + selected: SelectedMediaData; } function MediaViewScraping(props: MediaViewScrapingProps) { - const { eventLog, stream } = useScrape(props.meta); + const { eventLog, stream, pending } = useScrape(props.meta, props.selected); useEffect(() => { if (stream) { @@ -38,8 +46,6 @@ function MediaViewScraping(props: MediaViewScrapingProps) { } }, [stream, props]); - // TODO error screen if no streams found - return (
@@ -48,44 +54,91 @@ function MediaViewScraping(props: MediaViewScrapingProps) { title={props.meta.meta.title} />
-
- -

Finding the best video for you

- +
+ {pending ? ( + <> + +

+ Finding the best video for you +

+ + ) : ( + <> + +

+ Whoops, could't find any videos for you +

+ + )} +
+ +
); } export function MediaView() { - const reactHistory = useHistory(); const params = useParams<{ media: string }>(); - const goBack = useCallback(() => { - if (reactHistory.action !== "POP") reactHistory.goBack(); - else reactHistory.push("/"); - }, [reactHistory]); + const goBack = useGoBack(); const [meta, setMeta] = useState(null); + const [selected, setSelected] = useState(null); + const [exec, loading, error] = useLoading(async (mediaParams: string) => { + let type: MWMediaType; + let id = ""; + try { + const [t, i] = mediaParams.split("-", 2); + type = JWMediaToMediaType(t); + id = i; + } catch (err) { + return null; + } + return getMetaFromId(type, id); + }); const [stream, setStream] = useState(null); useEffect(() => { - // TODO handle errors - (async () => { - const [t, id] = params.media.split("-", 2); - const type = JWMediaToMediaType(t); - const fetchedMeta = await getMetaFromId(type, id); - setMeta(fetchedMeta); - })(); - }, [setMeta, params]); + exec(params.media).then((v) => { + setMeta(v ?? null); + if (v) + setSelected({ + type: v.meta.type, + episode: 0 as any, + season: 0 as any, + }); + else setSelected(null); + }); + }, [exec, params.media]); // TODO watched store // TODO error page with video header - if (!meta) return ; + if (loading) return ; + if (error) return ; + if (!meta || !selected) + return ( + + + + ); + + // scraping view will start scraping and return with onStream if (!stream) return ( - + ); + + // show stream once we have a stream return (
diff --git a/src/views/notfound/NotFoundChecks.tsx b/src/views/notfound/NotFoundChecks.tsx deleted file mode 100644 index cffd9b85..00000000 --- a/src/views/notfound/NotFoundChecks.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { ReactElement } from "react"; - -export interface NotFoundChecksProps { - id: string; - children?: ReactElement; -} - -/* - ** Component that only renders children if the passed in data is fully correct - */ -export function NotFoundChecks( - props: NotFoundChecksProps -): ReactElement | null { - // TODO do notfound check - - return props.children || null; -} diff --git a/src/views/notfound/NotFoundView.tsx b/src/views/notfound/NotFoundView.tsx index 14cb7829..49584bb3 100644 --- a/src/views/notfound/NotFoundView.tsx +++ b/src/views/notfound/NotFoundView.tsx @@ -5,11 +5,24 @@ import { Icons } from "@/components/Icon"; import { Navigation } from "@/components/layout/Navigation"; import { ArrowLink } from "@/components/text/ArrowLink"; import { Title } from "@/components/text/Title"; +import { useGoBack } from "@/hooks/useGoBack"; +import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader"; + +export function NotFoundWrapper(props: { + children?: ReactNode; + video?: boolean; +}) { + const goBack = useGoBack(); -function NotFoundWrapper(props: { children?: ReactNode }) { return (
- + {props.video ? ( +
+ +
+ ) : ( + + )}
{props.children}
diff --git a/src/views/search/SearchResultsView.tsx b/src/views/search/SearchResultsView.tsx index 8638ec71..60a61115 100644 --- a/src/views/search/SearchResultsView.tsx +++ b/src/views/search/SearchResultsView.tsx @@ -52,7 +52,6 @@ export function SearchResultsView({ searchQuery }: { searchQuery: MWQuery }) { ); useEffect(() => { - // TODO use cache async function runSearch(query: MWQuery) { const searchResults = await runSearchQuery(query); if (!searchResults) return; diff --git a/yarn.lock b/yarn.lock index e3e9dda4..d3d5529c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -974,6 +974,11 @@ "depd@^1.1.2": "version" "1.1.2" +"destr@^1.2.1": + "integrity" "sha512-lrbCJwD9saUQrqUfXvl6qoM+QN3W7tLV5pAOs+OqOmopCCz/JkE05MHedJR1xfk4IAnZuJXPVuN5+7jNA2ZCiA==" + "resolved" "https://registry.npmjs.org/destr/-/destr-1.2.2.tgz" + "version" "1.2.2" + "detective@^5.2.1": "integrity" "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==" "resolved" "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz" @@ -2363,6 +2368,11 @@ "negotiator@^0.6.3": "version" "0.6.3" +"node-fetch-native@^1.0.1": + "integrity" "sha512-VzW+TAk2wE4X9maiKMlT+GsPU4OMmR1U9CrHSmd3DFLn2IcZ9VJ6M6BBugGfYUnPCLSYxXdZy17M0BEJyhUTwg==" + "resolved" "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.0.1.tgz" + "version" "1.0.1" + "node-fetch@2.6.7": "integrity" "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==" "resolved" "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz" @@ -2631,6 +2641,15 @@ "define-properties" "^1.1.4" "es-abstract" "^1.20.4" +"ofetch@^1.0.0": + "integrity" "sha512-d40aof8czZFSQKJa4+F7Ch3UC5D631cK1TTUoK+iNEut9NoiCL+u0vykl/puYVUS2df4tIQl5upQcolIcEzQjQ==" + "resolved" "https://registry.npmjs.org/ofetch/-/ofetch-1.0.0.tgz" + "version" "1.0.0" + dependencies: + "destr" "^1.2.1" + "node-fetch-native" "^1.0.1" + "ufo" "^1.0.0" + "once@^1.3.0": "integrity" "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==" "resolved" "https://registry.npmjs.org/once/-/once-1.4.0.tgz" @@ -3430,6 +3449,11 @@ "resolved" "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz" "version" "4.9.4" +"ufo@^1.0.0": + "integrity" "sha512-boAm74ubXHY7KJQZLlXrtMz52qFvpsbOxDcZOnw/Wf+LS4Mmyu7JxmzD4tDLtUQtmZECypJ0FrCz4QIe6dvKRA==" + "resolved" "https://registry.npmjs.org/ufo/-/ufo-1.0.1.tgz" + "version" "1.0.1" + "unbox-primitive@^1.0.2": "integrity" "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==" "resolved" "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz" From ca169769bbe06639b1d3b95672ee6c7c475b8185 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sun, 15 Jan 2023 16:51:55 +0100 Subject: [PATCH 037/135] error handling video player --- src/components/layout/ErrorBoundary.tsx | 9 +- src/components/video/DecoratedVideoPlayer.tsx | 105 +++++++++--------- src/components/video/VideoPlayer.tsx | 17 ++- src/components/video/hooks/controlVideo.ts | 25 ++++- src/components/video/hooks/useVideoPlayer.ts | 21 ++++ .../video/parts/VideoErrorBoundary.tsx | 82 ++++++++++++++ .../video/parts/VideoPlayerError.tsx | 35 ++++++ src/index.tsx | 2 +- src/views/media/MediaView.tsx | 1 - 9 files changed, 233 insertions(+), 64 deletions(-) create mode 100644 src/components/video/parts/VideoErrorBoundary.tsx create mode 100644 src/components/video/parts/VideoPlayerError.tsx diff --git a/src/components/layout/ErrorBoundary.tsx b/src/components/layout/ErrorBoundary.tsx index 125a352b..5f6adacb 100644 --- a/src/components/layout/ErrorBoundary.tsx +++ b/src/components/layout/ErrorBoundary.tsx @@ -30,17 +30,22 @@ interface ErrorMessageProps { description: string; path: string; }; + localSize?: boolean; children?: React.ReactNode; } export function ErrorMessage(props: ErrorMessageProps) { return ( -
+
Whoops, it broke {props.children ? ( - props.children +

{props.children}

) : (

The app encountered an error and wasn't able to recover, please diff --git a/src/components/video/DecoratedVideoPlayer.tsx b/src/components/video/DecoratedVideoPlayer.tsx index e1582d5c..f5a471ee 100644 --- a/src/components/video/DecoratedVideoPlayer.tsx +++ b/src/components/video/DecoratedVideoPlayer.tsx @@ -8,6 +8,7 @@ import { PauseControl } from "./controls/PauseControl"; import { ProgressControl } from "./controls/ProgressControl"; import { TimeControl } from "./controls/TimeControl"; import { VolumeControl } from "./controls/VolumeControl"; +import { VideoPlayerError } from "./parts/VideoPlayerError"; import { VideoPlayerHeader } from "./parts/VideoPlayerHeader"; import { useVideoPlayerState } from "./VideoContext"; import { VideoPlayer, VideoPlayerProps } from "./VideoPlayer"; @@ -56,60 +57,62 @@ export function DecoratedVideoPlayer( return ( - -

- -
-
- -
- -
+ +
+ +
+
+ +
+ - -
- -
- +
+ +
+ +
+ +
-
- - -
+ - -
-
- - {props.children} +
+ +
+ + + {props.children} + ); } diff --git a/src/components/video/VideoPlayer.tsx b/src/components/video/VideoPlayer.tsx index 02d71440..d00b917a 100644 --- a/src/components/video/VideoPlayer.tsx +++ b/src/components/video/VideoPlayer.tsx @@ -1,4 +1,6 @@ +import { useGoBack } from "@/hooks/useGoBack"; import { forwardRef, useContext, useEffect, useRef } from "react"; +import { VideoErrorBoundary } from "./parts/VideoErrorBoundary"; import { VideoPlayerContext, VideoPlayerContextProvider } from "./VideoContext"; export interface VideoPlayerProps { @@ -35,6 +37,9 @@ const VideoPlayerInternals = forwardRef< export function VideoPlayer(props: VideoPlayerProps) { const playerRef = useRef(null); const playerWrapperRef = useRef(null); + const goBack = useGoBack(); + + // TODO move error boundary to only decorated, shouldn't have styling return ( @@ -42,11 +47,13 @@ export function VideoPlayer(props: VideoPlayerProps) { className="relative h-full w-full select-none overflow-hidden bg-black" ref={playerWrapperRef} > - -
{props.children}
+ + +
{props.children}
+
); diff --git a/src/components/video/hooks/controlVideo.ts b/src/components/video/hooks/controlVideo.ts index 50e85fde..d9e8f87b 100644 --- a/src/components/video/hooks/controlVideo.ts +++ b/src/components/video/hooks/controlVideo.ts @@ -108,19 +108,36 @@ export function populateControls( initPlayer(sourceUrl: string, sourceType: MWStreamType) { this.setVolume(getStoredVolume()); + // TODO test HLS errors if (sourceType === MWStreamType.HLS) { if (player.canPlayType("application/vnd.apple.mpegurl")) { player.src = sourceUrl; } else { // HLS support - if (!Hls.isSupported()) throw new Error("HLS not supported"); // TODO handle errors + if (!Hls.isSupported()) { + update((s) => ({ + ...s, + error: { + name: `Not supported`, + description: "Your browser does not support HLS video", + }, + })); + return; + } const hls = new Hls(); hls.on(Hls.Events.ERROR, (event, data) => { - // eslint-disable-next-line no-alert - if (data.fatal) alert("HLS fatal error"); - console.error("HLS error", data); // TODO handle errors + if (data.fatal) { + update((s) => ({ + ...s, + error: { + name: `error ${data.details}`, + description: data.error?.message ?? "Something went wrong", + }, + })); + } + console.error("HLS error", data); }); hls.attachMedia(player); diff --git a/src/components/video/hooks/useVideoPlayer.ts b/src/components/video/hooks/useVideoPlayer.ts index 4589c07e..937af32b 100644 --- a/src/components/video/hooks/useVideoPlayer.ts +++ b/src/components/video/hooks/useVideoPlayer.ts @@ -23,6 +23,10 @@ export type PlayerState = { hasInitialized: boolean; leftControlHovering: boolean; hasPlayedOnce: boolean; + error: null | { + name: string; + description: string; + }; }; export type PlayerContext = PlayerState & PlayerControls; @@ -42,6 +46,7 @@ export const initialPlayerState: PlayerContext = { hasInitialized: false, leftControlHovering: false, hasPlayedOnce: false, + error: null, ...initialControls, }; @@ -61,6 +66,7 @@ function readState(player: HTMLVideoElement, update: SetPlayer) { state.buffered = handleBuffered(player.currentTime, player.buffered); state.isLoading = false; state.hasInitialized = true; + state.error = null; update((s) => ({ ...state, @@ -131,6 +137,19 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { isFirstLoading: false, })); }; + const error = () => { + console.error("Native video player threw error", player.error); + // TODO check if these errors are actually fatal + update((s) => ({ + ...s, + error: player.error + ? { + description: player.error.message, + name: `Error ${player.error.code}`, + } + : null, + })); + }; player.addEventListener("pause", pause); player.addEventListener("playing", playing); @@ -143,6 +162,7 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { player.addEventListener("progress", progress); player.addEventListener("waiting", waiting); player.addEventListener("canplay", canplay); + player.addEventListener("error", error); return () => { player.removeEventListener("pause", pause); @@ -156,6 +176,7 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { player.removeEventListener("progress", progress); player.removeEventListener("waiting", waiting); player.removeEventListener("canplay", canplay); + player.removeEventListener("error", error); }; } diff --git a/src/components/video/parts/VideoErrorBoundary.tsx b/src/components/video/parts/VideoErrorBoundary.tsx new file mode 100644 index 00000000..c05bbd5a --- /dev/null +++ b/src/components/video/parts/VideoErrorBoundary.tsx @@ -0,0 +1,82 @@ +import { ErrorMessage } from "@/components/layout/ErrorBoundary"; +import { Link } from "@/components/text/Link"; +import { conf } from "@/setup/config"; +import { Component, ReactNode } from "react"; +import { VideoPlayerHeader } from "./VideoPlayerHeader"; + +interface ErrorBoundaryState { + hasError: boolean; + error?: { + name: string; + description: string; + path: string; + }; +} + +interface VideoErrorBoundaryProps { + children?: ReactNode; + title?: string; + onGoBack?: () => void; +} + +export class VideoErrorBoundary extends Component< + VideoErrorBoundaryProps, + ErrorBoundaryState +> { + constructor(props: VideoErrorBoundaryProps) { + super(props); + this.state = { + hasError: false, + }; + } + + static getDerivedStateFromError() { + return { + hasError: true, + }; + } + + componentDidCatch(error: any, errorInfo: any) { + console.error("Render error caught", error, errorInfo); + if (error instanceof Error) { + const realError: Error = error as Error; + this.setState((s) => ({ + ...s, + hasError: true, + error: { + name: realError.name, + description: realError.message, + path: errorInfo.componentStack.split("\n")[1], + }, + })); + } + } + + render() { + if (!this.state.hasError) return this.props.children; + + // TODO make responsive, needs to work in tiny player + + return ( +
+
+ +
+ + The video player encounted a fatal error, please report it to the{" "} + + Discord server + {" "} + or on{" "} + + GitHub + + . + +
+ ); + } +} diff --git a/src/components/video/parts/VideoPlayerError.tsx b/src/components/video/parts/VideoPlayerError.tsx new file mode 100644 index 00000000..26ae3dee --- /dev/null +++ b/src/components/video/parts/VideoPlayerError.tsx @@ -0,0 +1,35 @@ +import { IconPatch } from "@/components/buttons/IconPatch"; +import { Icons } from "@/components/Icon"; +import { Title } from "@/components/text/Title"; +import { ReactNode } from "react"; +import { useVideoPlayerState } from "../VideoContext"; +import { VideoPlayerHeader } from "./VideoPlayerHeader"; + +interface VideoPlayerErrorProps { + title?: string; + onGoBack?: () => void; + children?: ReactNode; +} + +export function VideoPlayerError(props: VideoPlayerErrorProps) { + const { videoState } = useVideoPlayerState(); + + const err = videoState.error; + + if (!err) return props.children as any; + + return ( +
+
+ + Failed to load media +

+ {err.name}: {err.description} +

+
+
+ +
+
+ ); +} diff --git a/src/index.tsx b/src/index.tsx index fce8fd42..008b958c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -17,7 +17,6 @@ if (key) { } // TODO video todos: -// - error handling // - captions // - mobile UI // - safari fullscreen will make video overlap player controls @@ -35,6 +34,7 @@ if (key) { // TODO general todos: // - localize everything +// - add titles to pages ReactDOM.render( diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index f0224953..b019e688 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -116,7 +116,6 @@ export function MediaView() { }, [exec, params.media]); // TODO watched store - // TODO error page with video header if (loading) return ; if (error) return ; From a369682a26dc3a51f55ac4cddf9a9d4d010e91e3 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Mon, 16 Jan 2023 21:19:49 +0100 Subject: [PATCH 038/135] add continue watching and bookmarks back --- src/components/media/MediaCard.tsx | 2 - src/index.tsx | 2 + src/state/bookmark/context.tsx | 10 +-- src/state/bookmark/store.ts | 1 - src/state/watched/context.tsx | 107 ++++++++++++++++------------- src/state/watched/store.ts | 2 - src/views/media/MediaView.tsx | 10 ++- src/views/search/HomeView.tsx | 62 ++++++++--------- 8 files changed, 99 insertions(+), 97 deletions(-) diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 7b63bf7e..e51db836 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -8,8 +8,6 @@ export interface MediaCardProps { linkable?: boolean; } -// TODO add progress back - function MediaCardContent({ media, linkable }: MediaCardProps) { return (
diff --git a/src/state/bookmark/context.tsx b/src/state/bookmark/context.tsx index 977b4596..b4593be2 100644 --- a/src/state/bookmark/context.tsx +++ b/src/state/bookmark/context.tsx @@ -63,15 +63,7 @@ export function BookmarkContextProvider(props: { children: ReactNode }) { if (bookmarked) { const itemIndex = getBookmarkIndexFromMedia(data.bookmarks, media); if (itemIndex === -1) { - const item = { - id: media.id, - type: media.type, - // providerId: media.providerId, - title: media.title, - year: media.year, - // episodeId: media.episodeId, - // seasonId: media.seasonId, - }; + const item: MWMediaMeta = { ...media }; data.bookmarks.push(item); } } else { diff --git a/src/state/bookmark/store.ts b/src/state/bookmark/store.ts index 06456b78..089a6693 100644 --- a/src/state/bookmark/store.ts +++ b/src/state/bookmark/store.ts @@ -9,7 +9,6 @@ export const BookmarkStore = versionedStoreBuilder() version: 1, migrate() { return { - // TODO actually migrate bookmarks: [], }; }, diff --git a/src/state/watched/context.tsx b/src/state/watched/context.tsx index c434801e..cefec243 100644 --- a/src/state/watched/context.tsx +++ b/src/state/watched/context.tsx @@ -1,5 +1,6 @@ -import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types"; -import React, { +import { DetailedMeta } from "@/backend/metadata/getmeta"; +import { MWMediaMeta } from "@/backend/metadata/types"; +import { createContext, ReactNode, useCallback, @@ -9,7 +10,16 @@ import React, { } from "react"; import { VideoProgressStore } from "./store"; -interface WatchedStoreItem extends MWMediaMeta { +interface MediaItem { + meta: MWMediaMeta; + series?: { + episode: number; + season: number; + }; +} + +interface WatchedStoreItem { + item: MediaItem; progress: number; percentage: number; } @@ -19,18 +29,11 @@ export interface WatchedStoreData { } interface WatchedStoreDataWrapper { - updateProgress(media: MWMediaMeta, progress: number, total: number): void; + updateProgress(media: MediaItem, progress: number, total: number): void; getFilteredWatched(): WatchedStoreItem[]; watched: WatchedStoreData; } -export function getWatchedFromPortable( - items: WatchedStoreItem[], - media: MWMediaMeta -): WatchedStoreItem | undefined { - return undefined; -} - const WatchedContext = createContext({ updateProgress: () => {}, getFilteredWatched: () => [], @@ -62,49 +65,39 @@ export function WatchedContextProvider(props: { children: ReactNode }) { const contextValue = useMemo( () => ({ - updateProgress( - media: MWMediaMeta, - progress: number, - total: number - ): void { - // setWatched((data: WatchedStoreData) => { - // let item = getWatchedFromPortable(data.items, media); - // if (!item) { - // item = { - // mediaId: media.mediaId, - // mediaType: media.mediaType, - // providerId: media.providerId, - // title: media.title, - // year: media.year, - // percentage: 0, - // progress: 0, - // episodeId: media.episodeId, - // seasonId: media.seasonId, - // }; - // data.items.push(item); - // } - // // update actual item - // item.progress = progress; - // item.percentage = Math.round((progress / total) * 100); - // return data; - // }); + updateProgress(media: MediaItem, progress: number, total: number): void { + setWatched((data: WatchedStoreData) => { + let item = data.items.find((v) => v.item.meta.id === media.meta.id); + if (!item) { + item = { + item: { + ...media, + meta: { ...media.meta }, + series: media.series ? { ...media.series } : undefined, + }, + progress: 0, + percentage: 0, + }; + data.items.push(item); + } + // update actual item + item.progress = progress; + item.percentage = Math.round((progress / total) * 100); + return data; + }); }, getFilteredWatched() { - // remove disabled providers - // let filtered = watched.items.filter( - // (item) => getProviderMetadata(item.providerId)?.enabled - // ); let filtered = watched.items; - // // get highest episode number for every anime/season + // get highest episode number for every anime/season const highestEpisode: Record = {}; const highestWatchedItem: Record = {}; filtered = filtered.filter((item) => { - if ([MWMediaType.ANIME, MWMediaType.SERIES].includes(item.type)) { - const key = `${item.type}-${item.id}`; + if (item.item.series) { + const key = item.item.meta.id; const current: [number, number] = [ - item.episodeId ? parseInt(item.episodeId, 10) : -1, - item.seasonId ? parseInt(item.seasonId, 10) : -1, + item.item.series.episode, + item.item.series.season, ]; let existing = highestEpisode[key]; if (!existing) { @@ -127,7 +120,7 @@ export function WatchedContextProvider(props: { children: ReactNode }) { }, watched, }), - [watched] + [watched, setWatched] ); return ( @@ -140,3 +133,23 @@ export function WatchedContextProvider(props: { children: ReactNode }) { export function useWatchedContext() { return useContext(WatchedContext); } + +export function useWatchedItem(meta: DetailedMeta | null) { + const { watched, updateProgress } = useContext(WatchedContext); + const item = useMemo( + () => watched.items.find((v) => meta && v.item.meta.id === meta?.meta.id), + [watched, meta] + ); + + const callback = useCallback( + (progress: number, total: number) => { + if (meta) { + // TODO add series support + updateProgress({ meta: meta.meta }, progress, total); + } + }, + [updateProgress, meta] + ); + + return { updateProgress: callback, watchedItem: item }; +} diff --git a/src/state/watched/store.ts b/src/state/watched/store.ts index 4ea10100..aada4131 100644 --- a/src/state/watched/store.ts +++ b/src/state/watched/store.ts @@ -8,7 +8,6 @@ export const VideoProgressStore = versionedStoreBuilder() .addVersion({ version: 1, migrate() { - // TODO add migration back return { items: [], }; @@ -17,7 +16,6 @@ export const VideoProgressStore = versionedStoreBuilder() .addVersion({ version: 2, migrate() { - // TODO actually migrate return { items: [], }; diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index b019e688..4c92fd4d 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -13,6 +13,8 @@ import { MWMediaType } from "@/backend/metadata/types"; import { useGoBack } from "@/hooks/useGoBack"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icons } from "@/components/Icon"; +import { useWatchedItem } from "@/state/watched"; +import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl"; import { MediaFetchErrorView } from "./MediaErrorView"; import { MediaScrapeLog } from "./MediaScrapeLog"; import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView"; @@ -102,6 +104,8 @@ export function MediaView() { }); const [stream, setStream] = useState(null); + const { updateProgress, watchedItem } = useWatchedItem(meta); + useEffect(() => { exec(params.media).then((v) => { setMeta(v ?? null); @@ -115,8 +119,6 @@ export function MediaView() { }); }, [exec, params.media]); - // TODO watched store - if (loading) return ; if (error) return ; if (!meta || !selected) @@ -142,6 +144,10 @@ export function MediaView() {
+
); diff --git a/src/views/search/HomeView.tsx b/src/views/search/HomeView.tsx index ac745ede..ffd9dd99 100644 --- a/src/views/search/HomeView.tsx +++ b/src/views/search/HomeView.tsx @@ -7,6 +7,7 @@ import { useBookmarkContext, } from "@/state/bookmark"; import { useWatchedContext } from "@/state/watched"; +import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; function Bookmarks() { const { t } = useTranslation(); @@ -21,52 +22,45 @@ function Bookmarks() { icon={Icons.BOOKMARK} > - {/* {bookmarks.map((v) => ( - - ))} */} + {bookmarks.map((v) => ( + + ))} ); } -// function Watched() { -// const { t } = useTranslation(); -// const { getFilteredBookmarks } = useBookmarkContext(); -// const { getFilteredWatched } = useWatchedContext(); +function Watched() { + const { t } = useTranslation(); + const { getFilteredBookmarks } = useBookmarkContext(); + const { getFilteredWatched } = useWatchedContext(); -// const bookmarks = getFilteredBookmarks(); -// const watchedItems = getFilteredWatched().filter( -// (v) => !getIfBookmarkedFromPortable(bookmarks, v) -// ); + const bookmarks = getFilteredBookmarks(); + const watchedItems = getFilteredWatched().filter( + (v) => !getIfBookmarkedFromPortable(bookmarks, v.item.meta) + ); -// if (watchedItems.length === 0) return null; + if (watchedItems.length === 0) return null; -// return ( -// -// -// {/* {watchedItems.map((v) => ( -// -// ))} */} -// -// -// ); -// } + return ( + + + {watchedItems.map((v) => ( + + ))} + + + ); +} export function HomeView() { return (
- {/* */} - {/* */} + +
); } From 714b378f68277331bde79e1094e2345692fb279b Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Mon, 16 Jan 2023 21:25:16 +0100 Subject: [PATCH 039/135] move around some todos --- src/backend/index.ts | 5 ----- src/components/video/hooks/controlVideo.ts | 1 - src/components/video/hooks/useVideoPlayer.ts | 1 - src/index.tsx | 12 ++++++++++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/backend/index.ts b/src/backend/index.ts index 6924b860..7140fe13 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -1,10 +1,5 @@ import { initializeScraperStore } from "./helpers/register"; -// TODO backend system: -// - caption support -// - move over old providers to new system -// - implement jons providers/embedscrapers - // providers import "./providers/gdriveplayer"; diff --git a/src/components/video/hooks/controlVideo.ts b/src/components/video/hooks/controlVideo.ts index d9e8f87b..dc2d1aa1 100644 --- a/src/components/video/hooks/controlVideo.ts +++ b/src/components/video/hooks/controlVideo.ts @@ -108,7 +108,6 @@ export function populateControls( initPlayer(sourceUrl: string, sourceType: MWStreamType) { this.setVolume(getStoredVolume()); - // TODO test HLS errors if (sourceType === MWStreamType.HLS) { if (player.canPlayType("application/vnd.apple.mpegurl")) { player.src = sourceUrl; diff --git a/src/components/video/hooks/useVideoPlayer.ts b/src/components/video/hooks/useVideoPlayer.ts index 937af32b..4c82de6d 100644 --- a/src/components/video/hooks/useVideoPlayer.ts +++ b/src/components/video/hooks/useVideoPlayer.ts @@ -139,7 +139,6 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { }; const error = () => { console.error("Native video player threw error", player.error); - // TODO check if these errors are actually fatal update((s) => ({ ...s, error: player.error diff --git a/src/index.tsx b/src/index.tsx index 8535ca1d..8b1b64bd 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -30,12 +30,20 @@ if (key) { // - browser: firefox, chrome, edge, safari desktop // - phones: android firefox, android chrome, iphone safari // - devices: ipadOS -// - features: HLS, error handling, preload interactions +// - HLS +// - HLS error handling +// - video player error handling + +// TODO backend system: +// - caption support +// - move over old providers to new system +// - implement jons providers/embedscrapers +// - AFTER all that: rank providers/embedscrapers // TODO general todos: // - localize everything // - add titles to pages -// - find place for bookmarks +// - find place for bookmark button // - find place for progress bar for "continue watching" section ReactDOM.render( From f656f8099608f529cdd16021de5f39e59993d1c0 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Mon, 16 Jan 2023 21:26:01 +0100 Subject: [PATCH 040/135] more todos --- src/index.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/index.tsx b/src/index.tsx index 8b1b64bd..f2f33418 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -19,6 +19,9 @@ if (key) { // TODO video todos: // - captions // - mobile UI +// - season/episode select +// - chrome cast support +// - source selection // - safari fullscreen will make video overlap player controls // - safari progress bar is fucked (video doesnt change time but video.currentTime does change) From f37bec7a7a6a6c567ebe9166bb057141899ea316 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Mon, 16 Jan 2023 21:53:38 +0100 Subject: [PATCH 041/135] progress restoring logic --- .../controls/ProgressListenerControl.tsx | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/components/video/controls/ProgressListenerControl.tsx b/src/components/video/controls/ProgressListenerControl.tsx index bdcc8f07..a8f9a80d 100644 --- a/src/components/video/controls/ProgressListenerControl.tsx +++ b/src/components/video/controls/ProgressListenerControl.tsx @@ -7,6 +7,25 @@ interface Props { onProgress?: (time: number, duration: number) => void; } +const FIVETEEN_MINUTES = 15 * 60; +const FIVE_MINUTES = 5 * 60; + +function shouldRestoreTime(time: number, duration: number): boolean { + const timeFromEnd = Math.max(0, duration - time); + + // short movie + if (duration < FIVETEEN_MINUTES) { + if (time < 5) return false; + if (timeFromEnd < 60) return false; + return true; + } + + // long movie + if (time < 30) return false; + if (timeFromEnd < FIVE_MINUTES) return false; + return true; +} + export function ProgressListenerControl(props: Props) { const { videoState } = useVideoPlayerState(); const didInitialize = useRef(null); @@ -31,7 +50,12 @@ export function ProgressListenerControl(props: Props) { useEffect(() => { if (didInitialize.current) return; if (!videoState.hasInitialized || Number.isNaN(videoState.duration)) return; - if (props.startAt !== undefined) videoState.setTime(props.startAt); + if ( + props.startAt !== undefined && + shouldRestoreTime(props.startAt, videoState.duration) + ) { + videoState.setTime(props.startAt); + } didInitialize.current = true; }, [didInitialize, videoState, props]); From 4d2fc166bcd825d9bb378fa0170b93b7728e6e94 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Tue, 17 Jan 2023 01:02:29 +0100 Subject: [PATCH 042/135] fix time render issue --- src/components/video/controls/TimeControl.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/video/controls/TimeControl.tsx b/src/components/video/controls/TimeControl.tsx index 42e78329..5af152e3 100644 --- a/src/components/video/controls/TimeControl.tsx +++ b/src/components/video/controls/TimeControl.tsx @@ -17,15 +17,15 @@ function formatSeconds(secs: number, showHours = false): string { const minutes = time % 60; time /= 60; - const hours = time % 60; + const hours = time; if (!showHours) - return `${Math.round(minutes).toString()}:${Math.round(seconds) + return `${Math.floor(minutes).toString()}:${Math.floor(seconds) .toString() .padStart(2, "0")}`; - return `${Math.round(hours).toString()}:${Math.round(minutes) + return `${Math.floor(hours).toString()}:${Math.floor(minutes) .toString() - .padStart(2, "0")}:${Math.round(seconds).toString().padStart(2, "0")}`; + .padStart(2, "0")}:${Math.floor(seconds).toString().padStart(2, "0")}`; } interface Props { From 40cca106601db71d1c6921ace8ec84238bf74985 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Tue, 17 Jan 2023 13:42:15 +0100 Subject: [PATCH 043/135] update time control display --- src/components/video/controls/TimeControl.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/components/video/controls/TimeControl.tsx b/src/components/video/controls/TimeControl.tsx index 5af152e3..49e933cd 100644 --- a/src/components/video/controls/TimeControl.tsx +++ b/src/components/video/controls/TimeControl.tsx @@ -13,19 +13,17 @@ function formatSeconds(secs: number, showHours = false): string { let time = secs; const seconds = time % 60; - time /= 60; + time = Math.floor(time / 60); const minutes = time % 60; - time /= 60; + time = Math.floor(time / 60); const hours = time; - if (!showHours) - return `${Math.floor(minutes).toString()}:${Math.floor(seconds) - .toString() - .padStart(2, "0")}`; - return `${Math.floor(hours).toString()}:${Math.floor(minutes) - .toString() - .padStart(2, "0")}:${Math.floor(seconds).toString().padStart(2, "0")}`; + const paddedSecs = seconds.toString().padStart(2, "0"); + const paddedMins = minutes.toString().padStart(2, "0"); + + if (!showHours) return [minutes, paddedSecs].join(":"); + return [hours, paddedMins, paddedSecs].join(":"); } interface Props { From 6353bf37997692a268c8c02cb45f4abf46fdfd4a Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Tue, 17 Jan 2023 19:11:10 +0100 Subject: [PATCH 044/135] fix time control once again --- src/components/video/controls/TimeControl.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/video/controls/TimeControl.tsx b/src/components/video/controls/TimeControl.tsx index 49e933cd..1adea300 100644 --- a/src/components/video/controls/TimeControl.tsx +++ b/src/components/video/controls/TimeControl.tsx @@ -11,13 +11,13 @@ function formatSeconds(secs: number, showHours = false): string { } let time = secs; - const seconds = time % 60; + const seconds = Math.floor(time % 60); - time = Math.floor(time / 60); - const minutes = time % 60; + time /= 60; + const minutes = Math.floor(time % 60); - time = Math.floor(time / 60); - const hours = time; + time /= 60; + const hours = Math.floor(time); const paddedSecs = seconds.toString().padStart(2, "0"); const paddedMins = minutes.toString().padStart(2, "0"); From fb960261953ccd2a093916ea452348e6b881ca1d Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Tue, 17 Jan 2023 21:12:39 +0100 Subject: [PATCH 045/135] continue watching and progress bars --- src/components/media/MediaCard.tsx | 43 +++++++++++++++++-- src/components/media/WatchedMediaCard.tsx | 16 ++++++- .../video/controls/BackdropControl.tsx | 1 + .../video/controls/ProgressControl.tsx | 2 + .../controls/ProgressListenerControl.tsx | 27 ++---------- .../video/controls/SourceControl.tsx | 5 ++- src/index.tsx | 2 +- src/state/watched/context.tsx | 37 ++++++++++++++-- 8 files changed, 100 insertions(+), 33 deletions(-) diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index e51db836..1b23b974 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -6,9 +6,21 @@ import { mediaTypeToJW } from "@/backend/metadata/justwatch"; export interface MediaCardProps { media: MWMediaMeta; linkable?: boolean; + series?: { + episode: number; + season: number; + }; + percentage?: number; } -function MediaCardContent({ media, linkable }: MediaCardProps) { +function MediaCardContent({ + media, + linkable, + series, + percentage, +}: MediaCardProps) { + const percentageString = `${Math.round(percentage ?? 0).toFixed(0)}%`; + return (
+ > + {series ? ( +
+

+ S{series.season} E{series.episode} +

+
+ ) : null} + + {percentage !== undefined ? ( + <> +
+
+
+
+
+
+
+ + ) : null} +

{media.title}

diff --git a/src/components/media/WatchedMediaCard.tsx b/src/components/media/WatchedMediaCard.tsx index 6dcf7064..1dd11d5e 100644 --- a/src/components/media/WatchedMediaCard.tsx +++ b/src/components/media/WatchedMediaCard.tsx @@ -1,4 +1,6 @@ import { MWMediaMeta } from "@/backend/metadata/types"; +import { useWatchedContext } from "@/state/watched"; +import { useMemo } from "react"; import { MediaCard } from "./MediaCard"; export interface WatchedMediaCardProps { @@ -6,5 +8,17 @@ export interface WatchedMediaCardProps { } export function WatchedMediaCard(props: WatchedMediaCardProps) { - return ; + const { watched } = useWatchedContext(); + const watchedMedia = useMemo(() => { + return watched.items.find((v) => v.item.meta.id === props.media.id); + }, [watched, props.media]); + + return ( + + ); } diff --git a/src/components/video/controls/BackdropControl.tsx b/src/components/video/controls/BackdropControl.tsx index 4c081ba7..2d099627 100644 --- a/src/components/video/controls/BackdropControl.tsx +++ b/src/components/video/controls/BackdropControl.tsx @@ -12,6 +12,7 @@ export function BackdropControl(props: BackdropControlProps) { const timeout = useRef | null>(null); const clickareaRef = useRef(null); + // TODO fix infinite loop const handleMouseMove = useCallback(() => { setMoved(true); if (timeout.current) clearTimeout(timeout.current); diff --git a/src/components/video/controls/ProgressControl.tsx b/src/components/video/controls/ProgressControl.tsx index b8e277a0..eaeed9ee 100644 --- a/src/components/video/controls/ProgressControl.tsx +++ b/src/components/video/controls/ProgressControl.tsx @@ -21,6 +21,8 @@ export function ProgressControl() { ref, commitTime ); + + // TODO make dragging update timer useEffect(() => { if (dragRef.current === dragging) return; dragRef.current = dragging; diff --git a/src/components/video/controls/ProgressListenerControl.tsx b/src/components/video/controls/ProgressListenerControl.tsx index a8f9a80d..b20fc4c8 100644 --- a/src/components/video/controls/ProgressListenerControl.tsx +++ b/src/components/video/controls/ProgressListenerControl.tsx @@ -7,25 +7,7 @@ interface Props { onProgress?: (time: number, duration: number) => void; } -const FIVETEEN_MINUTES = 15 * 60; -const FIVE_MINUTES = 5 * 60; - -function shouldRestoreTime(time: number, duration: number): boolean { - const timeFromEnd = Math.max(0, duration - time); - - // short movie - if (duration < FIVETEEN_MINUTES) { - if (time < 5) return false; - if (timeFromEnd < 60) return false; - return true; - } - - // long movie - if (time < 30) return false; - if (timeFromEnd < FIVE_MINUTES) return false; - return true; -} - +// TODO fix infinite loops export function ProgressListenerControl(props: Props) { const { videoState } = useVideoPlayerState(); const didInitialize = useRef(null); @@ -50,14 +32,11 @@ export function ProgressListenerControl(props: Props) { useEffect(() => { if (didInitialize.current) return; if (!videoState.hasInitialized || Number.isNaN(videoState.duration)) return; - if ( - props.startAt !== undefined && - shouldRestoreTime(props.startAt, videoState.duration) - ) { + if (props.startAt !== undefined) { videoState.setTime(props.startAt); } didInitialize.current = true; - }, [didInitialize, videoState, props]); + }, [didInitialize, props, videoState]); return null; } diff --git a/src/components/video/controls/SourceControl.tsx b/src/components/video/controls/SourceControl.tsx index 0e612a50..9025c404 100644 --- a/src/components/video/controls/SourceControl.tsx +++ b/src/components/video/controls/SourceControl.tsx @@ -1,5 +1,5 @@ import { MWStreamType } from "@/backend/helpers/streams"; -import { useContext, useEffect } from "react"; +import { useContext, useEffect, useRef } from "react"; import { VideoPlayerDispatchContext } from "../VideoContext"; interface SourceControlProps { @@ -9,13 +9,16 @@ interface SourceControlProps { export function SourceControl(props: SourceControlProps) { const dispatch = useContext(VideoPlayerDispatchContext); + const didInitialize = useRef(false); useEffect(() => { + if (didInitialize.current) return; dispatch({ type: "SET_SOURCE", url: props.source, sourceType: props.type, }); + didInitialize.current = true; }, [props, dispatch]); return null; diff --git a/src/index.tsx b/src/index.tsx index f2f33418..5d345107 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -21,6 +21,7 @@ if (key) { // - mobile UI // - season/episode select // - chrome cast support +// - airplay support // - source selection // - safari fullscreen will make video overlap player controls // - safari progress bar is fucked (video doesnt change time but video.currentTime does change) @@ -47,7 +48,6 @@ if (key) { // - localize everything // - add titles to pages // - find place for bookmark button -// - find place for progress bar for "continue watching" section ReactDOM.render( diff --git a/src/state/watched/context.tsx b/src/state/watched/context.tsx index cefec243..dd44ba58 100644 --- a/src/state/watched/context.tsx +++ b/src/state/watched/context.tsx @@ -10,6 +10,25 @@ import { } from "react"; import { VideoProgressStore } from "./store"; +const FIVETEEN_MINUTES = 15 * 60; +const FIVE_MINUTES = 5 * 60; + +function shouldSave(time: number, duration: number): boolean { + const timeFromEnd = Math.max(0, duration - time); + + // short movie + if (duration < FIVETEEN_MINUTES) { + if (time < 5) return false; + if (timeFromEnd < 60) return false; + return true; + } + + // long movie + if (time < 30) return false; + if (timeFromEnd < FIVE_MINUTES) return false; + return true; +} + interface MediaItem { meta: MWMediaMeta; series?: { @@ -66,8 +85,12 @@ export function WatchedContextProvider(props: { children: ReactNode }) { const contextValue = useMemo( () => ({ updateProgress(media: MediaItem, progress: number, total: number): void { + // TODO series support setWatched((data: WatchedStoreData) => { - let item = data.items.find((v) => v.item.meta.id === media.meta.id); + const newData = { ...data }; + let item = newData.items.find( + (v) => v.item.meta.id === media.meta.id + ); if (!item) { item = { item: { @@ -78,12 +101,20 @@ export function WatchedContextProvider(props: { children: ReactNode }) { progress: 0, percentage: 0, }; - data.items.push(item); + newData.items.push(item); } // update actual item item.progress = progress; item.percentage = Math.round((progress / total) * 100); - return data; + + // remove item if shouldnt save + if (!shouldSave(progress, total)) { + newData.items = data.items.filter( + (v) => v.item.meta.id !== media.meta.id + ); + } + + return newData; }); }, getFilteredWatched() { From 02cc4b7f1dd6bed00ecbea0774164244899e8f1a Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Thu, 19 Jan 2023 22:29:56 +0100 Subject: [PATCH 046/135] bookmarks, progress and editing of those --- package.json | 1 + src/components/Icon.tsx | 4 ++ src/components/buttons/EditButton.tsx | 32 +++++++++++ src/components/buttons/IconPatch.tsx | 17 ++++-- src/components/layout/SectionHeading.tsx | 2 +- src/components/media/MediaCard.tsx | 57 ++++++++++++++----- src/components/media/MediaGrid.tsx | 18 +++--- src/components/media/WatchedEpisodeButton.tsx | 24 -------- src/components/media/WatchedMediaCard.tsx | 4 ++ src/components/text/Tagline.tsx | 7 --- src/components/video/DecoratedVideoPlayer.tsx | 7 ++- .../video/parts/VideoErrorBoundary.tsx | 5 +- .../video/parts/VideoPlayerError.tsx | 5 +- .../video/parts/VideoPlayerHeader.tsx | 28 +++++++-- src/state/bookmark/context.tsx | 23 +++----- src/state/watched/context.tsx | 9 +++ src/views/media/MediaErrorView.tsx | 5 +- src/views/media/MediaView.tsx | 7 +-- src/views/search/HomeView.tsx | 55 ++++++++++++------ src/views/search/SearchResultsView.tsx | 11 ++-- yarn.lock | 5 ++ 21 files changed, 214 insertions(+), 112 deletions(-) create mode 100644 src/components/buttons/EditButton.tsx delete mode 100644 src/components/media/WatchedEpisodeButton.tsx delete mode 100644 src/components/text/Tagline.tsx diff --git a/package.json b/package.json index e7947361..ee11ba00 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "homepage": "https://movie.squeezebox.dev", "dependencies": { + "@formkit/auto-animate": "^1.0.0-beta.5", "@headlessui/react": "^1.5.0", "crypto-js": "^4.1.1", "fscreen": "^1.2.0", diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 7866066f..2b8fa81e 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -3,6 +3,7 @@ import { memo } from "react"; export enum Icons { SEARCH = "search", BOOKMARK = "bookmark", + BOOKMARK_OUTLINE = "bookmark_outline", CLOCK = "clock", EYE_SLASH = "eyeSlash", ARROW_LEFT = "arrowLeft", @@ -23,6 +24,7 @@ export enum Icons { VOLUME = "volume", VOLUME_X = "volume_x", X = "x", + EDIT = "edit", } export interface IconProps { @@ -53,6 +55,8 @@ const iconList: Record = { volume: ``, volume_x: ``, x: ``, + edit: ``, + bookmark_outline: ``, }; export const Icon = memo((props: IconProps) => { diff --git a/src/components/buttons/EditButton.tsx b/src/components/buttons/EditButton.tsx new file mode 100644 index 00000000..01988b51 --- /dev/null +++ b/src/components/buttons/EditButton.tsx @@ -0,0 +1,32 @@ +import { Icon, Icons } from "@/components/Icon"; +import { useAutoAnimate } from "@formkit/auto-animate/react"; +import { useCallback } from "react"; +import { ButtonControl } from "./ButtonControl"; + +export interface EditButtonProps { + editing: boolean; + onEdit?: (editing: boolean) => void; +} + +export function EditButton(props: EditButtonProps) { + const [parent] = useAutoAnimate(); + + const onClick = useCallback(() => { + props.onEdit?.(!props.editing); + }, [props]); + + return ( + + + {props.editing ? ( + Stop editing + ) : ( + + )} + + + ); +} diff --git a/src/components/buttons/IconPatch.tsx b/src/components/buttons/IconPatch.tsx index 53980322..d51f20b1 100644 --- a/src/components/buttons/IconPatch.tsx +++ b/src/components/buttons/IconPatch.tsx @@ -6,17 +6,24 @@ export interface IconPatchProps { clickable?: boolean; className?: string; icon: Icons; + transparent?: boolean; } export function IconPatch(props: IconPatchProps) { + const clickableClasses = props.clickable + ? "cursor-pointer hover:scale-110 hover:bg-denim-600 hover:text-white active:scale-125" + : ""; + const transparentClasses = props.transparent + ? "bg-opacity-0 hover:bg-opacity-50" + : ""; + const activeClasses = props.active + ? "border-bink-600 bg-bink-100 text-bink-600" + : ""; + return (
diff --git a/src/components/layout/SectionHeading.tsx b/src/components/layout/SectionHeading.tsx index eb245725..a9d01cb7 100644 --- a/src/components/layout/SectionHeading.tsx +++ b/src/components/layout/SectionHeading.tsx @@ -20,8 +20,8 @@ export function SectionHeading(props: SectionHeadingProps) { ) : null} {props.title}

+ {props.children}
- {props.children}
); } diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 1b23b974..0a72d463 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -2,6 +2,8 @@ import { Link } from "react-router-dom"; import { DotList } from "@/components/text/DotList"; import { MWMediaMeta } from "@/backend/metadata/types"; import { mediaTypeToJW } from "@/backend/metadata/justwatch"; +import { Icons } from "../Icon"; +import { IconPatch } from "../buttons/IconPatch"; export interface MediaCardProps { media: MWMediaMeta; @@ -11,6 +13,8 @@ export interface MediaCardProps { season: number; }; percentage?: number; + closable?: boolean; + onClose?: () => void; } function MediaCardContent({ @@ -18,18 +22,22 @@ function MediaCardContent({ linkable, series, percentage, + closable, + onClose, }: MediaCardProps) { const percentageString = `${Math.round(percentage ?? 0).toFixed(0)}%`; + const canLink = linkable && !closable; + return (
-
-
+
+
) : null} + +
+ closable && onClose?.()} + icon={Icons.X} + /> +

{media.title} @@ -75,14 +104,14 @@ function MediaCardContent({ export function MediaCard(props: MediaCardProps) { const content = ; - if (!props.linkable) return {content}; - return ( - - {content} - - ); + )}-${encodeURIComponent(props.media.id)}` + : "#"; + + if (!props.linkable) return {content}; + return {content}; } diff --git a/src/components/media/MediaGrid.tsx b/src/components/media/MediaGrid.tsx index 59a3e39e..a9f75b22 100644 --- a/src/components/media/MediaGrid.tsx +++ b/src/components/media/MediaGrid.tsx @@ -1,11 +1,15 @@ +import { forwardRef } from "react"; + interface MediaGridProps { children?: React.ReactNode; } -export function MediaGrid(props: MediaGridProps) { - return ( -
- {props.children} -
- ); -} +export const MediaGrid = forwardRef( + (props, ref) => { + return ( +
+ {props.children} +
+ ); + } +); diff --git a/src/components/media/WatchedEpisodeButton.tsx b/src/components/media/WatchedEpisodeButton.tsx deleted file mode 100644 index 779f2aef..00000000 --- a/src/components/media/WatchedEpisodeButton.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { MWMediaMeta } from "@/backend/metadata/types"; -import { useWatchedContext, getWatchedFromPortable } from "@/state/watched"; -import { Episode } from "./EpisodeButton"; - -export interface WatchedEpisodeProps { - media: MWMediaMeta; - onClick?: () => void; - active?: boolean; -} - -export function WatchedEpisode(props: WatchedEpisodeProps) { - // const { watched } = useWatchedContext(); - // const foundWatched = getWatchedFromPortable(watched.items, props.media); - // // const episode = getEpisodeFromMedia(props.media); - // const watchedPercentage = (foundWatched && foundWatched.percentage) || 0; - // return ( - // - // ); -} diff --git a/src/components/media/WatchedMediaCard.tsx b/src/components/media/WatchedMediaCard.tsx index 1dd11d5e..62ffcc73 100644 --- a/src/components/media/WatchedMediaCard.tsx +++ b/src/components/media/WatchedMediaCard.tsx @@ -5,6 +5,8 @@ import { MediaCard } from "./MediaCard"; export interface WatchedMediaCardProps { media: MWMediaMeta; + closable?: boolean; + onClose?: () => void; } export function WatchedMediaCard(props: WatchedMediaCardProps) { @@ -19,6 +21,8 @@ export function WatchedMediaCard(props: WatchedMediaCardProps) { series={watchedMedia?.item?.series} linkable percentage={watchedMedia?.percentage} + onClose={props.onClose} + closable={props.closable} /> ); } diff --git a/src/components/text/Tagline.tsx b/src/components/text/Tagline.tsx deleted file mode 100644 index 88633f5e..00000000 --- a/src/components/text/Tagline.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export interface TaglineProps { - children?: React.ReactNode; -} - -export function Tagline(props: TaglineProps) { - return

{props.children}

; -} diff --git a/src/components/video/DecoratedVideoPlayer.tsx b/src/components/video/DecoratedVideoPlayer.tsx index f5a471ee..7c68bf6f 100644 --- a/src/components/video/DecoratedVideoPlayer.tsx +++ b/src/components/video/DecoratedVideoPlayer.tsx @@ -1,3 +1,4 @@ +import { MWMediaMeta } from "@/backend/metadata/types"; import { useCallback, useRef, useState } from "react"; import { CSSTransition } from "react-transition-group"; import { BackdropControl } from "./controls/BackdropControl"; @@ -14,7 +15,7 @@ import { useVideoPlayerState } from "./VideoContext"; import { VideoPlayer, VideoPlayerProps } from "./VideoPlayer"; interface DecoratedVideoPlayerProps { - title?: string; + media?: MWMediaMeta; onGoBack?: () => void; } @@ -57,7 +58,7 @@ export function DecoratedVideoPlayer( return ( - +
@@ -107,7 +108,7 @@ export function DecoratedVideoPlayer( ref={top} className="pointer-events-auto absolute inset-x-0 top-0 flex flex-col py-6 px-8 pb-2" > - +
diff --git a/src/components/video/parts/VideoErrorBoundary.tsx b/src/components/video/parts/VideoErrorBoundary.tsx index c05bbd5a..205e27ae 100644 --- a/src/components/video/parts/VideoErrorBoundary.tsx +++ b/src/components/video/parts/VideoErrorBoundary.tsx @@ -1,3 +1,4 @@ +import { MWMediaMeta } from "@/backend/metadata/types"; import { ErrorMessage } from "@/components/layout/ErrorBoundary"; import { Link } from "@/components/text/Link"; import { conf } from "@/setup/config"; @@ -15,7 +16,7 @@ interface ErrorBoundaryState { interface VideoErrorBoundaryProps { children?: ReactNode; - title?: string; + media?: MWMediaMeta; onGoBack?: () => void; } @@ -61,7 +62,7 @@ export class VideoErrorBoundary extends Component<
diff --git a/src/components/video/parts/VideoPlayerError.tsx b/src/components/video/parts/VideoPlayerError.tsx index 26ae3dee..4b3e42da 100644 --- a/src/components/video/parts/VideoPlayerError.tsx +++ b/src/components/video/parts/VideoPlayerError.tsx @@ -1,3 +1,4 @@ +import { MWMediaMeta } from "@/backend/metadata/types"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icons } from "@/components/Icon"; import { Title } from "@/components/text/Title"; @@ -6,7 +7,7 @@ import { useVideoPlayerState } from "../VideoContext"; import { VideoPlayerHeader } from "./VideoPlayerHeader"; interface VideoPlayerErrorProps { - title?: string; + media?: MWMediaMeta; onGoBack?: () => void; children?: ReactNode; } @@ -28,7 +29,7 @@ export function VideoPlayerError(props: VideoPlayerErrorProps) {

- +

); diff --git a/src/components/video/parts/VideoPlayerHeader.tsx b/src/components/video/parts/VideoPlayerHeader.tsx index 2296caf7..0fbde691 100644 --- a/src/components/video/parts/VideoPlayerHeader.tsx +++ b/src/components/video/parts/VideoPlayerHeader.tsx @@ -1,13 +1,23 @@ +import { MWMediaMeta } from "@/backend/metadata/types"; +import { IconPatch } from "@/components/buttons/IconPatch"; import { Icon, Icons } from "@/components/Icon"; import { BrandPill } from "@/components/layout/BrandPill"; +import { + getIfBookmarkedFromPortable, + useBookmarkContext, +} from "@/state/bookmark"; interface VideoPlayerHeaderProps { - title?: string; + media?: MWMediaMeta; onClick?: () => void; } export function VideoPlayerHeader(props: VideoPlayerHeaderProps) { - const showDivider = props.title && props.onClick; + const { bookmarkStore, setItemBookmark } = useBookmarkContext(); + const isBookmarked = props.media + ? getIfBookmarkedFromPortable(bookmarkStore.bookmarks, props.media) + : false; + const showDivider = props.media && props.onClick; return (
@@ -24,8 +34,18 @@ export function VideoPlayerHeader(props: VideoPlayerHeaderProps) { {showDivider ? ( ) : null} - {props.title ? ( - {props.title} + {props.media ? ( + + {props.media.title} + + props.media && setItemBookmark(props.media, !isBookmarked) + } + /> + ) : null}

diff --git a/src/state/bookmark/context.tsx b/src/state/bookmark/context.tsx index b4593be2..65485a7b 100644 --- a/src/state/bookmark/context.tsx +++ b/src/state/bookmark/context.tsx @@ -59,24 +59,17 @@ export function BookmarkContextProvider(props: { children: ReactNode }) { const contextValue = useMemo( () => ({ setItemBookmark(media: MWMediaMeta, bookmarked: boolean) { - setBookmarked((data: BookmarkStoreData) => { - if (bookmarked) { - const itemIndex = getBookmarkIndexFromMedia(data.bookmarks, media); - if (itemIndex === -1) { - const item: MWMediaMeta = { ...media }; - data.bookmarks.push(item); - } - } else { - const itemIndex = getBookmarkIndexFromMedia(data.bookmarks, media); - if (itemIndex !== -1) { - data.bookmarks.splice(itemIndex); - } - } - return data; + setBookmarked((data: BookmarkStoreData): BookmarkStoreData => { + let bookmarks = [...data.bookmarks]; + bookmarks = bookmarks.filter((v) => v.id !== media.id); + if (bookmarked) bookmarks.push({ ...media }); + return { + bookmarks, + }; }); }, getFilteredBookmarks() { - return []; + return [...bookmarkStorage.bookmarks]; }, bookmarkStore: bookmarkStorage, }), diff --git a/src/state/watched/context.tsx b/src/state/watched/context.tsx index dd44ba58..00208956 100644 --- a/src/state/watched/context.tsx +++ b/src/state/watched/context.tsx @@ -50,12 +50,14 @@ export interface WatchedStoreData { interface WatchedStoreDataWrapper { updateProgress(media: MediaItem, progress: number, total: number): void; getFilteredWatched(): WatchedStoreItem[]; + removeProgress(id: string): void; watched: WatchedStoreData; } const WatchedContext = createContext({ updateProgress: () => {}, getFilteredWatched: () => [], + removeProgress: () => {}, watched: { items: [], }, @@ -84,6 +86,13 @@ export function WatchedContextProvider(props: { children: ReactNode }) { const contextValue = useMemo( () => ({ + removeProgress(id: string) { + setWatched((data: WatchedStoreData) => { + const newData = { ...data }; + newData.items = newData.items.filter((v) => v.item.meta.id !== id); + return newData; + }); + }, updateProgress(media: MediaItem, progress: number, total: number): void { // TODO series support setWatched((data: WatchedStoreData) => { diff --git a/src/views/media/MediaErrorView.tsx b/src/views/media/MediaErrorView.tsx index 586f123a..df5e9943 100644 --- a/src/views/media/MediaErrorView.tsx +++ b/src/views/media/MediaErrorView.tsx @@ -1,3 +1,4 @@ +import { MWMediaMeta } from "@/backend/metadata/types"; import { ErrorMessage } from "@/components/layout/ErrorBoundary"; import { Link } from "@/components/text/Link"; import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader"; @@ -22,13 +23,13 @@ export function MediaFetchErrorView() { ); } -export function MediaPlaybackErrorView(props: { title?: string }) { +export function MediaPlaybackErrorView(props: { media?: MWMediaMeta }) { const goBack = useGoBack(); return (
- +

diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index 4c92fd4d..f6bb4272 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -51,10 +51,7 @@ function MediaViewScraping(props: MediaViewScrapingProps) { return (

- +
{pending ? ( @@ -142,7 +139,7 @@ export function MediaView() { // show stream once we have a stream return (
- + (); if (bookmarks.length === 0) return null; return ( - - +
+ + + + {bookmarks.map((v) => ( - + setItemBookmark(v, false)} + /> ))} - +
); } function Watched() { const { t } = useTranslation(); const { getFilteredBookmarks } = useBookmarkContext(); - const { getFilteredWatched } = useWatchedContext(); + const { getFilteredWatched, removeProgress } = useWatchedContext(); + const [editing, setEditing] = useState(false); + const [gridRef] = useAutoAnimate(); const bookmarks = getFilteredBookmarks(); const watchedItems = getFilteredWatched().filter( @@ -43,16 +58,24 @@ function Watched() { if (watchedItems.length === 0) return null; return ( - - +
+ + + + {watchedItems.map((v) => ( - + removeProgress(v.item.meta.id)} + /> ))} - +
); } diff --git a/src/views/search/SearchResultsView.tsx b/src/views/search/SearchResultsView.tsx index 60a61115..0726ae5f 100644 --- a/src/views/search/SearchResultsView.tsx +++ b/src/views/search/SearchResultsView.tsx @@ -68,16 +68,17 @@ export function SearchResultsView({ searchQuery }: { searchQuery: MWQuery }) { return (
{results.length > 0 ? ( - +
+ {results.map((v) => ( ))} - +
) : null} diff --git a/yarn.lock b/yarn.lock index d3d5529c..50e52b88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -40,6 +40,11 @@ "minimatch" "^3.1.2" "strip-json-comments" "^3.1.1" +"@formkit/auto-animate@^1.0.0-beta.5": + "integrity" "sha512-WoSwyhAZPOe6RB/IgicOtCHtrWwEpfKIZ/H/nxpKfnZL9CB6hhhBGU5bCdMRw7YpAUF2CDlQa+WWh+gCqz5lDg==" + "resolved" "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-1.0.0-beta.5.tgz" + "version" "1.0.0-beta.5" + "@gar/promisify@^1.1.3": "version" "1.1.3" From b6a23aa0b7fa8c2c743d0c01e7f6adb7d3191e86 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Thu, 19 Jan 2023 22:37:16 +0100 Subject: [PATCH 047/135] update todos --- src/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/index.tsx b/src/index.tsx index 5d345107..630068b8 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -47,7 +47,6 @@ if (key) { // TODO general todos: // - localize everything // - add titles to pages -// - find place for bookmark button ReactDOM.render( From 5a01a68ce44b4836cc735f1f9e75ab31dd35227a Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sat, 21 Jan 2023 23:45:26 +0100 Subject: [PATCH 048/135] fix recursive rendering + show meta in player --- src/components/video/DecoratedVideoPlayer.tsx | 22 +++++---- .../video/controls/BackdropControl.tsx | 14 ++++-- .../controls/ProgressListenerControl.tsx | 1 - src/components/video/controls/ShowControl.tsx | 26 ++++++++++ .../video/controls/ShowTitleControl.tsx | 19 ++++++++ src/components/video/hooks/controlVideo.ts | 14 ++++++ src/components/video/hooks/useVideoPlayer.ts | 11 +++++ .../video/parts/VideoPlayerHeader.tsx | 21 ++++---- src/index.tsx | 4 -- src/state/watched/context.tsx | 12 +++-- src/views/media/MediaView.tsx | 48 +++++++++++++------ 11 files changed, 147 insertions(+), 45 deletions(-) create mode 100644 src/components/video/controls/ShowControl.tsx create mode 100644 src/components/video/controls/ShowTitleControl.tsx diff --git a/src/components/video/DecoratedVideoPlayer.tsx b/src/components/video/DecoratedVideoPlayer.tsx index 7c68bf6f..11b4fd68 100644 --- a/src/components/video/DecoratedVideoPlayer.tsx +++ b/src/components/video/DecoratedVideoPlayer.tsx @@ -7,6 +7,7 @@ import { LoadingControl } from "./controls/LoadingControl"; import { MiddlePauseControl } from "./controls/MiddlePauseControl"; import { PauseControl } from "./controls/PauseControl"; import { ProgressControl } from "./controls/ProgressControl"; +import { ShowTitleControl } from "./controls/ShowTitleControl"; import { TimeControl } from "./controls/TimeControl"; import { VolumeControl } from "./controls/VolumeControl"; import { VideoPlayerError } from "./parts/VideoPlayerError"; @@ -30,15 +31,18 @@ function LeftSideControls() { }, [videoState]); return ( -
- - - -
+ <> +
+ + + +
+ + ); } diff --git a/src/components/video/controls/BackdropControl.tsx b/src/components/video/controls/BackdropControl.tsx index 2d099627..2fba50f3 100644 --- a/src/components/video/controls/BackdropControl.tsx +++ b/src/components/video/controls/BackdropControl.tsx @@ -12,15 +12,14 @@ export function BackdropControl(props: BackdropControlProps) { const timeout = useRef | null>(null); const clickareaRef = useRef(null); - // TODO fix infinite loop const handleMouseMove = useCallback(() => { - setMoved(true); + if (!moved) setMoved(true); if (timeout.current) clearTimeout(timeout.current); timeout.current = setTimeout(() => { - setMoved(false); + if (moved) setMoved(false); timeout.current = null; }, 3000); - }, [timeout, setMoved]); + }, [setMoved, moved]); const handleMouseLeave = useCallback(() => { setMoved(false); @@ -45,8 +44,13 @@ export function BackdropControl(props: BackdropControlProps) { [videoState, clickareaRef] ); + const lastBackdropValue = useRef(null); useEffect(() => { - props.onBackdropChange?.(moved || videoState.isPaused); + const currentValue = moved || videoState.isPaused; + if (currentValue !== lastBackdropValue.current) { + lastBackdropValue.current = currentValue; + props.onBackdropChange?.(currentValue); + } }, [videoState, moved, props]); const showUI = moved || videoState.isPaused; diff --git a/src/components/video/controls/ProgressListenerControl.tsx b/src/components/video/controls/ProgressListenerControl.tsx index b20fc4c8..6c23bb18 100644 --- a/src/components/video/controls/ProgressListenerControl.tsx +++ b/src/components/video/controls/ProgressListenerControl.tsx @@ -7,7 +7,6 @@ interface Props { onProgress?: (time: number, duration: number) => void; } -// TODO fix infinite loops export function ProgressListenerControl(props: Props) { const { videoState } = useVideoPlayerState(); const didInitialize = useRef(null); diff --git a/src/components/video/controls/ShowControl.tsx b/src/components/video/controls/ShowControl.tsx new file mode 100644 index 00000000..5e2467e9 --- /dev/null +++ b/src/components/video/controls/ShowControl.tsx @@ -0,0 +1,26 @@ +import { useEffect } from "react"; +import { useVideoPlayerState } from "../VideoContext"; + +interface ShowControlProps { + series?: { + episode: number; + season: number; + }; + title?: string; +} + +export function ShowControl(props: ShowControlProps) { + const { videoState } = useVideoPlayerState(); + + useEffect(() => { + videoState.setShowData({ + current: props.series, + isSeries: !!props.series, + title: props.title, + }); + // we only want it to run when props change, not when videoState changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props]); + + return null; +} diff --git a/src/components/video/controls/ShowTitleControl.tsx b/src/components/video/controls/ShowTitleControl.tsx new file mode 100644 index 00000000..06cc7f7b --- /dev/null +++ b/src/components/video/controls/ShowTitleControl.tsx @@ -0,0 +1,19 @@ +import { useVideoPlayerState } from "../VideoContext"; + +export function ShowTitleControl() { + const { videoState } = useVideoPlayerState(); + + if (!videoState.seasonData.isSeries) return null; + if (!videoState.seasonData.title || !videoState.seasonData.current) + return null; + + const cur = videoState.seasonData.current; + const selectedText = `S${cur.season} E${cur.episode}`; + + return ( +

+ {selectedText} + {videoState.seasonData.title} +

+ ); +} diff --git a/src/components/video/hooks/controlVideo.ts b/src/components/video/hooks/controlVideo.ts index dc2d1aa1..6a30aa0e 100644 --- a/src/components/video/hooks/controlVideo.ts +++ b/src/components/video/hooks/controlVideo.ts @@ -11,6 +11,15 @@ import React, { RefObject } from "react"; import { PlayerState } from "./useVideoPlayer"; import { getStoredVolume, setStoredVolume } from "./volumeStore"; +interface ShowData { + current?: { + episode: number; + season: number; + }; + isSeries: boolean; + title?: string; +} + export interface PlayerControls { play(): void; pause(): void; @@ -21,6 +30,7 @@ export interface PlayerControls { setSeeking(active: boolean): void; setLeftControlsHover(hovering: boolean): void; initPlayer(sourceUrl: string, sourceType: MWStreamType): void; + setShowData(data: ShowData): void; } export const initialControls: PlayerControls = { @@ -33,6 +43,7 @@ export const initialControls: PlayerControls = { setSeeking: () => null, setLeftControlsHover: () => null, initPlayer: () => null, + setShowData: () => null, }; export function populateControls( @@ -105,6 +116,9 @@ export function populateControls( setLeftControlsHover(hovering) { update((s) => ({ ...s, leftControlHovering: hovering })); }, + setShowData(data) { + update((s) => ({ ...s, seasonData: data })); + }, initPlayer(sourceUrl: string, sourceType: MWStreamType) { this.setVolume(getStoredVolume()); diff --git a/src/components/video/hooks/useVideoPlayer.ts b/src/components/video/hooks/useVideoPlayer.ts index 4c82de6d..9c17a47e 100644 --- a/src/components/video/hooks/useVideoPlayer.ts +++ b/src/components/video/hooks/useVideoPlayer.ts @@ -23,6 +23,14 @@ export type PlayerState = { hasInitialized: boolean; leftControlHovering: boolean; hasPlayedOnce: boolean; + seasonData: { + isSeries: boolean; + current?: { + episode: number; + season: number; + }; + title?: string; + }; error: null | { name: string; description: string; @@ -47,6 +55,9 @@ export const initialPlayerState: PlayerContext = { leftControlHovering: false, hasPlayedOnce: false, error: null, + seasonData: { + isSeries: false, + }, ...initialControls, }; diff --git a/src/components/video/parts/VideoPlayerHeader.tsx b/src/components/video/parts/VideoPlayerHeader.tsx index 0fbde691..3f835645 100644 --- a/src/components/video/parts/VideoPlayerHeader.tsx +++ b/src/components/video/parts/VideoPlayerHeader.tsx @@ -35,19 +35,22 @@ export function VideoPlayerHeader(props: VideoPlayerHeaderProps) { ) : null} {props.media ? ( - + {props.media.title} - - props.media && setItemBookmark(props.media, !isBookmarked) - } - /> ) : null}

+ {props.media ? ( + + props.media && setItemBookmark(props.media, !isBookmarked) + } + /> + ) : null}
diff --git a/src/index.tsx b/src/index.tsx index 630068b8..53312ce8 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -26,10 +26,6 @@ if (key) { // - safari fullscreen will make video overlap player controls // - safari progress bar is fucked (video doesnt change time but video.currentTime does change) -// TODO optional todos: -// - shortcuts when player is active -// - improve seekables (if possible) - // TODO stuff to test: // - browser: firefox, chrome, edge, safari desktop // - phones: android firefox, android chrome, iphone safari diff --git a/src/state/watched/context.tsx b/src/state/watched/context.tsx index 00208956..ae6421ae 100644 --- a/src/state/watched/context.tsx +++ b/src/state/watched/context.tsx @@ -6,6 +6,7 @@ import { useCallback, useContext, useMemo, + useRef, useState, } from "react"; import { VideoProgressStore } from "./store"; @@ -180,15 +181,20 @@ export function useWatchedItem(meta: DetailedMeta | null) { () => watched.items.find((v) => meta && v.item.meta.id === meta?.meta.id), [watched, meta] ); + const lastCommitedTime = useRef([0, 0]); const callback = useCallback( (progress: number, total: number) => { - if (meta) { - // TODO add series support + // TODO add series support + const hasChanged = + lastCommitedTime.current[0] !== progress || + lastCommitedTime.current[1] !== total; + if (meta && hasChanged) { + lastCommitedTime.current = [progress, total]; updateProgress({ meta: meta.meta }, progress, total); } }, - [updateProgress, meta] + [meta, updateProgress] ); return { updateProgress: callback, watchedItem: item }; diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index f6bb4272..fb3583a6 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -1,5 +1,5 @@ import { useParams } from "react-router-dom"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer"; import { MWStream } from "@/backend/helpers/streams"; import { SelectedMediaData, useScrape } from "@/hooks/useScrape"; @@ -15,6 +15,7 @@ import { IconPatch } from "@/components/buttons/IconPatch"; import { Icons } from "@/components/Icon"; import { useWatchedItem } from "@/state/watched"; import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl"; +import { ShowControl } from "@/components/video/controls/ShowControl"; import { MediaFetchErrorView } from "./MediaErrorView"; import { MediaScrapeLog } from "./MediaScrapeLog"; import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView"; @@ -81,6 +82,37 @@ function MediaViewScraping(props: MediaViewScrapingProps) { ); } +interface MediaViewPlayerProps { + meta: DetailedMeta; + stream: MWStream; +} +export function MediaViewPlayer(props: MediaViewPlayerProps) { + const goBack = useGoBack(); + const { updateProgress, watchedItem } = useWatchedItem(props.meta); + const firstStartTime = useRef(watchedItem?.progress); + useEffect(() => { + firstStartTime.current = watchedItem?.progress; + // only want it to change when stream changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.stream]); + + return ( +
+ + + + + +
+ ); +} + export function MediaView() { const params = useParams<{ media: string }>(); const goBack = useGoBack(); @@ -101,8 +133,6 @@ export function MediaView() { }); const [stream, setStream] = useState(null); - const { updateProgress, watchedItem } = useWatchedItem(meta); - useEffect(() => { exec(params.media).then((v) => { setMeta(v ?? null); @@ -137,15 +167,5 @@ export function MediaView() { ); // show stream once we have a stream - return ( -
- - - - -
- ); + return ; } From f472f0473559cc623c7535aa51f8098b6f92b20b Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sun, 22 Jan 2023 19:26:08 +0100 Subject: [PATCH 049/135] episode ids , shorter debounce and flixHQ provider --- package.json | 2 + src/backend/helpers/fetch.ts | 17 +++- src/backend/helpers/provider.ts | 4 +- src/backend/helpers/scrape.ts | 4 +- src/backend/index.ts | 1 + src/backend/metadata/getmeta.ts | 15 +++- src/backend/metadata/justwatch.ts | 76 +++++++++++++++- src/backend/metadata/types.ts | 33 ++++++- src/backend/providers/flixhq.ts | 72 ++++++++++----- src/components/buttons/EditButton.tsx | 2 +- src/components/media/MediaCard.tsx | 6 +- src/components/video/controls/ShowControl.tsx | 30 +++++-- .../video/controls/VolumeControl.tsx | 2 +- src/components/video/hooks/controlVideo.ts | 5 +- src/components/video/hooks/useVideoPlayer.ts | 5 +- src/hooks/useScrape.ts | 4 +- src/index.tsx | 1 - src/setup/App.tsx | 5 ++ src/views/media/MediaErrorView.tsx | 4 + src/views/media/MediaView.tsx | 90 +++++++++++++------ src/views/notfound/NotFoundView.tsx | 4 + src/views/search/SearchResultsPartial.tsx | 2 +- src/views/search/SearchView.tsx | 4 + yarn.lock | 31 ++++++- 24 files changed, 337 insertions(+), 82 deletions(-) diff --git a/package.json b/package.json index ee11ba00..8dd02062 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "dependencies": { "@formkit/auto-animate": "^1.0.0-beta.5", "@headlessui/react": "^1.5.0", + "@types/react-helmet": "^6.1.6", "crypto-js": "^4.1.1", "fscreen": "^1.2.0", "fuse.js": "^6.4.6", @@ -19,6 +20,7 @@ "ofetch": "^1.0.0", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-helmet": "^6.1.0", "react-i18next": "^12.1.1", "react-router-dom": "^5.2.0", "react-stickynode": "^4.1.0", diff --git a/src/backend/helpers/fetch.ts b/src/backend/helpers/fetch.ts index 9804ff40..b61ae55c 100644 --- a/src/backend/helpers/fetch.ts +++ b/src/backend/helpers/fetch.ts @@ -21,7 +21,22 @@ export function mwFetch(url: string, ops: P[1]): R { } export function proxiedFetch(url: string, ops: P[1]): R { - const parsedUrl = new URL(url); + let combinedUrl = ops?.baseURL ?? ""; + if ( + combinedUrl.length > 0 && + combinedUrl.endsWith("/") && + url.startsWith("/") + ) + combinedUrl += url.slice(1); + else if ( + combinedUrl.length > 0 && + !combinedUrl.endsWith("/") && + !url.startsWith("/") + ) + combinedUrl += `/${url}`; + else combinedUrl += url; + + const parsedUrl = new URL(combinedUrl); Object.entries(ops?.params ?? {}).forEach(([k, v]) => { parsedUrl.searchParams.set(k, v); }); diff --git a/src/backend/helpers/provider.ts b/src/backend/helpers/provider.ts index 348152b3..95f5e374 100644 --- a/src/backend/helpers/provider.ts +++ b/src/backend/helpers/provider.ts @@ -20,8 +20,8 @@ type MWProviderTypeSpecific = } | { type: MWMediaType.SERIES; - episode: number; - season: number; + episode: string; + season: string; }; export type MWProviderContext = MWProviderTypeSpecific & MWProviderBase; diff --git a/src/backend/helpers/scrape.ts b/src/backend/helpers/scrape.ts index 3ad57843..2e9e5e65 100644 --- a/src/backend/helpers/scrape.ts +++ b/src/backend/helpers/scrape.ts @@ -31,8 +31,8 @@ type MWProviderRunContextTypeSpecific = } | { type: MWMediaType.SERIES; - episode: number; - season: number; + episode: string; + season: string; }; export type MWProviderRunContext = MWProviderRunContextBase & diff --git a/src/backend/index.ts b/src/backend/index.ts index 7140fe13..46764b1f 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -2,6 +2,7 @@ import { initializeScraperStore } from "./helpers/register"; // providers import "./providers/gdriveplayer"; +import "./providers/flixhq"; // embeds // -- nothing here yet diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index fc25fddf..b8717fcc 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -3,6 +3,7 @@ import { makeUrl, mwFetch } from "../helpers/fetch"; import { formatJWMeta, JWMediaResult, + JWSeasonMetaResult, JW_API_BASE, mediaTypeToJW, } from "./justwatch"; @@ -33,7 +34,8 @@ export interface DetailedMeta { export async function getMetaFromId( type: MWMediaType, - id: string + id: string, + seasonId?: string ): Promise { const queryType = mediaTypeToJW(type); @@ -61,8 +63,17 @@ export async function getMetaFromId( if (!imdbId || !tmdbId) throw new Error("not enough info"); + let seasonData: JWSeasonMetaResult | undefined; + if (data.object_type === "show") { + const seasonToScrape = seasonId ?? data.seasons?.[0].id.toString() ?? ""; + const url = makeUrl("/content/titles/show_season/{id}/locale/en_US", { + id: seasonToScrape, + }); + seasonData = await mwFetch(url, { baseURL: JW_API_BASE }); + } + return { - meta: formatJWMeta(data), + meta: formatJWMeta(data, seasonData), imdbId, tmdbId, }; diff --git a/src/backend/metadata/justwatch.ts b/src/backend/metadata/justwatch.ts index 31aa78ed..b3ef32f7 100644 --- a/src/backend/metadata/justwatch.ts +++ b/src/backend/metadata/justwatch.ts @@ -1,10 +1,22 @@ -import { MWMediaType } from "./types"; +import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types"; export const JW_API_BASE = "https://apis.justwatch.com"; export const JW_IMAGE_BASE = "https://images.justwatch.com"; export type JWContentTypes = "movie" | "show"; +export type JWSeasonShort = { + title: string; + id: number; + season_number: number; +}; + +export type JWEpisodeShort = { + title: string; + id: number; + episode_number: number; +}; + export type JWMediaResult = { title: string; poster?: string; @@ -12,6 +24,14 @@ export type JWMediaResult = { original_release_year: number; jw_entity_id: string; object_type: JWContentTypes; + seasons?: JWSeasonShort[]; +}; + +export type JWSeasonMetaResult = { + title: string; + id: string; + season_number: number; + episodes: JWEpisodeShort[]; }; export function mediaTypeToJW(type: MWMediaType): JWContentTypes { @@ -26,8 +46,24 @@ export function JWMediaToMediaType(type: string): MWMediaType { throw new Error("unsupported type"); } -export function formatJWMeta(media: JWMediaResult) { +export function formatJWMeta( + media: JWMediaResult, + season?: JWSeasonMetaResult +): MWMediaMeta { const type = JWMediaToMediaType(media.object_type); + let seasons: undefined | MWSeasonMeta[]; + if (type === MWMediaType.SERIES) { + seasons = media.seasons + ?.sort((a, b) => a.season_number - b.season_number) + .map( + (v): MWSeasonMeta => ({ + id: v.id.toString(), + number: v.season_number, + title: v.title, + }) + ); + } + return { title: media.title, id: media.id.toString(), @@ -36,5 +72,41 @@ export function formatJWMeta(media: JWMediaResult) { ? `${JW_IMAGE_BASE}${media.poster.replace("{profile}", "s166")}` : undefined, type, + seasons: seasons as any, + seasonData: season + ? ({ + id: season.id.toString(), + number: season.season_number, + title: season.title, + episodes: season.episodes + .sort((a, b) => a.episode_number - b.episode_number) + .map((v) => ({ + id: v.id.toString(), + number: v.episode_number, + title: v.title, + })), + } as any) + : (undefined as any), + }; +} + +export function JWMediaToId(media: MWMediaMeta): string { + return ["JW", mediaTypeToJW(media.type), media.id].join("-"); +} + +export function decodeJWId( + paramId: string +): { id: string; type: MWMediaType } | null { + const [prefix, type, id] = paramId.split("-", 3); + if (prefix !== "JW") return null; + let mediaType; + try { + mediaType = JWMediaToMediaType(type); + } catch { + return null; + } + return { + type: mediaType, + id, }; } diff --git a/src/backend/metadata/types.ts b/src/backend/metadata/types.ts index afabb970..66bb9c1a 100644 --- a/src/backend/metadata/types.ts +++ b/src/backend/metadata/types.ts @@ -4,14 +4,43 @@ export enum MWMediaType { ANIME = "anime", } -export type MWMediaMeta = { +export type MWSeasonMeta = { + id: string; + number: number; + title: string; +}; + +export type MWSeasonWithEpisodeMeta = { + id: string; + number: number; + title: string; + episodes: { + id: string; + number: number; + title: string; + }[]; +}; + +type MWMediaMetaBase = { title: string; id: string; year: string; poster?: string; - type: MWMediaType; }; +type MWMediaMetaSpecific = + | { + type: MWMediaType.MOVIE | MWMediaType.ANIME; + seasons: undefined; + } + | { + type: MWMediaType.SERIES; + seasons: MWSeasonMeta[]; + seasonData: MWSeasonWithEpisodeMeta; + }; + +export type MWMediaMeta = MWMediaMetaBase & MWMediaMetaSpecific; + export interface MWQuery { searchQuery: string; type: MWMediaType; diff --git a/src/backend/providers/flixhq.ts b/src/backend/providers/flixhq.ts index e5b5d1fe..493a1c3b 100644 --- a/src/backend/providers/flixhq.ts +++ b/src/backend/providers/flixhq.ts @@ -1,37 +1,63 @@ +import { proxiedFetch } from "../helpers/fetch"; import { registerProvider } from "../helpers/register"; +import { MWStreamQuality, MWStreamType } from "../helpers/streams"; import { MWMediaType } from "../metadata/types"; -const timeout = (time: number) => - new Promise((resolve) => { - setTimeout(() => resolve(), time); - }); +const flixHqBase = "https://api.consumet.org/movies/flixhq"; registerProvider({ - id: "testprov", - rank: 42, + id: "flixhq", + displayName: "FlixHQ", + rank: 100, type: [MWMediaType.MOVIE], - disabled: true, - async scrape({ progress }) { - await timeout(1000); + async scrape({ media, progress }) { + // search for relevant item + const searchResults = await proxiedFetch( + `/${encodeURIComponent(media.meta.title)}`, + { + baseURL: flixHqBase, + } + ); + // TODO fuzzy match or normalize title before comparison + const foundItem = searchResults.results.find((v: any) => { + return v.title === media.meta.title && v.releaseDate === media.meta.year; + }); + if (!foundItem) throw new Error("No watchable item found"); + const flixId = foundItem.id; + + // get media info progress(25); - await timeout(1000); - progress(50); - await timeout(1000); + const mediaInfo = await proxiedFetch("/info", { + baseURL: flixHqBase, + params: { + id: flixId, + }, + }); + + // get stream info from media progress(75); - await timeout(1000); + const watchInfo = await proxiedFetch("/watch", { + baseURL: flixHqBase, + params: { + episodeId: mediaInfo.episodes[0].id, + mediaId: flixId, + }, + }); + + // get best quality source + const source = watchInfo.sources.reduce((p: any, c: any) => + c.quality > p.quality ? c : p + ); return { - embeds: [ - // { - // type: MWEmbedType.OPENLOAD, - // url: "https://google.com", - // }, - // { - // type: MWEmbedType.ANOTHER, - // url: "https://google.com", - // }, - ], + embeds: [], + stream: { + streamUrl: source.url, + quality: MWStreamQuality.QUNKNOWN, + type: source.isM3U8 ? MWStreamType.HLS : MWStreamType.MP4, + captions: [], + }, }; }, }); diff --git a/src/components/buttons/EditButton.tsx b/src/components/buttons/EditButton.tsx index 01988b51..a72eb8f8 100644 --- a/src/components/buttons/EditButton.tsx +++ b/src/components/buttons/EditButton.tsx @@ -22,7 +22,7 @@ export function EditButton(props: EditButtonProps) { > {props.editing ? ( - Stop editing + Stop editing ) : ( )} diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 0a72d463..66a53ed2 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -1,7 +1,7 @@ import { Link } from "react-router-dom"; import { DotList } from "@/components/text/DotList"; import { MWMediaMeta } from "@/backend/metadata/types"; -import { mediaTypeToJW } from "@/backend/metadata/justwatch"; +import { JWMediaToId } from "@/backend/metadata/justwatch"; import { Icons } from "../Icon"; import { IconPatch } from "../buttons/IconPatch"; @@ -107,9 +107,7 @@ export function MediaCard(props: MediaCardProps) { const canLink = props.linkable && !props.closable; const link = canLink - ? `/media/${encodeURIComponent( - mediaTypeToJW(props.media.type) - )}-${encodeURIComponent(props.media.id)}` + ? `/media/${encodeURIComponent(JWMediaToId(props.media))}` : "#"; if (!props.linkable) return {content}; diff --git a/src/components/video/controls/ShowControl.tsx b/src/components/video/controls/ShowControl.tsx index 5e2467e9..5dafdf45 100644 --- a/src/components/video/controls/ShowControl.tsx +++ b/src/components/video/controls/ShowControl.tsx @@ -1,26 +1,46 @@ -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; import { useVideoPlayerState } from "../VideoContext"; interface ShowControlProps { series?: { - episode: number; - season: number; + episodeId: string; + seasonId: string; }; - title?: string; + onSelect?: (state: { episodeId?: string; seasonId?: string }) => void; } export function ShowControl(props: ShowControlProps) { const { videoState } = useVideoPlayerState(); + const lastState = useRef<{ + episodeId?: string; + seasonId?: string; + } | null>({ + episodeId: props.series?.episodeId, + seasonId: props.series?.seasonId, + }); useEffect(() => { videoState.setShowData({ current: props.series, isSeries: !!props.series, - title: props.title, }); // we only want it to run when props change, not when videoState changes // eslint-disable-next-line react-hooks/exhaustive-deps }, [props]); + useEffect(() => { + const currentState = { + episodeId: videoState.seasonData.current?.episodeId, + seasonId: videoState.seasonData.current?.seasonId, + }; + if ( + currentState.episodeId !== lastState.current?.episodeId || + currentState.seasonId !== lastState.current?.seasonId + ) { + lastState.current = currentState; + props.onSelect?.(currentState); + } + }, [videoState, props]); + return null; } diff --git a/src/components/video/controls/VolumeControl.tsx b/src/components/video/controls/VolumeControl.tsx index cfad9441..99ec160f 100644 --- a/src/components/video/controls/VolumeControl.tsx +++ b/src/components/video/controls/VolumeControl.tsx @@ -63,7 +63,7 @@ export function VolumeControl(props: Props) {
diff --git a/src/setup/App.tsx b/src/setup/App.tsx index 563a8643..6ab1374c 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -16,6 +16,11 @@ function App() { + diff --git a/src/views/media/MediaErrorView.tsx b/src/views/media/MediaErrorView.tsx index df5e9943..f9b98280 100644 --- a/src/views/media/MediaErrorView.tsx +++ b/src/views/media/MediaErrorView.tsx @@ -4,12 +4,16 @@ import { Link } from "@/components/text/Link"; import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader"; import { useGoBack } from "@/hooks/useGoBack"; import { conf } from "@/setup/config"; +import { Helmet } from "react-helmet"; export function MediaFetchErrorView() { const goBack = useGoBack(); return (
+ + Failed to load meta +
diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index fb3583a6..5265728f 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -1,11 +1,12 @@ -import { useParams } from "react-router-dom"; +import { useHistory, useParams } from "react-router-dom"; +import { Helmet } from "react-helmet"; import { useEffect, useRef, useState } from "react"; import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer"; import { MWStream } from "@/backend/helpers/streams"; import { SelectedMediaData, useScrape } from "@/hooks/useScrape"; import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader"; import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta"; -import { JWMediaToMediaType } from "@/backend/metadata/justwatch"; +import { decodeJWId } from "@/backend/metadata/justwatch"; import { SourceControl } from "@/components/video/controls/SourceControl"; import { Loading } from "@/components/layout/Loading"; import { useLoading } from "@/hooks/useLoading"; @@ -23,6 +24,9 @@ import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView"; function MediaViewLoading(props: { onGoBack(): void }) { return (
+ + Loading... +
@@ -51,6 +55,9 @@ function MediaViewScraping(props: MediaViewScrapingProps) { return (
+ + {props.meta.meta.title} +
@@ -85,6 +92,7 @@ function MediaViewScraping(props: MediaViewScrapingProps) { interface MediaViewPlayerProps { meta: DetailedMeta; stream: MWStream; + selected: SelectedMediaData; } export function MediaViewPlayer(props: MediaViewPlayerProps) { const goBack = useGoBack(); @@ -96,8 +104,13 @@ export function MediaViewPlayer(props: MediaViewPlayerProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.stream]); + // TODO show episode title + return (
+ + {props.meta.meta.title} + - + {props.selected.type === MWMediaType.SERIES ? ( + console.log("selected stuff", d)} + /> + ) : null}
); } export function MediaView() { - const params = useParams<{ media: string }>(); + const params = useParams<{ + media: string; + episode?: string; + season?: string; + }>(); const goBack = useGoBack(); + const history = useHistory(); const [meta, setMeta] = useState(null); const [selected, setSelected] = useState(null); - const [exec, loading, error] = useLoading(async (mediaParams: string) => { - let type: MWMediaType; - let id = ""; - try { - const [t, i] = mediaParams.split("-", 2); - type = JWMediaToMediaType(t); - id = i; - } catch (err) { - return null; + const [exec, loading, error] = useLoading( + async (mediaParams: string, seasonId?: string) => { + const data = decodeJWId(mediaParams); + if (!data) return null; + return getMetaFromId(data.type, data.id, seasonId); } - return getMetaFromId(type, id); - }); + ); const [stream, setStream] = useState(null); useEffect(() => { - exec(params.media).then((v) => { + console.log("I am being ran"); + exec(params.media, params.season).then((v) => { setMeta(v ?? null); - if (v) - setSelected({ - type: v.meta.type, - episode: 0 as any, - season: 0 as any, - }); - else setSelected(null); + if (v) { + if (v.meta.type !== MWMediaType.SERIES) { + setSelected({ + type: v.meta.type, + season: undefined, + episode: undefined, + }); + } else { + const season = params.season ?? v.meta.seasonData.id; + const episode = params.episode ?? v.meta.seasonData.episodes[0].id; + setSelected({ + type: MWMediaType.SERIES, + season, + episode, + }); + if (season !== params.season || episode !== params.episode) + history.replace( + `/media/${encodeURIComponent(params.media)}/${encodeURIComponent( + season + )}/${encodeURIComponent(episode)}` + ); + } + } else setSelected(null); }); - }, [exec, params.media]); + // dont rerender when params changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [exec, history]); if (loading) return ; if (error) return ; @@ -167,5 +207,5 @@ export function MediaView() { ); // show stream once we have a stream - return ; + return ; } diff --git a/src/views/notfound/NotFoundView.tsx b/src/views/notfound/NotFoundView.tsx index 49584bb3..00f4eddc 100644 --- a/src/views/notfound/NotFoundView.tsx +++ b/src/views/notfound/NotFoundView.tsx @@ -7,6 +7,7 @@ import { ArrowLink } from "@/components/text/ArrowLink"; import { Title } from "@/components/text/Title"; import { useGoBack } from "@/hooks/useGoBack"; import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader"; +import { Helmet } from "react-helmet"; export function NotFoundWrapper(props: { children?: ReactNode; @@ -16,6 +17,9 @@ export function NotFoundWrapper(props: { return (
+ + Not found + {props.video ? (
diff --git a/src/views/search/SearchResultsPartial.tsx b/src/views/search/SearchResultsPartial.tsx index d7859612..63250193 100644 --- a/src/views/search/SearchResultsPartial.tsx +++ b/src/views/search/SearchResultsPartial.tsx @@ -13,7 +13,7 @@ export function SearchResultsPartial({ search }: SearchResultsPartialProps) { const [searching, setSearching] = useState(false); const [loading, setLoading] = useState(false); - const debouncedSearch = useDebounce(search, 2000); + const debouncedSearch = useDebounce(search, 500); useEffect(() => { setSearching(search.searchQuery !== ""); setLoading(search.searchQuery !== ""); diff --git a/src/views/search/SearchView.tsx b/src/views/search/SearchView.tsx index ad61b696..f2fc661e 100644 --- a/src/views/search/SearchView.tsx +++ b/src/views/search/SearchView.tsx @@ -7,6 +7,7 @@ import { SearchBarInput } from "@/components/SearchBar"; import { Title } from "@/components/text/Title"; import { useSearchQuery } from "@/hooks/useSearchQuery"; import { WideContainer } from "@/components/layout/WideContainer"; +import { Helmet } from "react-helmet"; import { SearchResultsPartial } from "./SearchResultsPartial"; export function SearchView() { @@ -22,6 +23,9 @@ export function SearchView() { return ( <>
+ + movie-web +
diff --git a/yarn.lock b/yarn.lock index 50e52b88..8378343c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -321,6 +321,13 @@ dependencies: "@types/react" "^17" +"@types/react-helmet@^6.1.6": + "integrity" "sha512-ZKcoOdW/Tg+kiUbkFCBtvDw0k3nD4HJ/h/B9yWxN4uDO8OkRksWTO+EL+z/Qu3aHTeTll3Ro0Cc/8UhwBCMG5A==" + "resolved" "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.6.tgz" + "version" "6.1.6" + dependencies: + "@types/react" "*" + "@types/react-router-dom@^5.3.3": "integrity" "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==" "resolved" "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz" @@ -2881,7 +2888,7 @@ dependencies: "read" "1" -"prop-types@^15.6.0", "prop-types@^15.6.2", "prop-types@^15.8.1": +"prop-types@^15.6.0", "prop-types@^15.6.2", "prop-types@^15.7.2", "prop-types@^15.8.1": "integrity" "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==" "resolved" "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" "version" "15.8.1" @@ -2924,6 +2931,21 @@ "object-assign" "^4.1.1" "scheduler" "^0.20.2" +"react-fast-compare@^3.1.1": + "integrity" "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" + "resolved" "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz" + "version" "3.2.0" + +"react-helmet@^6.1.0": + "integrity" "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==" + "resolved" "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz" + "version" "6.1.0" + dependencies: + "object-assign" "^4.1.1" + "prop-types" "^15.7.2" + "react-fast-compare" "^3.1.1" + "react-side-effect" "^2.1.0" + "react-i18next@^12.1.1": "integrity" "sha512-mFdieOI0LDy84q3JuZU6Aou1DoWW2fhapcTGeBS8+vWSJuViuoCLQAMYSb0QoHhXS8B0WKUOPpx4cffAP7r/aA==" "resolved" "https://registry.npmjs.org/react-i18next/-/react-i18next-12.1.1.tgz" @@ -2965,6 +2987,11 @@ "tiny-invariant" "^1.0.2" "tiny-warning" "^1.0.0" +"react-side-effect@^2.1.0": + "integrity" "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==" + "resolved" "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz" + "version" "2.1.2" + "react-stickynode@^4.1.0": "integrity" "sha512-zylWgfad75jLfh/gYIayDcDWIDwO4weZrsZqDpjZ/axhF06zRjdCWFBgUr33Pvv2+htKWqPSFksWTyB6aMQ1ZQ==" "resolved" "https://registry.npmjs.org/react-stickynode/-/react-stickynode-4.1.0.tgz" @@ -2986,7 +3013,7 @@ "loose-envify" "^1.4.0" "prop-types" "^15.6.2" -"react@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16 || ^17 || ^18", "react@^17.0.2", "react@>= 16.8.0", "react@>=15", "react@>=16.6.0", "react@17.0.2": +"react@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16 || ^17 || ^18", "react@^16.3.0 || ^17.0.0 || ^18.0.0", "react@^17.0.2", "react@>= 16.8.0", "react@>=15", "react@>=16.3.0", "react@>=16.6.0", "react@17.0.2": "integrity" "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==" "resolved" "https://registry.npmjs.org/react/-/react-17.0.2.tgz" "version" "17.0.2" From e7a648409426c45f04196760955464f169a826b7 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sun, 22 Jan 2023 20:51:58 +0100 Subject: [PATCH 050/135] fix multi origin and add airplay support --- src/backend/metadata/getmeta.ts | 6 ++--- src/backend/metadata/search.ts | 4 +-- src/components/Icon.tsx | 2 ++ src/components/video/DecoratedVideoPlayer.tsx | 2 ++ .../video/controls/AirplayControl.tsx | 26 +++++++++++++++++++ src/components/video/hooks/controlVideo.ts | 7 +++++ src/components/video/hooks/useVideoPlayer.ts | 18 +++++++++++++ src/index.tsx | 4 ++- src/views/media/MediaView.tsx | 1 - 9 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 src/components/video/controls/AirplayControl.tsx diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index b8717fcc..cb622e3b 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -1,5 +1,5 @@ import { FetchError } from "ofetch"; -import { makeUrl, mwFetch } from "../helpers/fetch"; +import { makeUrl, proxiedFetch } from "../helpers/fetch"; import { formatJWMeta, JWMediaResult, @@ -45,7 +45,7 @@ export async function getMetaFromId( type: queryType, id, }); - data = await mwFetch(url, { baseURL: JW_API_BASE }); + data = await proxiedFetch(url, { baseURL: JW_API_BASE }); } catch (err) { if (err instanceof FetchError) { // 400 and 404 are treated as not found @@ -69,7 +69,7 @@ export async function getMetaFromId( const url = makeUrl("/content/titles/show_season/{id}/locale/en_US", { id: seasonToScrape, }); - seasonData = await mwFetch(url, { baseURL: JW_API_BASE }); + seasonData = await proxiedFetch(url, { baseURL: JW_API_BASE }); } return { diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 4ad0434b..1c3c4598 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -1,5 +1,5 @@ import { SimpleCache } from "@/utils/cache"; -import { mwFetch } from "../helpers/fetch"; +import { proxiedFetch } from "../helpers/fetch"; import { formatJWMeta, JWContentTypes, @@ -42,7 +42,7 @@ export async function searchForMedia(query: MWQuery): Promise { page_size: 40, }; - const data = await mwFetch>( + const data = await proxiedFetch>( "/content/titles/en_US/popular", { baseURL: JW_API_BASE, diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 2b8fa81e..54287d0c 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -25,6 +25,7 @@ export enum Icons { VOLUME_X = "volume_x", X = "x", EDIT = "edit", + AIRPLAY = "airplay", } export interface IconProps { @@ -57,6 +58,7 @@ const iconList: Record = { x: ``, edit: ``, bookmark_outline: ``, + airplay: ``, }; export const Icon = memo((props: IconProps) => { diff --git a/src/components/video/DecoratedVideoPlayer.tsx b/src/components/video/DecoratedVideoPlayer.tsx index 11b4fd68..41d2d224 100644 --- a/src/components/video/DecoratedVideoPlayer.tsx +++ b/src/components/video/DecoratedVideoPlayer.tsx @@ -1,6 +1,7 @@ import { MWMediaMeta } from "@/backend/metadata/types"; import { useCallback, useRef, useState } from "react"; import { CSSTransition } from "react-transition-group"; +import { AirplayControl } from "./controls/AirplayControl"; import { BackdropControl } from "./controls/BackdropControl"; import { FullscreenControl } from "./controls/FullscreenControl"; import { LoadingControl } from "./controls/LoadingControl"; @@ -91,6 +92,7 @@ export function DecoratedVideoPlayer(
+
diff --git a/src/components/video/controls/AirplayControl.tsx b/src/components/video/controls/AirplayControl.tsx new file mode 100644 index 00000000..55ba7ec4 --- /dev/null +++ b/src/components/video/controls/AirplayControl.tsx @@ -0,0 +1,26 @@ +import { Icons } from "@/components/Icon"; +import { useCallback } from "react"; +import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton"; +import { useVideoPlayerState } from "../VideoContext"; + +interface Props { + className?: string; +} + +export function AirplayControl(props: Props) { + const { videoState } = useVideoPlayerState(); + + const handleClick = useCallback(() => { + videoState.startAirplay(); + }, [videoState]); + + if (!videoState.canAirplay) return null; + + return ( + + ); +} diff --git a/src/components/video/hooks/controlVideo.ts b/src/components/video/hooks/controlVideo.ts index 9ff7f5e8..531e94c3 100644 --- a/src/components/video/hooks/controlVideo.ts +++ b/src/components/video/hooks/controlVideo.ts @@ -30,6 +30,7 @@ export interface PlayerControls { setLeftControlsHover(hovering: boolean): void; initPlayer(sourceUrl: string, sourceType: MWStreamType): void; setShowData(data: ShowData): void; + startAirplay(): void; } export const initialControls: PlayerControls = { @@ -43,6 +44,7 @@ export const initialControls: PlayerControls = { setLeftControlsHover: () => null, initPlayer: () => null, setShowData: () => null, + startAirplay: () => null, }; export function populateControls( @@ -118,6 +120,11 @@ export function populateControls( setShowData(data) { update((s) => ({ ...s, seasonData: data })); }, + startAirplay() { + const videoPlayer = player as any; + if (videoPlayer.webkitShowPlaybackTargetPicker) + videoPlayer.webkitShowPlaybackTargetPicker(); + }, initPlayer(sourceUrl: string, sourceType: MWStreamType) { this.setVolume(getStoredVolume()); diff --git a/src/components/video/hooks/useVideoPlayer.ts b/src/components/video/hooks/useVideoPlayer.ts index 296cd683..dfb929c7 100644 --- a/src/components/video/hooks/useVideoPlayer.ts +++ b/src/components/video/hooks/useVideoPlayer.ts @@ -34,6 +34,7 @@ export type PlayerState = { name: string; description: string; }; + canAirplay: boolean; }; export type PlayerContext = PlayerState & PlayerControls; @@ -57,6 +58,7 @@ export const initialPlayerState: PlayerContext = { seasonData: { isSeries: false, }, + canAirplay: false, ...initialControls, }; @@ -159,6 +161,14 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { : null, })); }; + const canAirplay = (e: any) => { + if (e.availability === "available") { + update((s) => ({ + ...s, + canAirplay: true, + })); + } + }; player.addEventListener("pause", pause); player.addEventListener("playing", playing); @@ -172,6 +182,10 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { player.addEventListener("waiting", waiting); player.addEventListener("canplay", canplay); player.addEventListener("error", error); + player.addEventListener( + "webkitplaybacktargetavailabilitychanged", + canAirplay + ); return () => { player.removeEventListener("pause", pause); @@ -186,6 +200,10 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { player.removeEventListener("waiting", waiting); player.removeEventListener("canplay", canplay); player.removeEventListener("error", error); + player.removeEventListener( + "webkitplaybacktargetavailabilitychanged", + canAirplay + ); }; } diff --git a/src/index.tsx b/src/index.tsx index ea6781b3..5a471b6f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -25,6 +25,7 @@ if (key) { // - source selection // - safari fullscreen will make video overlap player controls // - safari progress bar is fucked (video doesnt change time but video.currentTime does change) +// - safari progress bar cannot be dragged // TODO stuff to test: // - browser: firefox, chrome, edge, safari desktop @@ -41,7 +42,8 @@ if (key) { // - AFTER all that: rank providers/embedscrapers // TODO general todos: -// - localize everything +// - localize everything (fix loading screen text (series vs movies)) +// - make mobile friendly ReactDOM.render( diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index 5265728f..d8014674 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -155,7 +155,6 @@ export function MediaView() { const [stream, setStream] = useState(null); useEffect(() => { - console.log("I am being ran"); exec(params.media, params.season).then((v) => { setMeta(v ?? null); if (v) { From 7a2865313d431ec76b1e347d3b2edf6a90b36475 Mon Sep 17 00:00:00 2001 From: Jip Fr Date: Sun, 22 Jan 2023 23:01:49 +0100 Subject: [PATCH 051/135] feat(providers): add superstream --- src/backend/helpers/streams.ts | 1 + src/backend/index.ts | 1 + .../providers/superstream/superstream.ts | 137 +++++++++++++----- yarn.lock | 102 ++++++------- 4 files changed, 147 insertions(+), 94 deletions(-) diff --git a/src/backend/helpers/streams.ts b/src/backend/helpers/streams.ts index 628bad4a..3c80f7a6 100644 --- a/src/backend/helpers/streams.ts +++ b/src/backend/helpers/streams.ts @@ -5,6 +5,7 @@ export enum MWStreamType { export enum MWStreamQuality { Q360P = "360p", + Q480P = "480p", Q720P = "720p", Q1080P = "1080p", QUNKNOWN = "unknown", diff --git a/src/backend/index.ts b/src/backend/index.ts index 46764b1f..7859f2cd 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -3,6 +3,7 @@ import { initializeScraperStore } from "./helpers/register"; // providers import "./providers/gdriveplayer"; import "./providers/flixhq"; +import "./providers/superstream/superstream"; // embeds // -- nothing here yet diff --git a/src/backend/providers/superstream/superstream.ts b/src/backend/providers/superstream/superstream.ts index 86987ecd..cb0c3928 100644 --- a/src/backend/providers/superstream/superstream.ts +++ b/src/backend/providers/superstream/superstream.ts @@ -5,9 +5,20 @@ import { conf } from "@/setup/config"; import { customAlphabet } from "nanoid"; // import toWebVTT from "srt-webvtt"; import CryptoJS from "crypto-js"; +import { proxiedFetch } from "@/backend/helpers/fetch"; +import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams"; +import { MetadataSchema } from "hls.js"; const nanoid = customAlphabet("0123456789abcdef", 32); +const qualityMap = { + "360p": MWStreamQuality.Q360P, + "480p": MWStreamQuality.Q480P, + "720p": MWStreamQuality.Q720P, + "1080p": MWStreamQuality.Q1080P, +}; +type QualityInMap = keyof typeof qualityMap; + // CONSTANTS, read below (taken from og) // We do not want content scanners to notice this scraping going on so we've hidden all constants // The source has its origins in China so I added some extra security with banned words @@ -76,8 +87,9 @@ const get = (data: object, altApi = false) => { formatted.append("medium", "Website"); const requestUrl = altApi ? apiUrls[1] : apiUrls[0]; - return fetch(`${conf().CORS_PROXY_URL}${requestUrl}`, { + return proxiedFetch(requestUrl, { method: "POST", + parseResponse: JSON.parse, headers: { Platform: "android", "Content-Type": "application/x-www-form-urlencoded", @@ -88,26 +100,44 @@ const get = (data: object, altApi = false) => { registerProvider({ id: "superstream", - rank: 50, + displayName: "Superstream", + rank: 200, type: [MWMediaType.MOVIE, MWMediaType.SERIES], - disabled: true, - async scrape({ - media: { - meta: { type }, - tmdbId, - }, - }) { - if (type === MWMediaType.MOVIE) { + async scrape({ media, episode, progress }) { + // Find Superstream ID for show + const searchQuery = { + module: "Search3", + page: "1", + type: "all", + keyword: media.meta.title, + pagelimit: "20", + }; + const searchRes = (await get(searchQuery, true)).data; + progress(33); + + // TODO: add fuzzy search and normalise strings before matching + const superstreamEntry = searchRes.find( + (res: any) => + res.title === media.meta.title && res.year === Number(media.meta.year) + ); + + if (!superstreamEntry) throw new Error("No entry found on SuperStream"); + const superstreamId = superstreamEntry.id; + + // Movie logic + if (media.meta.type === MWMediaType.MOVIE) { const apiQuery = { uid: "", module: "Movie_downloadurl_v3", - mid: tmdbId, + mid: superstreamId, oss: "1", group: "", }; - const mediaRes = (await get(apiQuery).then((r) => r.json())).data; + const mediaRes = (await get(apiQuery)).data; + progress(50); + const hdQuality = mediaRes.list.find( (quality: any) => quality.quality === "1080p" && quality.path @@ -124,6 +154,8 @@ registerProvider({ if (!hdQuality) throw new Error("No quality could be found."); + console.log(hdQuality); + // const subtitleApiQuery = { // fid: hdQuality.fid, // uid: "", @@ -147,34 +179,52 @@ registerProvider({ // }) // ); - return { embeds: [], stream: hdQuality.path }; + return { + embeds: [], + stream: { + streamUrl: hdQuality.path, + quality: qualityMap[hdQuality.quality as QualityInMap], + type: MWStreamType.MP4, + }, + }; } - // const apiQuery = { - // uid: "", - // module: "TV_downloadurl_v3", - // episode: media.episodeId, - // tid: media.mediaId, - // season: media.seasonId, - // oss: "1", - // group: "", - // }; - // const mediaRes = (await get(apiQuery).then((r) => r.json())).data; - // const hdQuality = - // mediaRes.list.find( - // (quality: any) => quality.quality === "1080p" && quality.path - // ) ?? - // mediaRes.list.find( - // (quality: any) => quality.quality === "720p" && quality.path - // ) ?? - // mediaRes.list.find( - // (quality: any) => quality.quality === "480p" && quality.path - // ) ?? - // mediaRes.list.find( - // (quality: any) => quality.quality === "360p" && quality.path - // ); - - // if (!hdQuality) throw new Error("No quality could be found."); + if (media.meta.type !== MWMediaType.SERIES) + throw new Error("Unsupported type"); + + // Fetch requested episode + const apiQuery = { + uid: "", + module: "TV_downloadurl_v3", + tid: superstreamId, + season: media.meta.seasonData.number.toString(), + episode: ( + media.meta.seasonData.episodes.find( + (episodeInfo) => episodeInfo.id === episode + )?.number ?? 1 + ).toString(), + oss: "1", + group: "", + }; + + const mediaRes = (await get(apiQuery)).data; + progress(66); + + const hdQuality = + mediaRes.list.find( + (quality: any) => quality.quality === "1080p" && quality.path + ) ?? + mediaRes.list.find( + (quality: any) => quality.quality === "720p" && quality.path + ) ?? + mediaRes.list.find( + (quality: any) => quality.quality === "480p" && quality.path + ) ?? + mediaRes.list.find( + (quality: any) => quality.quality === "360p" && quality.path + ); + + if (!hdQuality) throw new Error("No quality could be found."); // const subtitleApiQuery = { // fid: hdQuality.fid, @@ -200,6 +250,15 @@ registerProvider({ // }) // ); - return { embeds: [] }; + return { + embeds: [], + stream: { + quality: qualityMap[ + hdQuality.quality as QualityInMap + ] as MWStreamQuality, + streamUrl: hdQuality.path, + type: MWStreamType.MP4, + }, + }; }, }); diff --git a/yarn.lock b/yarn.lock index 8378343c..2b105f10 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20,9 +20,9 @@ "@colors/colors@1.5.0": "version" "1.5.0" -"@esbuild/linux-x64@0.16.5": - "integrity" "sha512-vsOwzKN+4NenUTyuoWLmg5dAuO8JKuLD9MXSeENA385XucuOZbblmOMwwgPlHsgVRtSjz38riqPJU2ALI/CWYQ==" - "resolved" "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.5.tgz" +"@esbuild/darwin-arm64@0.16.5": + "integrity" "sha512-4HlbUMy50cRaHGVriBjShs46WRPshtnVOqkxEGhEuDuJhgZ3regpWzaQxXOcDXFvVwue8RiqDAAcOi/QlVLE6Q==" + "resolved" "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.5.tgz" "version" "0.16.5" "@eslint/eslintrc@^1.3.3": @@ -98,8 +98,8 @@ "@nodelib/fs.scandir" "2.1.5" "fastq" "^1.6.0" -"@npmcli/arborist@^6.1.5": - "version" "6.1.5" +"@npmcli/arborist@^6.1.6": + "version" "6.1.6" dependencies: "@isaacs/string-locale-compare" "^1.1.0" "@npmcli/fs" "^3.1.0" @@ -135,8 +135,8 @@ "treeverse" "^3.0.0" "walk-up-path" "^1.0.0" -"@npmcli/config@^6.1.0": - "version" "6.1.0" +"@npmcli/config@^6.1.1": + "version" "6.1.1" dependencies: "@npmcli/map-workspaces" "^3.0.0" "ini" "^3.0.0" @@ -233,14 +233,9 @@ "read-package-json-fast" "^3.0.0" "which" "^3.0.0" -"@swc/core-linux-x64-gnu@1.3.22": - "integrity" "sha512-FLkbiqsdXsVIFZi6iedx4rSBGX8x0vo/5aDlklSxJAAYOcQpO0QADKP5Yr65iMT1d6ABCt2d+/StpGLF7GWOcA==" - "resolved" "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.22.tgz" - "version" "1.3.22" - -"@swc/core-linux-x64-musl@1.3.22": - "integrity" "sha512-giBuw+Z0Bq6fpZ0Y5TcfpcQwf9p/cE1fOQyO/K1XSTn/haQOqFi7421Jq/dFThSARZiXw1u9Om9VFbwxr8VI+A==" - "resolved" "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.22.tgz" +"@swc/core-darwin-arm64@1.3.22": + "integrity" "sha512-MMhtPsuXp8gpUgr9bs+RZQ2IyFGiUNDG93usCDAFgAF+6VVp+YaAVjET/3/Bx5Lk2WAt0RxT62C9KTEw1YMo3w==" + "resolved" "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.22.tgz" "version" "1.3.22" "@swc/core@^1.3.21": @@ -891,9 +886,9 @@ "version" "3.26.1" "core-js@^3.6.5": - "integrity" "sha512-GutwJLBChfGCpwwhbYoqfv03LAfmiz7e7D/BNxzeMxwQf10GRSzqiOjx7AmtEk+heiD/JWmBuyBPgFtx0Sg1ww==" - "resolved" "https://registry.npmjs.org/core-js/-/core-js-3.27.1.tgz" - "version" "3.27.1" + "integrity" "sha512-9ashVQskuh5AZEZ1JdQWp1GqSoC1e1G87MzRqg2gIfVAQ7Qn9K+uFj8EcniUFA4P2NLZfV+TOlX1SzoKfo+s7w==" + "resolved" "https://registry.npmjs.org/core-js/-/core-js-3.27.2.tgz" + "version" "3.27.2" "cross-fetch@3.1.5": "integrity" "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==" @@ -1055,13 +1050,6 @@ "resolved" "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" "version" "9.2.2" -"encoding@^0.1.0": - "integrity" "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==" - "resolved" "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz" - "version" "0.1.13" - dependencies: - "iconv-lite" "^0.6.2" - "encoding@^0.1.13": "version" "0.1.13" dependencies: @@ -1501,6 +1489,11 @@ "resolved" "https://registry.npmjs.org/fscreen/-/fscreen-1.2.0.tgz" "version" "1.2.0" +"fsevents@~2.3.2": + "integrity" "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==" + "resolved" "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" + "version" "2.3.2" + "function-bind@^1.1.1": "integrity" "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" "resolved" "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" @@ -2022,16 +2015,16 @@ "version" "1.1.4" "json5@^1.0.1": - "integrity" "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==" - "resolved" "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz" - "version" "1.0.2" + "integrity" "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==" + "resolved" "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz" + "version" "1.0.1" dependencies: "minimist" "^1.2.0" "json5@^2.2.0": - "integrity" "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" - "resolved" "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" - "version" "2.2.3" + "integrity" "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" + "resolved" "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz" + "version" "2.2.1" "jsonparse@^1.3.1": "version" "1.3.1" @@ -2076,10 +2069,10 @@ "npm-package-arg" "^10.1.0" "npm-registry-fetch" "^14.0.3" -"libnpmdiff@^5.0.6": - "version" "5.0.6" +"libnpmdiff@^5.0.7": + "version" "5.0.7" dependencies: - "@npmcli/arborist" "^6.1.5" + "@npmcli/arborist" "^6.1.6" "@npmcli/disparity-colors" "^3.0.0" "@npmcli/installed-package-contents" "^2.0.0" "binary-extensions" "^2.2.0" @@ -2089,10 +2082,10 @@ "pacote" "^15.0.7" "tar" "^6.1.13" -"libnpmexec@^5.0.6": - "version" "5.0.6" +"libnpmexec@^5.0.7": + "version" "5.0.7" dependencies: - "@npmcli/arborist" "^6.1.5" + "@npmcli/arborist" "^6.1.6" "@npmcli/run-script" "^6.0.0" "chalk" "^4.1.0" "ci-info" "^3.7.0" @@ -2105,10 +2098,10 @@ "semver" "^7.3.7" "walk-up-path" "^1.0.0" -"libnpmfund@^4.0.6": - "version" "4.0.6" +"libnpmfund@^4.0.7": + "version" "4.0.7" dependencies: - "@npmcli/arborist" "^6.1.5" + "@npmcli/arborist" "^6.1.6" "libnpmhook@^9.0.1": "version" "9.0.1" @@ -2122,10 +2115,10 @@ "aproba" "^2.0.0" "npm-registry-fetch" "^14.0.3" -"libnpmpack@^5.0.6": - "version" "5.0.6" +"libnpmpack@^5.0.7": + "version" "5.0.7" dependencies: - "@npmcli/arborist" "^6.1.5" + "@npmcli/arborist" "^6.1.6" "@npmcli/run-script" "^6.0.0" "npm-package-arg" "^10.1.0" "pacote" "^15.0.7" @@ -2288,9 +2281,9 @@ "encoding" "^0.1.13" "minipass-fetch@^3.0.0": - "version" "3.0.0" + "version" "3.0.1" dependencies: - "minipass" "^3.1.6" + "minipass" "^4.0.0" "minipass-sized" "^1.0.3" "minizlib" "^2.1.2" optionalDependencies: @@ -2499,13 +2492,13 @@ "version" "1.0.1" "npm@^9.2.0": - "integrity" "sha512-oypVdaWGHDuV79RXLvp+B9gh6gDyAmoHKrQ0/JBYTWWx5D8/+AAxFdZC84fSIiyDdyW4qfrSyYGKhekxDOaMXQ==" - "resolved" "https://registry.npmjs.org/npm/-/npm-9.2.0.tgz" - "version" "9.2.0" + "integrity" "sha512-ydRVmnWEVXmc3DCM+F9BjiNj3IHkZ3Mwz5VbJYS2BpY/6d4PcKxNW+Xb0vzGeE6PkVhLcPxwhoIi+RFV2fSfEA==" + "resolved" "https://registry.npmjs.org/npm/-/npm-9.3.1.tgz" + "version" "9.3.1" dependencies: "@isaacs/string-locale-compare" "^1.1.0" - "@npmcli/arborist" "^6.1.5" - "@npmcli/config" "^6.1.0" + "@npmcli/arborist" "^6.1.6" + "@npmcli/config" "^6.1.1" "@npmcli/map-workspaces" "^3.0.0" "@npmcli/package-json" "^3.0.0" "@npmcli/run-script" "^6.0.0" @@ -2527,12 +2520,12 @@ "is-cidr" "^4.0.2" "json-parse-even-better-errors" "^3.0.0" "libnpmaccess" "^7.0.1" - "libnpmdiff" "^5.0.6" - "libnpmexec" "^5.0.6" - "libnpmfund" "^4.0.6" + "libnpmdiff" "^5.0.7" + "libnpmexec" "^5.0.7" + "libnpmfund" "^4.0.7" "libnpmhook" "^9.0.1" "libnpmorg" "^5.0.1" - "libnpmpack" "^5.0.6" + "libnpmpack" "^5.0.7" "libnpmpublish" "^7.0.6" "libnpmsearch" "^6.0.1" "libnpmteam" "^5.0.1" @@ -2561,7 +2554,6 @@ "read" "~1.0.7" "read-package-json" "^6.0.0" "read-package-json-fast" "^3.0.1" - "rimraf" "^3.0.2" "semver" "^7.3.8" "ssri" "^10.0.1" "tar" "^6.1.13" From 5e1727e8f70c069e4bd5353f68f0a5acb8d00848 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sun, 22 Jan 2023 23:03:55 +0100 Subject: [PATCH 052/135] provider changes Co-authored-by: James Hawkins --- index.html | 2 + src/backend/helpers/fetch.ts | 4 +- src/backend/index.ts | 1 + src/backend/metadata/getmeta.ts | 1 + src/backend/providers/gomostream.ts | 98 +++++++++++++++++++ src/components/video/DecoratedVideoPlayer.tsx | 2 + .../video/controls/ChromeCastControl.tsx | 15 +++ 7 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 src/backend/providers/gomostream.ts create mode 100644 src/components/video/controls/ChromeCastControl.tsx diff --git a/index.html b/index.html index c6f4d667..cd808f2e 100644 --- a/index.html +++ b/index.html @@ -41,6 +41,8 @@ + + movie-web diff --git a/src/backend/helpers/fetch.ts b/src/backend/helpers/fetch.ts index b61ae55c..b2871c4f 100644 --- a/src/backend/helpers/fetch.ts +++ b/src/backend/helpers/fetch.ts @@ -16,11 +16,11 @@ export function makeUrl(url: string, data: Record) { return parsedUrl; } -export function mwFetch(url: string, ops: P[1]): R { +export function mwFetch(url: string, ops: P[1] = {}): R { return baseFetch(url, ops); } -export function proxiedFetch(url: string, ops: P[1]): R { +export function proxiedFetch(url: string, ops: P[1] = {}): R { let combinedUrl = ops?.baseURL ?? ""; if ( combinedUrl.length > 0 && diff --git a/src/backend/index.ts b/src/backend/index.ts index 46764b1f..6dbb95a6 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -3,6 +3,7 @@ import { initializeScraperStore } from "./helpers/register"; // providers import "./providers/gdriveplayer"; import "./providers/flixhq"; +import "./providers/gomostream"; // embeds // -- nothing here yet diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index cb622e3b..7fb14ae6 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -54,6 +54,7 @@ export async function getMetaFromId( throw err; } + console.log(data.external_ids); const imdbId = data.external_ids.find( (v) => v.provider === "imdb_latest" )?.external_id; diff --git a/src/backend/providers/gomostream.ts b/src/backend/providers/gomostream.ts new file mode 100644 index 00000000..275e6099 --- /dev/null +++ b/src/backend/providers/gomostream.ts @@ -0,0 +1,98 @@ +import { unpack } from "unpacker"; +import { proxiedFetch } from "../helpers/fetch"; +import { registerProvider } from "../helpers/register"; +import { MWStreamQuality, MWStreamType } from "../helpers/streams"; +import { MWMediaType } from "../metadata/types"; +import json5 from "json5"; + +const gomoBase = "https://gomo.to/"; + +registerProvider({ + id: "gomostream", + displayName: "gomostream", + rank: 999, + type: [MWMediaType.MOVIE], + + async scrape({ media, progress }) { + // get movie from gomostream + const contentResult = await proxiedFetch( + `/${media.meta.type}/${media.imdbId}`, + { + baseURL: gomoBase, + } + ); + + // movie doesn't exist + if ( + contentResult === "Movie not available." || + contentResult === "Episode not available." + ) + throw new Error("No watchable item found."); + + // decode stream + progress(25); + + const tc = contentResult.match(/var tc = '(.+)';/)?.[1] || ""; + const _token = contentResult.match(/"_token": "(.+)",/)?.[1] || ""; + + const fd = new FormData(); + fd.append("tokenCode", tc); + fd.append("_token", _token); + + const src = await proxiedFetch(`/decoding_v3.php`, { + baseURL: gomoBase, + method: "POST", + body: fd, + headers: { + "x-token": `${tc.slice(5, 13).split("").reverse().join("")}13574199`, + }, + parseResponse: JSON.parse, + }); + + // TODO should check all embed urls in future + const embedUrl = src.filter((url: string) => url.includes("gomo.to"))[1]; + + // get stream info + progress(50); + + const streamRes = await proxiedFetch(embedUrl); + + const streamResDom = new DOMParser().parseFromString( + streamRes, + "text/html" + ); + if (streamResDom.body.innerText === "File was deleted") + throw new Error("No watchable item found."); + + const script = Array.from(streamResDom.querySelectorAll("script")).find( + (s: HTMLScriptElement) => + s.innerHTML.includes("eval(function(p,a,c,k,e,d") + )?.innerHTML; + if (!script) throw new Error("Could not get packed data"); + + // unpack data + progress(75); + + const unpacked = unpack(script); + const rawSources = /sources:(\[.*?\])/.exec(unpacked); + if (!rawSources) throw new Error("Could not get stream URL"); + + const sources = json5.parse(rawSources[1]); + const streamUrl = sources[0].file; + + console.log(sources); + + const streamType = streamUrl.split(".").at(-1); + if (streamType !== "mp4" && streamType !== "m3u8") + throw new Error("Unsupported stream type"); + + return { + embeds: [], + stream: { + quality: streamType, + streamUrl: streamUrl, + type: streamType, + }, + }; + }, +}); diff --git a/src/components/video/DecoratedVideoPlayer.tsx b/src/components/video/DecoratedVideoPlayer.tsx index 41d2d224..702496d8 100644 --- a/src/components/video/DecoratedVideoPlayer.tsx +++ b/src/components/video/DecoratedVideoPlayer.tsx @@ -3,6 +3,7 @@ import { useCallback, useRef, useState } from "react"; import { CSSTransition } from "react-transition-group"; import { AirplayControl } from "./controls/AirplayControl"; import { BackdropControl } from "./controls/BackdropControl"; +import { ChromeCastControl } from "./controls/ChromeCastControl"; import { FullscreenControl } from "./controls/FullscreenControl"; import { LoadingControl } from "./controls/LoadingControl"; import { MiddlePauseControl } from "./controls/MiddlePauseControl"; @@ -93,6 +94,7 @@ export function DecoratedVideoPlayer(
+
diff --git a/src/components/video/controls/ChromeCastControl.tsx b/src/components/video/controls/ChromeCastControl.tsx new file mode 100644 index 00000000..27cd6dc0 --- /dev/null +++ b/src/components/video/controls/ChromeCastControl.tsx @@ -0,0 +1,15 @@ +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "google-cast-launcher": React.DetailedHTMLProps< + React.HTMLAttributes, + HTMLElement + >; + } + } +} + +export function ChromeCastControl() { + return ; +} From fa9785bf69617736d64d112be2f2d5b482891bda Mon Sep 17 00:00:00 2001 From: Jip Fr Date: Sun, 22 Jan 2023 23:06:29 +0100 Subject: [PATCH 053/135] chore(superstream): improve import, move quality finding to its own function --- src/backend/index.ts | 2 +- .../superstream/{superstream.ts => index.ts} | 38 ++++++------------- 2 files changed, 13 insertions(+), 27 deletions(-) rename src/backend/providers/superstream/{superstream.ts => index.ts} (88%) diff --git a/src/backend/index.ts b/src/backend/index.ts index 7859f2cd..5e4f7989 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -3,7 +3,7 @@ import { initializeScraperStore } from "./helpers/register"; // providers import "./providers/gdriveplayer"; import "./providers/flixhq"; -import "./providers/superstream/superstream"; +import "./providers/superstream"; // embeds // -- nothing here yet diff --git a/src/backend/providers/superstream/superstream.ts b/src/backend/providers/superstream/index.ts similarity index 88% rename from src/backend/providers/superstream/superstream.ts rename to src/backend/providers/superstream/index.ts index cb0c3928..aaf460b0 100644 --- a/src/backend/providers/superstream/superstream.ts +++ b/src/backend/providers/superstream/index.ts @@ -98,6 +98,16 @@ const get = (data: object, altApi = false) => { }); }; +// Find best resolution +const getBestQuality = (list: any[]) => { + return ( + list.find((quality: any) => quality.quality === "1080p" && quality.path) ?? + list.find((quality: any) => quality.quality === "720p" && quality.path) ?? + list.find((quality: any) => quality.quality === "480p" && quality.path) ?? + list.find((quality: any) => quality.quality === "360p" && quality.path) + ); +}; + registerProvider({ id: "superstream", displayName: "Superstream", @@ -138,19 +148,7 @@ registerProvider({ const mediaRes = (await get(apiQuery)).data; progress(50); - const hdQuality = - mediaRes.list.find( - (quality: any) => quality.quality === "1080p" && quality.path - ) ?? - mediaRes.list.find( - (quality: any) => quality.quality === "720p" && quality.path - ) ?? - mediaRes.list.find( - (quality: any) => quality.quality === "480p" && quality.path - ) ?? - mediaRes.list.find( - (quality: any) => quality.quality === "360p" && quality.path - ); + const hdQuality = getBestQuality(mediaRes.list); if (!hdQuality) throw new Error("No quality could be found."); @@ -210,19 +208,7 @@ registerProvider({ const mediaRes = (await get(apiQuery)).data; progress(66); - const hdQuality = - mediaRes.list.find( - (quality: any) => quality.quality === "1080p" && quality.path - ) ?? - mediaRes.list.find( - (quality: any) => quality.quality === "720p" && quality.path - ) ?? - mediaRes.list.find( - (quality: any) => quality.quality === "480p" && quality.path - ) ?? - mediaRes.list.find( - (quality: any) => quality.quality === "360p" && quality.path - ); + const hdQuality = getBestQuality(mediaRes.list); if (!hdQuality) throw new Error("No quality could be found."); From f339a7156a73135d2084d633e92cadd543b06a33 Mon Sep 17 00:00:00 2001 From: Jip Fr Date: Sun, 22 Jan 2023 23:07:26 +0100 Subject: [PATCH 054/135] chore: remove log --- src/backend/providers/superstream/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/backend/providers/superstream/index.ts b/src/backend/providers/superstream/index.ts index aaf460b0..d54b7984 100644 --- a/src/backend/providers/superstream/index.ts +++ b/src/backend/providers/superstream/index.ts @@ -152,8 +152,6 @@ registerProvider({ if (!hdQuality) throw new Error("No quality could be found."); - console.log(hdQuality); - // const subtitleApiQuery = { // fid: hdQuality.fid, // uid: "", From 62220532d787fc1acd246a99352bd1d6cdb3c5a4 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sun, 22 Jan 2023 23:11:18 +0100 Subject: [PATCH 055/135] fix linting --- src/backend/metadata/getmeta.ts | 1 - src/backend/providers/gomostream.ts | 98 ------------------- .../providers/superstream/superstream.ts | 4 - 3 files changed, 103 deletions(-) delete mode 100644 src/backend/providers/gomostream.ts diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 7fb14ae6..cb622e3b 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -54,7 +54,6 @@ export async function getMetaFromId( throw err; } - console.log(data.external_ids); const imdbId = data.external_ids.find( (v) => v.provider === "imdb_latest" )?.external_id; diff --git a/src/backend/providers/gomostream.ts b/src/backend/providers/gomostream.ts deleted file mode 100644 index 275e6099..00000000 --- a/src/backend/providers/gomostream.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { unpack } from "unpacker"; -import { proxiedFetch } from "../helpers/fetch"; -import { registerProvider } from "../helpers/register"; -import { MWStreamQuality, MWStreamType } from "../helpers/streams"; -import { MWMediaType } from "../metadata/types"; -import json5 from "json5"; - -const gomoBase = "https://gomo.to/"; - -registerProvider({ - id: "gomostream", - displayName: "gomostream", - rank: 999, - type: [MWMediaType.MOVIE], - - async scrape({ media, progress }) { - // get movie from gomostream - const contentResult = await proxiedFetch( - `/${media.meta.type}/${media.imdbId}`, - { - baseURL: gomoBase, - } - ); - - // movie doesn't exist - if ( - contentResult === "Movie not available." || - contentResult === "Episode not available." - ) - throw new Error("No watchable item found."); - - // decode stream - progress(25); - - const tc = contentResult.match(/var tc = '(.+)';/)?.[1] || ""; - const _token = contentResult.match(/"_token": "(.+)",/)?.[1] || ""; - - const fd = new FormData(); - fd.append("tokenCode", tc); - fd.append("_token", _token); - - const src = await proxiedFetch(`/decoding_v3.php`, { - baseURL: gomoBase, - method: "POST", - body: fd, - headers: { - "x-token": `${tc.slice(5, 13).split("").reverse().join("")}13574199`, - }, - parseResponse: JSON.parse, - }); - - // TODO should check all embed urls in future - const embedUrl = src.filter((url: string) => url.includes("gomo.to"))[1]; - - // get stream info - progress(50); - - const streamRes = await proxiedFetch(embedUrl); - - const streamResDom = new DOMParser().parseFromString( - streamRes, - "text/html" - ); - if (streamResDom.body.innerText === "File was deleted") - throw new Error("No watchable item found."); - - const script = Array.from(streamResDom.querySelectorAll("script")).find( - (s: HTMLScriptElement) => - s.innerHTML.includes("eval(function(p,a,c,k,e,d") - )?.innerHTML; - if (!script) throw new Error("Could not get packed data"); - - // unpack data - progress(75); - - const unpacked = unpack(script); - const rawSources = /sources:(\[.*?\])/.exec(unpacked); - if (!rawSources) throw new Error("Could not get stream URL"); - - const sources = json5.parse(rawSources[1]); - const streamUrl = sources[0].file; - - console.log(sources); - - const streamType = streamUrl.split(".").at(-1); - if (streamType !== "mp4" && streamType !== "m3u8") - throw new Error("Unsupported stream type"); - - return { - embeds: [], - stream: { - quality: streamType, - streamUrl: streamUrl, - type: streamType, - }, - }; - }, -}); diff --git a/src/backend/providers/superstream/superstream.ts b/src/backend/providers/superstream/superstream.ts index cb0c3928..7cdcc851 100644 --- a/src/backend/providers/superstream/superstream.ts +++ b/src/backend/providers/superstream/superstream.ts @@ -1,13 +1,11 @@ import { registerProvider } from "@/backend/helpers/register"; import { MWMediaType } from "@/backend/metadata/types"; -import { conf } from "@/setup/config"; import { customAlphabet } from "nanoid"; // import toWebVTT from "srt-webvtt"; import CryptoJS from "crypto-js"; import { proxiedFetch } from "@/backend/helpers/fetch"; import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams"; -import { MetadataSchema } from "hls.js"; const nanoid = customAlphabet("0123456789abcdef", 32); @@ -154,8 +152,6 @@ registerProvider({ if (!hdQuality) throw new Error("No quality could be found."); - console.log(hdQuality); - // const subtitleApiQuery = { // fid: hdQuality.fid, // uid: "", From 1f7e8abda5108da224ce53632e64956976663426 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sun, 22 Jan 2023 23:11:43 +0100 Subject: [PATCH 056/135] remove gomostream --- src/backend/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/backend/index.ts b/src/backend/index.ts index 61386d2a..5e4f7989 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -4,7 +4,6 @@ import { initializeScraperStore } from "./helpers/register"; import "./providers/gdriveplayer"; import "./providers/flixhq"; import "./providers/superstream"; -import "./providers/gomostream"; // embeds // -- nothing here yet From b8e49850f41e05b558bcd256a634940e6eed3220 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Mon, 23 Jan 2023 01:55:57 +0100 Subject: [PATCH 057/135] episode selection Co-authored-by: James Hawkins Co-authored-by: Jip Frijlink --- .eslintrc.js | 21 +- src/components/Icon.tsx | 4 + src/components/video/DecoratedVideoPlayer.tsx | 2 + .../video/controls/SeriesSelectionControl.tsx | 208 ++++++++++++++++++ src/components/video/controls/ShowControl.tsx | 37 +++- .../video/controls/ShowTitleControl.tsx | 23 +- src/components/video/hooks/controlVideo.ts | 24 ++ src/components/video/hooks/useVideoPlayer.ts | 6 + src/views/media/MediaView.tsx | 47 +++- 9 files changed, 343 insertions(+), 29 deletions(-) create mode 100644 src/components/video/controls/SeriesSelectionControl.tsx diff --git a/.eslintrc.js b/.eslintrc.js index 7710c4fd..5feb5ad4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,25 +8,25 @@ const a11yOff = Object.keys(require("eslint-plugin-jsx-a11y").rules).reduce( module.exports = { env: { - browser: true, + browser: true }, extends: [ "airbnb", "airbnb/hooks", "plugin:@typescript-eslint/recommended", "prettier", - "plugin:prettier/recommended", + "plugin:prettier/recommended" ], ignorePatterns: ["public/*", "dist/*", "/*.js", "/*.ts"], parser: "@typescript-eslint/parser", parserOptions: { project: "./tsconfig.json", - tsconfigRootDir: "./", + tsconfigRootDir: "./" }, settings: { "import/resolver": { - typescript: {}, - }, + typescript: {} + } }, plugins: ["@typescript-eslint", "import"], rules: { @@ -48,18 +48,19 @@ module.exports = { "no-continue": "off", "no-eval": "off", "no-await-in-loop": "off", + "no-nested-ternary": "off", "react/jsx-filename-extension": [ "error", - { extensions: [".js", ".tsx", ".jsx"] }, + { extensions: [".js", ".tsx", ".jsx"] } ], "import/extensions": [ "error", "ignorePackages", { ts: "never", - tsx: "never", - }, + tsx: "never" + } ], - ...a11yOff, - }, + ...a11yOff + } }; diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 54287d0c..92455784 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -10,6 +10,7 @@ export enum Icons { ARROW_RIGHT = "arrowRight", CHEVRON_DOWN = "chevronDown", CHEVRON_RIGHT = "chevronRight", + CHEVRON_LEFT = "chevronLeft", CLAPPER_BOARD = "clapperBoard", FILM = "film", DRAGON = "dragon", @@ -26,6 +27,7 @@ export enum Icons { X = "x", EDIT = "edit", AIRPLAY = "airplay", + EPISODES = "episodes", } export interface IconProps { @@ -41,6 +43,7 @@ const iconList: Record = { arrowLeft: ``, chevronDown: ``, chevronRight: ``, + chevronLeft: ``, clapperBoard: ``, film: ``, dragon: ``, @@ -59,6 +62,7 @@ const iconList: Record = { edit: ``, bookmark_outline: ``, airplay: ``, + episodes: ``, }; export const Icon = memo((props: IconProps) => { diff --git a/src/components/video/DecoratedVideoPlayer.tsx b/src/components/video/DecoratedVideoPlayer.tsx index 702496d8..a7b7601d 100644 --- a/src/components/video/DecoratedVideoPlayer.tsx +++ b/src/components/video/DecoratedVideoPlayer.tsx @@ -9,6 +9,7 @@ import { LoadingControl } from "./controls/LoadingControl"; import { MiddlePauseControl } from "./controls/MiddlePauseControl"; import { PauseControl } from "./controls/PauseControl"; import { ProgressControl } from "./controls/ProgressControl"; +import { SeriesSelectionControl } from "./controls/SeriesSelectionControl"; import { ShowTitleControl } from "./controls/ShowTitleControl"; import { TimeControl } from "./controls/TimeControl"; import { VolumeControl } from "./controls/VolumeControl"; @@ -93,6 +94,7 @@ export function DecoratedVideoPlayer(
+ diff --git a/src/components/video/controls/SeriesSelectionControl.tsx b/src/components/video/controls/SeriesSelectionControl.tsx new file mode 100644 index 00000000..efb3e675 --- /dev/null +++ b/src/components/video/controls/SeriesSelectionControl.tsx @@ -0,0 +1,208 @@ +import React, { useCallback, useMemo, useState } from "react"; +import { useParams } from "react-router-dom"; +import { Icon, Icons } from "@/components/Icon"; +import { useLoading } from "@/hooks/useLoading"; +import { MWMediaType, MWSeasonWithEpisodeMeta } from "@/backend/metadata/types"; +import { getMetaFromId } from "@/backend/metadata/getmeta"; +import { decodeJWId } from "@/backend/metadata/justwatch"; +import { Loading } from "@/components/layout/Loading"; +import { IconPatch } from "@/components/buttons/IconPatch"; +import { useVideoPlayerState } from "../VideoContext"; +import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton"; + +interface Props { + className?: string; +} + +export function PopupThingy(props: { + children?: React.ReactNode; + containerClassName?: string; +}) { + return ( +
+
+
+ {props.children} +
+
+
+ ); +} + +function PopupSection(props: { + children?: React.ReactNode; + className?: string; +}) { + return ( +
+ {props.children} +
+ ); +} + +function PopupEpisodeSelect() { + const params = useParams<{ + media: string; + }>(); + const { videoState } = useVideoPlayerState(); + const [isPickingSeason, setIsPickingSeason] = useState(false); + const { current, seasons } = videoState.seasonData; + const [currentVisibleSeason, setCurrentVisibleSeason] = useState<{ + seasonId: string; + season?: MWSeasonWithEpisodeMeta; + } | null>(null); + const [reqSeasonMeta, loading, error] = useLoading( + (id: string, seasonId: string) => { + return getMetaFromId(MWMediaType.SERIES, id, seasonId); + } + ); + const requestSeason = useCallback( + (sId: string) => { + setCurrentVisibleSeason({ + seasonId: sId, + season: undefined, + }); + setIsPickingSeason(false); + reqSeasonMeta(decodeJWId(params.media)?.id as string, sId).then((v) => { + if (v?.meta.type !== MWMediaType.SERIES) return; + setCurrentVisibleSeason({ + seasonId: sId, + season: v?.meta.seasonData, + }); + }); + }, + [reqSeasonMeta, params.media] + ); + + const currentSeasonId = currentVisibleSeason?.seasonId ?? current?.seasonId; + + const setCurrent = useCallback( + (seasonId: string, episodeId: string) => { + videoState.setCurrentEpisode(seasonId, episodeId); + }, + [videoState] + ); + + const currentSeasonInfo = useMemo(() => { + return seasons?.find((season) => season.id === currentSeasonId); + }, [seasons, currentSeasonId]); + + const currentSeasonEpisodes = useMemo(() => { + if (currentVisibleSeason?.season) { + return currentVisibleSeason?.season?.episodes; + } + return videoState?.seasonData.seasons?.find?.( + (season) => season && season.id === currentSeasonId + )?.episodes; + }, [videoState, currentSeasonId, currentVisibleSeason]); + + const toggleIsPickingSeason = () => { + setIsPickingSeason(!isPickingSeason); + }; + + const setSeason = (id: string) => { + requestSeason(id); + setCurrentVisibleSeason({ seasonId: id }); + }; + + if (isPickingSeason) + return ( + <> + + Pick a season + + +
+ {currentSeasonInfo + ? videoState?.seasonData?.seasons?.map?.((season) => ( +
setSeason(season.id)} + > + {season.title} +
+ )) + : "No season"} +
+
+ + ); + + return ( + <> + + + {currentSeasonInfo?.title || ""} + + + {loading ? ( +
+ +
+ ) : error ? ( +
+
+ +

+ Something went wrong loading the episodes for{" "} + {currentSeasonInfo?.title?.toLowerCase()} +

+
+
+ ) : ( +
+ {currentSeasonEpisodes && currentSeasonInfo + ? currentSeasonEpisodes.map((e) => ( +
setCurrent(currentSeasonInfo.id, e.id)} + key={e.id} + > + {e.number}. {e.title} +
+ )) + : "No episodes"} +
+ )} +
+ + ); +} + +export function SeriesSelectionControl(props: Props) { + const { videoState } = useVideoPlayerState(); + const [open, setOpen] = useState(false); + + if (!videoState.seasonData.isSeries) return null; + + return ( +
+
+ {open ? ( + + + + ) : null} + setOpen((s) => !s)} + /> +
+
+ ); +} diff --git a/src/components/video/controls/ShowControl.tsx b/src/components/video/controls/ShowControl.tsx index 5dafdf45..162870b7 100644 --- a/src/components/video/controls/ShowControl.tsx +++ b/src/components/video/controls/ShowControl.tsx @@ -1,4 +1,9 @@ +import { + MWSeasonMeta, + MWSeasonWithEpisodeMeta, +} from "@/backend/metadata/types"; import { useEffect, useRef } from "react"; +import { PlayerContext } from "../hooks/useVideoPlayer"; import { useVideoPlayerState } from "../VideoContext"; interface ShowControlProps { @@ -6,9 +11,28 @@ interface ShowControlProps { episodeId: string; seasonId: string; }; + seasons: MWSeasonMeta[]; + seasonData: MWSeasonWithEpisodeMeta; onSelect?: (state: { episodeId?: string; seasonId?: string }) => void; } +function setVideoShowState(videoState: PlayerContext, props: ShowControlProps) { + const seasonsWithEpisodes = props.seasons.map((v) => { + if (v.id === props.seasonData.id) + return { + ...v, + episodes: props.seasonData.episodes, + }; + return v; + }); + + videoState.setShowData({ + current: props.series, + isSeries: !!props.series, + seasons: seasonsWithEpisodes, + }); +} + export function ShowControl(props: ShowControlProps) { const { videoState } = useVideoPlayerState(); const lastState = useRef<{ @@ -19,14 +43,13 @@ export function ShowControl(props: ShowControlProps) { seasonId: props.series?.seasonId, }); + const hasInitialized = useRef(false); useEffect(() => { - videoState.setShowData({ - current: props.series, - isSeries: !!props.series, - }); - // we only want it to run when props change, not when videoState changes - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props]); + if (hasInitialized.current) return; + if (!videoState.hasInitialized) return; + setVideoShowState(videoState, props); + hasInitialized.current = true; + }, [props, videoState]); useEffect(() => { const currentState = { diff --git a/src/components/video/controls/ShowTitleControl.tsx b/src/components/video/controls/ShowTitleControl.tsx index 06cc7f7b..2cb08420 100644 --- a/src/components/video/controls/ShowTitleControl.tsx +++ b/src/components/video/controls/ShowTitleControl.tsx @@ -1,19 +1,30 @@ +import { useMemo } from "react"; import { useVideoPlayerState } from "../VideoContext"; export function ShowTitleControl() { const { videoState } = useVideoPlayerState(); + const { current, seasons } = videoState.seasonData; + + const currentSeasonInfo = useMemo(() => { + return seasons?.find((season) => season.id === current?.seasonId); + }, [seasons, current]); + + const currentEpisodeInfo = useMemo(() => { + return currentSeasonInfo?.episodes?.find( + (episode) => episode.id === current?.episodeId + ); + }, [currentSeasonInfo, current]); + if (!videoState.seasonData.isSeries) return null; - if (!videoState.seasonData.title || !videoState.seasonData.current) - return null; + if (!videoState.seasonData.current) return null; - const cur = videoState.seasonData.current; - const selectedText = `S${cur.season} E${cur.episode}`; + const selectedText = `S${currentSeasonInfo?.number} E${currentEpisodeInfo?.number}`; return ( -

+

{selectedText} - {videoState.seasonData.title} + {currentEpisodeInfo?.title}

); } diff --git a/src/components/video/hooks/controlVideo.ts b/src/components/video/hooks/controlVideo.ts index 531e94c3..57d7c130 100644 --- a/src/components/video/hooks/controlVideo.ts +++ b/src/components/video/hooks/controlVideo.ts @@ -17,6 +17,16 @@ interface ShowData { seasonId: string; }; isSeries: boolean; + seasons?: { + id: string; + number: number; + title: string; + episodes?: { + id: string; + number: number; + title: string; + }[]; + }[]; } export interface PlayerControls { @@ -30,6 +40,7 @@ export interface PlayerControls { setLeftControlsHover(hovering: boolean): void; initPlayer(sourceUrl: string, sourceType: MWStreamType): void; setShowData(data: ShowData): void; + setCurrentEpisode(sId: string, eId: string): void; startAirplay(): void; } @@ -45,6 +56,7 @@ export const initialControls: PlayerControls = { initPlayer: () => null, setShowData: () => null, startAirplay: () => null, + setCurrentEpisode: () => null, }; export function populateControls( @@ -120,6 +132,18 @@ export function populateControls( setShowData(data) { update((s) => ({ ...s, seasonData: data })); }, + setCurrentEpisode(sId: string, eId: string) { + update((s) => ({ + ...s, + seasonData: { + ...s.seasonData, + current: { + seasonId: sId, + episodeId: eId, + }, + }, + })); + }, startAirplay() { const videoPlayer = player as any; if (videoPlayer.webkitShowPlaybackTargetPicker) diff --git a/src/components/video/hooks/useVideoPlayer.ts b/src/components/video/hooks/useVideoPlayer.ts index dfb929c7..b1099d08 100644 --- a/src/components/video/hooks/useVideoPlayer.ts +++ b/src/components/video/hooks/useVideoPlayer.ts @@ -29,6 +29,12 @@ export type PlayerState = { episodeId: string; seasonId: string; }; + seasons?: { + id: string; + number: number; + title: string; + episodes?: { id: string; number: number; title: string }[]; + }[]; }; error: null | { name: string; diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index d8014674..2ff5305b 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -93,6 +93,7 @@ interface MediaViewPlayerProps { meta: DetailedMeta; stream: MWStream; selected: SelectedMediaData; + onChangeStream: (sId: string, eId: string) => void; } export function MediaViewPlayer(props: MediaViewPlayerProps) { const goBack = useGoBack(); @@ -120,13 +121,20 @@ export function MediaViewPlayer(props: MediaViewPlayerProps) { startAt={firstStartTime.current} onProgress={updateProgress} /> - {props.selected.type === MWMediaType.SERIES ? ( + {props.selected.type === MWMediaType.SERIES && + props.meta.meta.type === MWMediaType.SERIES ? ( console.log("selected stuff", d)} + onSelect={(d) => + d.seasonId && + d.episodeId && + props.onChangeStream?.(d.seasonId, d.episodeId) + } + seasonData={props.meta.meta.seasonData} + seasons={props.meta.meta.seasons} /> ) : null} @@ -154,9 +162,25 @@ export function MediaView() { ); const [stream, setStream] = useState(null); + const lastSearchValue = useRef<(string | undefined)[] | null>(null); useEffect(() => { + const newValue = [params.media, params.season, params.episode]; + const lastVal = lastSearchValue.current; + + const isSame = + lastVal?.[0] === newValue[0] && + (lastVal?.[1] === newValue[1] || !lastVal?.[1]) && + (lastVal?.[2] === newValue[2] || !lastVal?.[2]); + + lastSearchValue.current = newValue; + if (isSame && lastVal !== null) return; + + setMeta(null); + setStream(null); + setSelected(null); exec(params.media, params.season).then((v) => { setMeta(v ?? null); + setStream(null); if (v) { if (v.meta.type !== MWMediaType.SERIES) { setSelected({ @@ -181,9 +205,7 @@ export function MediaView() { } } else setSelected(null); }); - // dont rerender when params changes - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [exec, history]); + }, [exec, history, params]); if (loading) return ; if (error) return ; @@ -206,5 +228,18 @@ export function MediaView() { ); // show stream once we have a stream - return ; + return ( + { + history.replace( + `/media/${encodeURIComponent(params.media)}/${encodeURIComponent( + sId + )}/${encodeURIComponent(eId)}` + ); + }} + /> + ); } From b2748f7390dceffec004c6d2a13bf47bdede6e8d Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Mon, 23 Jan 2023 02:01:59 +0100 Subject: [PATCH 058/135] fix a color --- src/components/video/controls/SeriesSelectionControl.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/video/controls/SeriesSelectionControl.tsx b/src/components/video/controls/SeriesSelectionControl.tsx index efb3e675..63a7b9d8 100644 --- a/src/components/video/controls/SeriesSelectionControl.tsx +++ b/src/components/video/controls/SeriesSelectionControl.tsx @@ -131,7 +131,7 @@ function PopupEpisodeSelect() { return ( <> - +
diff --git a/src/components/video/controls/BackdropControl.tsx b/src/components/video/controls/BackdropControl.tsx index 2fba50f3..af9a9967 100644 --- a/src/components/video/controls/BackdropControl.tsx +++ b/src/components/video/controls/BackdropControl.tsx @@ -29,6 +29,8 @@ export function BackdropControl(props: BackdropControlProps) { (e: React.MouseEvent) => { if (!clickareaRef.current || clickareaRef.current !== e.target) return; + if (videoState.popout !== null) return; + if (videoState.isPlaying) videoState.pause(); else videoState.play(); }, @@ -49,6 +51,7 @@ export function BackdropControl(props: BackdropControlProps) { const currentValue = moved || videoState.isPaused; if (currentValue !== lastBackdropValue.current) { lastBackdropValue.current = currentValue; + if (!currentValue) videoState.closePopout(); props.onBackdropChange?.(currentValue); } }, [videoState, moved, props]); diff --git a/src/components/video/controls/ProgressControl.tsx b/src/components/video/controls/ProgressControl.tsx index eaeed9ee..0f1c496b 100644 --- a/src/components/video/controls/ProgressControl.tsx +++ b/src/components/video/controls/ProgressControl.tsx @@ -45,6 +45,7 @@ export function ProgressControl() { ref={ref} className="-my-3 flex h-8 items-center" onMouseDown={dragMouseDown} + onTouchStart={dragMouseDown} >
-
-
- {props.children} -
-
-
- ); -} - function PopupSection(props: { children?: React.ReactNode; className?: string; @@ -185,22 +171,22 @@ function PopupEpisodeSelect() { export function SeriesSelectionControl(props: Props) { const { videoState } = useVideoPlayerState(); - const [open, setOpen] = useState(false); if (!videoState.seasonData.isSeries) return null; return (
- {open ? ( - - - - ) : null} + + + setOpen((s) => !s)} + onClick={() => videoState.openPopout("episodes")} />
diff --git a/src/components/video/controls/SkipTime.tsx b/src/components/video/controls/SkipTime.tsx new file mode 100644 index 00000000..59c2923f --- /dev/null +++ b/src/components/video/controls/SkipTime.tsx @@ -0,0 +1,46 @@ +import { useVideoPlayerState } from "../VideoContext"; + +function durationExceedsHour(secs: number): boolean { + return secs > 60 * 60; +} + +function formatSeconds(secs: number, showHours = false): string { + if (Number.isNaN(secs)) { + if (showHours) return "0:00:00"; + return "0:00"; + } + + let time = secs; + const seconds = Math.floor(time % 60); + + time /= 60; + const minutes = Math.floor(time % 60); + + time /= 60; + const hours = Math.floor(time); + + const paddedSecs = seconds.toString().padStart(2, "0"); + const paddedMins = minutes.toString().padStart(2, "0"); + + if (!showHours) return [minutes, paddedSecs].join(":"); + return [hours, paddedMins, paddedSecs].join(":"); +} + +interface Props { + className?: string; +} + +export function SkipTime(props: Props) { + const { videoState } = useVideoPlayerState(); + const hasHours = durationExceedsHour(videoState.duration); + const time = formatSeconds(videoState.time, hasHours); + const duration = formatSeconds(videoState.duration, hasHours); + + return ( +
+

+ {time} / {duration} +

+
+ ); +} diff --git a/src/components/video/controls/TimeControl.tsx b/src/components/video/controls/TimeControl.tsx index 1adea300..1012b560 100644 --- a/src/components/video/controls/TimeControl.tsx +++ b/src/components/video/controls/TimeControl.tsx @@ -1,3 +1,5 @@ +import { Icon, Icons } from "@/components/Icon"; +import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton"; import { useVideoPlayerState } from "../VideoContext"; function durationExceedsHour(secs: number): boolean { @@ -32,14 +34,26 @@ interface Props { export function TimeControl(props: Props) { const { videoState } = useVideoPlayerState(); - const hasHours = durationExceedsHour(videoState.duration); - const time = formatSeconds(videoState.time, hasHours); - const duration = formatSeconds(videoState.duration, hasHours); + + const skipForward = () => { + videoState.setTime(videoState.time + 10); + }; + + const skipBackward = () => { + videoState.setTime(videoState.time - 10); + }; return (
-

- {time} / {duration} +

+ +

); diff --git a/src/components/video/hooks/controlVideo.ts b/src/components/video/hooks/controlVideo.ts index 57d7c130..7a34543c 100644 --- a/src/components/video/hooks/controlVideo.ts +++ b/src/components/video/hooks/controlVideo.ts @@ -42,6 +42,8 @@ export interface PlayerControls { setShowData(data: ShowData): void; setCurrentEpisode(sId: string, eId: string): void; startAirplay(): void; + openPopout(id: string): void; + closePopout(): void; } export const initialControls: PlayerControls = { @@ -57,6 +59,8 @@ export const initialControls: PlayerControls = { setShowData: () => null, startAirplay: () => null, setCurrentEpisode: () => null, + openPopout: () => null, + closePopout: () => null, }; export function populateControls( @@ -129,6 +133,12 @@ export function populateControls( setLeftControlsHover(hovering) { update((s) => ({ ...s, leftControlHovering: hovering })); }, + openPopout(id: string) { + update((s) => ({ ...s, popout: id })); + }, + closePopout() { + update((s) => ({ ...s, popout: null })); + }, setShowData(data) { update((s) => ({ ...s, seasonData: data })); }, diff --git a/src/components/video/hooks/useVideoPlayer.ts b/src/components/video/hooks/useVideoPlayer.ts index b1099d08..5ba7dbac 100644 --- a/src/components/video/hooks/useVideoPlayer.ts +++ b/src/components/video/hooks/useVideoPlayer.ts @@ -23,6 +23,7 @@ export type PlayerState = { hasInitialized: boolean; leftControlHovering: boolean; hasPlayedOnce: boolean; + popout: string | null; seasonData: { isSeries: boolean; current?: { @@ -61,6 +62,7 @@ export const initialPlayerState: PlayerContext = { leftControlHovering: false, hasPlayedOnce: false, error: null, + popout: null, seasonData: { isSeries: false, }, diff --git a/src/components/video/parts/VideoPopout.tsx b/src/components/video/parts/VideoPopout.tsx new file mode 100644 index 00000000..6ad49302 --- /dev/null +++ b/src/components/video/parts/VideoPopout.tsx @@ -0,0 +1,54 @@ +import { useEffect, useRef } from "react"; +import { useVideoPlayerState } from "../VideoContext"; + +interface Props { + children?: React.ReactNode; + id?: string; + className?: string; +} + +export function VideoPopout(props: Props) { + const { videoState } = useVideoPlayerState(); + const popoutRef = useRef(null); + const isOpen = videoState.popout === props.id; + + useEffect(() => { + if (!isOpen) return; + const popoutEl = popoutRef.current; + let hasTriggered = false; + function windowClick() { + setTimeout(() => { + if (hasTriggered) return; + videoState.closePopout(); + hasTriggered = false; + }, 10); + } + function popoutClick() { + hasTriggered = true; + setTimeout(() => { + hasTriggered = false; + }, 100); + } + window.addEventListener("click", windowClick); + popoutEl?.addEventListener("click", popoutClick); + return () => { + window.removeEventListener("click", windowClick); + popoutEl?.removeEventListener("click", popoutClick); + }; + }, [isOpen, videoState]); + + if (!isOpen) return null; + + return ( +
+
+
+ {props.children} +
+
+
+ ); +} diff --git a/src/hooks/useProgressBar.ts b/src/hooks/useProgressBar.ts index 7bb7070b..ad069709 100644 --- a/src/hooks/useProgressBar.ts +++ b/src/hooks/useProgressBar.ts @@ -1,5 +1,9 @@ import React, { RefObject, useCallback, useEffect, useState } from "react"; +type ActivityEvent = + | React.MouseEvent + | React.TouchEvent; + export function makePercentageString(num: number) { return `${num.toFixed(2)}%`; } @@ -8,6 +12,18 @@ export function makePercentage(num: number) { return Number(Math.max(0, Math.min(num, 100)).toFixed(2)); } +function isClickEvent( + evt: React.MouseEvent | React.TouchEvent +): evt is React.MouseEvent { + return evt.type === "mousedown"; +} + +const getEventX = ( + evt: React.MouseEvent | React.TouchEvent +) => { + return isClickEvent(evt) ? evt.pageX : evt.touches[0].pageX; +}; + export function useProgressBar( barRef: RefObject, commit: (percentage: number) => void, @@ -25,19 +41,20 @@ export function useProgressBar( if (commitImmediately) commit(pos); } - function mouseUp(ev: MouseEvent) { + function mouseUp(ev: MouseEvent | TouchEvent) { if (!mouseDown) return; setMouseDown(false); document.body.removeAttribute("data-no-select"); if (!barRef.current) return; const rect = barRef.current.getBoundingClientRect(); - const pos = (ev.pageX - rect.left) / barRef.current.offsetWidth; + const pos = (getEventX(ev) - rect.left) / barRef.current.offsetWidth; commit(pos); } document.addEventListener("mousemove", mouseMove); document.addEventListener("mouseup", mouseUp); + document.addEventListener("touchend", mouseUp); return () => { document.removeEventListener("mousemove", mouseMove); @@ -46,13 +63,14 @@ export function useProgressBar( }, [mouseDown, barRef, commit, commitImmediately]); const dragMouseDown = useCallback( - (ev: React.MouseEvent) => { + (ev: React.MouseEvent | React.TouchEvent) => { setMouseDown(true); document.body.setAttribute("data-no-select", "true"); if (!barRef.current) return; const rect = barRef.current.getBoundingClientRect(); - const pos = ((ev.pageX - rect.left) / barRef.current.offsetWidth) * 100; + const pos = + ((getEventX(ev) - rect.left) / barRef.current.offsetWidth) * 100; setProgress(pos); }, [setProgress, barRef] From a07741776192c7c5b6411996095a595dd43a5794 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Mon, 23 Jan 2023 23:01:08 +0100 Subject: [PATCH 060/135] mobile safe video sizes --- src/components/video/DecoratedVideoPlayer.tsx | 2 +- src/components/video/controls/TimeControl.tsx | 28 +------------------ src/hooks/useProgressBar.ts | 27 ++++++++++-------- src/setup/index.css | 5 +++- src/views/media/MediaView.tsx | 4 +-- 5 files changed, 22 insertions(+), 44 deletions(-) diff --git a/src/components/video/DecoratedVideoPlayer.tsx b/src/components/video/DecoratedVideoPlayer.tsx index b86ec2ff..6fd9e498 100644 --- a/src/components/video/DecoratedVideoPlayer.tsx +++ b/src/components/video/DecoratedVideoPlayer.tsx @@ -90,7 +90,7 @@ export function DecoratedVideoPlayer( >
diff --git a/src/components/video/controls/TimeControl.tsx b/src/components/video/controls/TimeControl.tsx index 1012b560..fdc67064 100644 --- a/src/components/video/controls/TimeControl.tsx +++ b/src/components/video/controls/TimeControl.tsx @@ -1,33 +1,7 @@ -import { Icon, Icons } from "@/components/Icon"; +import { Icons } from "@/components/Icon"; import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton"; import { useVideoPlayerState } from "../VideoContext"; -function durationExceedsHour(secs: number): boolean { - return secs > 60 * 60; -} - -function formatSeconds(secs: number, showHours = false): string { - if (Number.isNaN(secs)) { - if (showHours) return "0:00:00"; - return "0:00"; - } - - let time = secs; - const seconds = Math.floor(time % 60); - - time /= 60; - const minutes = Math.floor(time % 60); - - time /= 60; - const hours = Math.floor(time); - - const paddedSecs = seconds.toString().padStart(2, "0"); - const paddedMins = minutes.toString().padStart(2, "0"); - - if (!showHours) return [minutes, paddedSecs].join(":"); - return [hours, paddedMins, paddedSecs].join(":"); -} - interface Props { className?: string; } diff --git a/src/hooks/useProgressBar.ts b/src/hooks/useProgressBar.ts index ad069709..caaf2e89 100644 --- a/src/hooks/useProgressBar.ts +++ b/src/hooks/useProgressBar.ts @@ -2,7 +2,9 @@ import React, { RefObject, useCallback, useEffect, useState } from "react"; type ActivityEvent = | React.MouseEvent - | React.TouchEvent; + | React.TouchEvent + | MouseEvent + | TouchEvent; export function makePercentageString(num: number) { return `${num.toFixed(2)}%`; @@ -13,15 +15,13 @@ export function makePercentage(num: number) { } function isClickEvent( - evt: React.MouseEvent | React.TouchEvent -): evt is React.MouseEvent { - return evt.type === "mousedown"; + evt: ActivityEvent +): evt is React.MouseEvent | MouseEvent { + return evt.type === "mousedown" || evt.type === "mouseup"; } -const getEventX = ( - evt: React.MouseEvent | React.TouchEvent -) => { - return isClickEvent(evt) ? evt.pageX : evt.touches[0].pageX; +const getEventX = (evt: ActivityEvent) => { + return isClickEvent(evt) ? evt.pageX : evt.changedTouches[0].pageX; }; export function useProgressBar( @@ -33,15 +33,15 @@ export function useProgressBar( const [progress, setProgress] = useState(0); useEffect(() => { - function mouseMove(ev: MouseEvent) { + function mouseMove(ev: ActivityEvent) { if (!mouseDown || !barRef.current) return; const rect = barRef.current.getBoundingClientRect(); - const pos = (ev.pageX - rect.left) / barRef.current.offsetWidth; + const pos = (getEventX(ev) - rect.left) / barRef.current.offsetWidth; setProgress(pos * 100); if (commitImmediately) commit(pos); } - function mouseUp(ev: MouseEvent | TouchEvent) { + function mouseUp(ev: ActivityEvent) { if (!mouseDown) return; setMouseDown(false); document.body.removeAttribute("data-no-select"); @@ -53,17 +53,20 @@ export function useProgressBar( } document.addEventListener("mousemove", mouseMove); + document.addEventListener("touchmove", mouseMove); document.addEventListener("mouseup", mouseUp); document.addEventListener("touchend", mouseUp); return () => { document.removeEventListener("mousemove", mouseMove); + document.removeEventListener("touchmove", mouseMove); document.removeEventListener("mouseup", mouseUp); + document.removeEventListener("touchend", mouseUp); }; }, [mouseDown, barRef, commit, commitImmediately]); const dragMouseDown = useCallback( - (ev: React.MouseEvent | React.TouchEvent) => { + (ev: ActivityEvent) => { setMouseDown(true); document.body.setAttribute("data-no-select", "true"); diff --git a/src/setup/index.css b/src/setup/index.css index b8e12018..57c9a487 100644 --- a/src/setup/index.css +++ b/src/setup/index.css @@ -4,12 +4,15 @@ html, body { - @apply bg-denim-100 text-denim-700 font-open-sans min-h-screen overflow-x-hidden; + @apply bg-denim-100 text-denim-700 font-open-sans overflow-x-hidden; + min-height: 100vh; + min-height: 100dvh; } #root { padding: 0.05px; min-height: 100vh; + min-height: 100dvh; width: 100%; } diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index 2ff5305b..016c0521 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -105,10 +105,8 @@ export function MediaViewPlayer(props: MediaViewPlayerProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.stream]); - // TODO show episode title - return ( -
+
{props.meta.meta.title} From 177860aed4c36c679baabf514eaf039b6d6199b0 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Mon, 23 Jan 2023 23:51:40 +0100 Subject: [PATCH 061/135] series support for continue watching --- src/components/media/MediaCard.tsx | 8 +- src/components/media/WatchedMediaCard.tsx | 20 +++- src/index.tsx | 3 - src/setup/index.css | 4 + src/state/watched/context.tsx | 109 ++++++++++++++-------- src/views/media/MediaView.tsx | 8 +- 6 files changed, 104 insertions(+), 48 deletions(-) diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 66a53ed2..f2a652d3 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -11,6 +11,8 @@ export interface MediaCardProps { series?: { episode: number; season: number; + episodeId: string; + seasonId: string; }; percentage?: number; closable?: boolean; @@ -106,9 +108,13 @@ export function MediaCard(props: MediaCardProps) { const canLink = props.linkable && !props.closable; - const link = canLink + let link = canLink ? `/media/${encodeURIComponent(JWMediaToId(props.media))}` : "#"; + if (canLink && props.series) + link += `/${encodeURIComponent(props.series.seasonId)}/${encodeURIComponent( + props.series.episodeId + )}`; if (!props.linkable) return {content}; return {content}; diff --git a/src/components/media/WatchedMediaCard.tsx b/src/components/media/WatchedMediaCard.tsx index 62ffcc73..935ceb84 100644 --- a/src/components/media/WatchedMediaCard.tsx +++ b/src/components/media/WatchedMediaCard.tsx @@ -9,16 +9,32 @@ export interface WatchedMediaCardProps { onClose?: () => void; } +function formatSeries( + obj: + | { episodeId: string; seasonId: string; episode: number; season: number } + | undefined +) { + if (!obj) return undefined; + return { + season: obj.season, + episode: obj.episode, + episodeId: obj.episodeId, + seasonId: obj.seasonId, + }; +} + export function WatchedMediaCard(props: WatchedMediaCardProps) { const { watched } = useWatchedContext(); const watchedMedia = useMemo(() => { - return watched.items.find((v) => v.item.meta.id === props.media.id); + return watched.items + .sort((a, b) => b.watchedAt - a.watchedAt) + .find((v) => v.item.meta.id === props.media.id); }, [watched, props.media]); return ( diff --git a/src/setup/index.css b/src/setup/index.css index 57c9a487..8fda6ca6 100644 --- a/src/setup/index.css +++ b/src/setup/index.css @@ -9,6 +9,10 @@ body { min-height: 100dvh; } +html[data-full], html[data-full] body { + overscroll-behavior-y: none; +} + #root { padding: 0.05px; min-height: 100vh; diff --git a/src/state/watched/context.tsx b/src/state/watched/context.tsx index ae6421ae..e2a7a7f4 100644 --- a/src/state/watched/context.tsx +++ b/src/state/watched/context.tsx @@ -1,5 +1,5 @@ import { DetailedMeta } from "@/backend/metadata/getmeta"; -import { MWMediaMeta } from "@/backend/metadata/types"; +import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types"; import { createContext, ReactNode, @@ -33,6 +33,8 @@ function shouldSave(time: number, duration: number): boolean { interface MediaItem { meta: MWMediaMeta; series?: { + episodeId: string; + seasonId: string; episode: number; season: number; }; @@ -42,6 +44,7 @@ interface WatchedStoreItem { item: MediaItem; progress: number; percentage: number; + watchedAt: number; } export interface WatchedStoreData { @@ -65,6 +68,15 @@ const WatchedContext = createContext({ }); WatchedContext.displayName = "WatchedContext"; +function isSameEpisode(media: MediaItem, v: MediaItem) { + return ( + media.meta.id === v.meta.id && + (!media.series || + (media.series.seasonId === v.series?.seasonId && + media.series.episodeId === v.series?.episodeId)) + ); +} + export function WatchedContextProvider(props: { children: ReactNode }) { const watchedLocalstorage = VideoProgressStore.get(); const [watched, setWatchedReal] = useState( @@ -95,12 +107,9 @@ export function WatchedContextProvider(props: { children: ReactNode }) { }); }, updateProgress(media: MediaItem, progress: number, total: number): void { - // TODO series support setWatched((data: WatchedStoreData) => { const newData = { ...data }; - let item = newData.items.find( - (v) => v.item.meta.id === media.meta.id - ); + let item = newData.items.find((v) => isSameEpisode(media, v.item)); if (!item) { item = { item: { @@ -110,6 +119,7 @@ export function WatchedContextProvider(props: { children: ReactNode }) { }, progress: 0, percentage: 0, + watchedAt: Date.now(), }; newData.items.push(item); } @@ -120,7 +130,7 @@ export function WatchedContextProvider(props: { children: ReactNode }) { // remove item if shouldnt save if (!shouldSave(progress, total)) { newData.items = data.items.filter( - (v) => v.item.meta.id !== media.meta.id + (v) => !isSameEpisode(v.item, media) ); } @@ -130,34 +140,19 @@ export function WatchedContextProvider(props: { children: ReactNode }) { getFilteredWatched() { let filtered = watched.items; - // get highest episode number for every anime/season - const highestEpisode: Record = {}; - const highestWatchedItem: Record = {}; - filtered = filtered.filter((item) => { - if (item.item.series) { - const key = item.item.meta.id; - const current: [number, number] = [ - item.item.series.episode, - item.item.series.season, - ]; - let existing = highestEpisode[key]; - if (!existing) { - existing = current; - highestEpisode[key] = current; - highestWatchedItem[key] = item; - } - if ( - current[0] > existing[0] || - (current[0] === existing[0] && current[1] > existing[1]) - ) { - highestEpisode[key] = current; - highestWatchedItem[key] = item; - } - return false; - } - return true; - }); - return [...filtered, ...Object.values(highestWatchedItem)]; + // get most recently watched for every single item + const alreadyFoundMedia: string[] = []; + filtered = filtered + .sort((a, b) => { + return b.watchedAt - a.watchedAt; + }) + .filter((item) => { + const mediaId = item.item.meta.id; + if (alreadyFoundMedia.includes(mediaId)) return false; + alreadyFoundMedia.push(mediaId); + return true; + }); + return filtered; }, watched, }), @@ -175,26 +170,60 @@ export function useWatchedContext() { return useContext(WatchedContext); } -export function useWatchedItem(meta: DetailedMeta | null) { +function isSameEpisodeMeta( + media: MediaItem, + mediaTwo: DetailedMeta | null, + episodeId?: string +) { + if (mediaTwo?.meta.type === MWMediaType.SERIES && episodeId) { + return isSameEpisode(media, { + meta: mediaTwo.meta, + series: { + season: 0, + episode: 0, + episodeId, + seasonId: mediaTwo.meta.seasonData.id, + }, + }); + } + if (!mediaTwo) return () => false; + return isSameEpisode(media, { meta: mediaTwo.meta }); +} + +export function useWatchedItem(meta: DetailedMeta | null, episodeId?: string) { const { watched, updateProgress } = useContext(WatchedContext); const item = useMemo( - () => watched.items.find((v) => meta && v.item.meta.id === meta?.meta.id), - [watched, meta] + () => watched.items.find((v) => isSameEpisodeMeta(v.item, meta, episodeId)), + [watched, meta, episodeId] ); const lastCommitedTime = useRef([0, 0]); const callback = useCallback( (progress: number, total: number) => { - // TODO add series support const hasChanged = lastCommitedTime.current[0] !== progress || lastCommitedTime.current[1] !== total; if (meta && hasChanged) { lastCommitedTime.current = [progress, total]; - updateProgress({ meta: meta.meta }, progress, total); + const obj = { + meta: meta.meta, + series: + meta.meta.type === MWMediaType.SERIES && episodeId + ? { + seasonId: meta.meta.seasonData.id, + episodeId, + season: meta.meta.seasonData.number, + episode: + meta.meta.seasonData.episodes.find( + (ep) => ep.id === episodeId + )?.number || 0, + } + : undefined, + }; + updateProgress(obj, progress, total); } }, - [meta, updateProgress] + [meta, updateProgress, episodeId] ); return { updateProgress: callback, watchedItem: item }; diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index 016c0521..f63cc5ee 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -97,7 +97,10 @@ interface MediaViewPlayerProps { } export function MediaViewPlayer(props: MediaViewPlayerProps) { const goBack = useGoBack(); - const { updateProgress, watchedItem } = useWatchedItem(props.meta); + const { updateProgress, watchedItem } = useWatchedItem( + props.meta, + props.selected.episode + ); const firstStartTime = useRef(watchedItem?.progress); useEffect(() => { firstStartTime.current = watchedItem?.progress; @@ -106,9 +109,10 @@ export function MediaViewPlayer(props: MediaViewPlayerProps) { }, [props.stream]); return ( -
+
{props.meta.meta.title} + Date: Mon, 23 Jan 2023 23:58:40 +0100 Subject: [PATCH 062/135] add navigation todo --- src/components/video/parts/VideoPopout.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/video/parts/VideoPopout.tsx b/src/components/video/parts/VideoPopout.tsx index 6ad49302..e8de0262 100644 --- a/src/components/video/parts/VideoPopout.tsx +++ b/src/components/video/parts/VideoPopout.tsx @@ -7,6 +7,7 @@ interface Props { className?: string; } +// TODO store popout in router history so you can press back to yeet export function VideoPopout(props: Props) { const { videoState } = useVideoPlayerState(); const popoutRef = useRef(null); From da097b97d1e88821e21880b7d08762df0eb4099c Mon Sep 17 00:00:00 2001 From: Jip Fr Date: Tue, 24 Jan 2023 00:02:12 +0100 Subject: [PATCH 063/135] style(media): reduce border radius on hover --- src/components/media/MediaCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index f2a652d3..cc30312a 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -43,7 +43,7 @@ function MediaCardContent({ }`} >
Date: Tue, 24 Jan 2023 01:07:30 +0100 Subject: [PATCH 064/135] fix(player): use paddedMins for videos less than an hour --- src/components/video/controls/SkipTime.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/video/controls/SkipTime.tsx b/src/components/video/controls/SkipTime.tsx index 59c2923f..928b3e5c 100644 --- a/src/components/video/controls/SkipTime.tsx +++ b/src/components/video/controls/SkipTime.tsx @@ -22,7 +22,7 @@ function formatSeconds(secs: number, showHours = false): string { const paddedSecs = seconds.toString().padStart(2, "0"); const paddedMins = minutes.toString().padStart(2, "0"); - if (!showHours) return [minutes, paddedSecs].join(":"); + if (!showHours) return [paddedMins, paddedSecs].join(":"); return [hours, paddedMins, paddedSecs].join(":"); } From 3604a2f0d7dc13f7732822a358ccf3eb67f21edf Mon Sep 17 00:00:00 2001 From: destruc7i0n Date: Mon, 23 Jan 2023 20:39:56 -0500 Subject: [PATCH 065/135] show the episode info in the page title --- src/components/video/DecoratedVideoPlayer.tsx | 2 ++ .../video/controls/PageTitleControl.tsx | 23 +++++++++++++ .../video/controls/ShowTitleControl.tsx | 25 +++----------- .../hooks/useCurrentSeriesEpisodeInfo.ts | 33 +++++++++++++++++++ src/views/media/MediaView.tsx | 1 - 5 files changed, 63 insertions(+), 21 deletions(-) create mode 100644 src/components/video/controls/PageTitleControl.tsx create mode 100644 src/components/video/hooks/useCurrentSeriesEpisodeInfo.ts diff --git a/src/components/video/DecoratedVideoPlayer.tsx b/src/components/video/DecoratedVideoPlayer.tsx index 6fd9e498..e34f51c0 100644 --- a/src/components/video/DecoratedVideoPlayer.tsx +++ b/src/components/video/DecoratedVideoPlayer.tsx @@ -14,6 +14,7 @@ import { ShowTitleControl } from "./controls/ShowTitleControl"; import { SkipTime } from "./controls/SkipTime"; import { TimeControl } from "./controls/TimeControl"; import { VolumeControl } from "./controls/VolumeControl"; +import { PageTitleControl } from "./controls/PageTitleControl"; import { VideoPlayerError } from "./parts/VideoPlayerError"; import { VideoPlayerHeader } from "./parts/VideoPlayerHeader"; import { useVideoPlayerState } from "./VideoContext"; @@ -67,6 +68,7 @@ export function DecoratedVideoPlayer( return ( +
diff --git a/src/components/video/controls/PageTitleControl.tsx b/src/components/video/controls/PageTitleControl.tsx new file mode 100644 index 00000000..cbbee11f --- /dev/null +++ b/src/components/video/controls/PageTitleControl.tsx @@ -0,0 +1,23 @@ +import { MWMediaMeta } from "@/backend/metadata/types"; +import { Helmet } from "react-helmet"; +import { useCurrentSeriesEpisodeInfo } from "../hooks/useCurrentSeriesEpisodeInfo"; + +interface PageTitleControlProps { + media?: MWMediaMeta; +} + +export function PageTitleControl(props: PageTitleControlProps) { + const { isSeries, episodeIdentifier } = useCurrentSeriesEpisodeInfo(); + + if (!props.media) return null; + + const title = isSeries + ? `${props.media.title} - ${episodeIdentifier}` + : props.media.title; + + return ( + + {title} + + ); +} diff --git a/src/components/video/controls/ShowTitleControl.tsx b/src/components/video/controls/ShowTitleControl.tsx index 2cb08420..37d76fad 100644 --- a/src/components/video/controls/ShowTitleControl.tsx +++ b/src/components/video/controls/ShowTitleControl.tsx @@ -1,29 +1,14 @@ -import { useMemo } from "react"; -import { useVideoPlayerState } from "../VideoContext"; +import { useCurrentSeriesEpisodeInfo } from "../hooks/useCurrentSeriesEpisodeInfo"; export function ShowTitleControl() { - const { videoState } = useVideoPlayerState(); + const { isSeries, currentEpisodeInfo, episodeIdentifier } = + useCurrentSeriesEpisodeInfo(); - const { current, seasons } = videoState.seasonData; - - const currentSeasonInfo = useMemo(() => { - return seasons?.find((season) => season.id === current?.seasonId); - }, [seasons, current]); - - const currentEpisodeInfo = useMemo(() => { - return currentSeasonInfo?.episodes?.find( - (episode) => episode.id === current?.episodeId - ); - }, [currentSeasonInfo, current]); - - if (!videoState.seasonData.isSeries) return null; - if (!videoState.seasonData.current) return null; - - const selectedText = `S${currentSeasonInfo?.number} E${currentEpisodeInfo?.number}`; + if (!isSeries) return null; return (

- {selectedText} + {episodeIdentifier} {currentEpisodeInfo?.title}

); diff --git a/src/components/video/hooks/useCurrentSeriesEpisodeInfo.ts b/src/components/video/hooks/useCurrentSeriesEpisodeInfo.ts new file mode 100644 index 00000000..e6b00c16 --- /dev/null +++ b/src/components/video/hooks/useCurrentSeriesEpisodeInfo.ts @@ -0,0 +1,33 @@ +import { useMemo } from "react"; +import { useVideoPlayerState } from "../VideoContext"; + +export function useCurrentSeriesEpisodeInfo() { + const { videoState } = useVideoPlayerState(); + + const { current, seasons } = videoState.seasonData; + + const currentSeasonInfo = useMemo(() => { + return seasons?.find((season) => season.id === current?.seasonId); + }, [seasons, current]); + + const currentEpisodeInfo = useMemo(() => { + return currentSeasonInfo?.episodes?.find( + (episode) => episode.id === current?.episodeId + ); + }, [currentSeasonInfo, current]); + + const isSeries = Boolean( + videoState.seasonData.isSeries && videoState.seasonData.current + ); + + if (!isSeries) return { isSeries: false }; + + const episodeIdentifier = `S${currentSeasonInfo?.number} E${currentEpisodeInfo?.number}`; + + return { + isSeries: true, + episodeIdentifier, + currentSeasonInfo, + currentEpisodeInfo, + }; +} diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index f63cc5ee..eedf7853 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -111,7 +111,6 @@ export function MediaViewPlayer(props: MediaViewPlayerProps) { return (
- {props.meta.meta.title} From c3985873d406cc776f64985efbef61c49f9d7dbf Mon Sep 17 00:00:00 2001 From: destruc7i0n Date: Tue, 24 Jan 2023 08:38:37 -0500 Subject: [PATCH 066/135] rename to `humanizedEpisodeId` --- src/components/video/controls/PageTitleControl.tsx | 4 ++-- src/components/video/controls/ShowTitleControl.tsx | 4 ++-- src/components/video/hooks/useCurrentSeriesEpisodeInfo.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/video/controls/PageTitleControl.tsx b/src/components/video/controls/PageTitleControl.tsx index cbbee11f..7f8305c7 100644 --- a/src/components/video/controls/PageTitleControl.tsx +++ b/src/components/video/controls/PageTitleControl.tsx @@ -7,12 +7,12 @@ interface PageTitleControlProps { } export function PageTitleControl(props: PageTitleControlProps) { - const { isSeries, episodeIdentifier } = useCurrentSeriesEpisodeInfo(); + const { isSeries, humanizedEpisodeId } = useCurrentSeriesEpisodeInfo(); if (!props.media) return null; const title = isSeries - ? `${props.media.title} - ${episodeIdentifier}` + ? `${props.media.title} - ${humanizedEpisodeId}` : props.media.title; return ( diff --git a/src/components/video/controls/ShowTitleControl.tsx b/src/components/video/controls/ShowTitleControl.tsx index 37d76fad..dbaa5399 100644 --- a/src/components/video/controls/ShowTitleControl.tsx +++ b/src/components/video/controls/ShowTitleControl.tsx @@ -1,14 +1,14 @@ import { useCurrentSeriesEpisodeInfo } from "../hooks/useCurrentSeriesEpisodeInfo"; export function ShowTitleControl() { - const { isSeries, currentEpisodeInfo, episodeIdentifier } = + const { isSeries, currentEpisodeInfo, humanizedEpisodeId } = useCurrentSeriesEpisodeInfo(); if (!isSeries) return null; return (

- {episodeIdentifier} + {humanizedEpisodeId} {currentEpisodeInfo?.title}

); diff --git a/src/components/video/hooks/useCurrentSeriesEpisodeInfo.ts b/src/components/video/hooks/useCurrentSeriesEpisodeInfo.ts index e6b00c16..a1e432d1 100644 --- a/src/components/video/hooks/useCurrentSeriesEpisodeInfo.ts +++ b/src/components/video/hooks/useCurrentSeriesEpisodeInfo.ts @@ -22,11 +22,11 @@ export function useCurrentSeriesEpisodeInfo() { if (!isSeries) return { isSeries: false }; - const episodeIdentifier = `S${currentSeasonInfo?.number} E${currentEpisodeInfo?.number}`; + const humanizedEpisodeId = `S${currentSeasonInfo?.number} E${currentEpisodeInfo?.number}`; return { isSeries: true, - episodeIdentifier, + humanizedEpisodeId, currentSeasonInfo, currentEpisodeInfo, }; From 701b3db798c503f01ebb42ec64d268fb924fada2 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Tue, 24 Jan 2023 15:17:23 +0100 Subject: [PATCH 067/135] volume control touch events fix Co-authored-by: Jip Frijlink Co-authored-by: James Hawkins --- src/components/video/controls/VolumeControl.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/video/controls/VolumeControl.tsx b/src/components/video/controls/VolumeControl.tsx index 99ec160f..61aff1fc 100644 --- a/src/components/video/controls/VolumeControl.tsx +++ b/src/components/video/controls/VolumeControl.tsx @@ -70,6 +70,7 @@ export function VolumeControl(props: Props) { ref={ref} className="flex h-10 w-20 items-center px-2" onMouseDown={dragMouseDown} + onTouchStart={dragMouseDown} >
Date: Tue, 24 Jan 2023 18:12:37 +0100 Subject: [PATCH 068/135] quality display control, source selection beginning, mobile player UI, keyboard shortcuts Co-authored-by: Jip Frijlink Co-authored-by: James Hawkins --- index.html | 2 +- src/components/Icon.tsx | 2 + src/components/video/DecoratedVideoPlayer.tsx | 101 +++++++--- src/components/video/VideoContext.tsx | 12 +- src/components/video/VideoPlayer.tsx | 84 +++++++- .../video/controls/MobileCenterControl.tsx | 20 ++ .../video/controls/PauseControl.tsx | 2 + .../video/controls/ProgressControl.tsx | 2 +- .../video/controls/QualityDisplayControl.tsx | 14 ++ src/components/video/controls/SkipTime.tsx | 3 +- .../video/controls/SourceControl.tsx | 4 +- .../video/controls/SourceSelectionControl.tsx | 185 ++++++++++++++++++ src/components/video/controls/TimeControl.tsx | 36 ++-- .../video/controls/VolumeControl.tsx | 12 +- src/components/video/hooks/useVideoPlayer.ts | 17 ++ .../video/parts/VideoPlayerHeader.tsx | 16 +- .../video/parts/VideoPlayerIconButton.tsx | 3 +- src/components/video/parts/VideoPopout.tsx | 43 ++-- src/hooks/useIsMobile.ts | 28 +++ src/hooks/useProgressBar.ts | 6 +- src/hooks/useVolumeToggle.ts | 22 +++ src/index.tsx | 6 +- src/setup/index.css | 13 ++ src/views/media/MediaView.tsx | 3 +- 24 files changed, 548 insertions(+), 88 deletions(-) create mode 100644 src/components/video/controls/MobileCenterControl.tsx create mode 100644 src/components/video/controls/QualityDisplayControl.tsx create mode 100644 src/components/video/controls/SourceSelectionControl.tsx create mode 100644 src/hooks/useIsMobile.ts create mode 100644 src/hooks/useVolumeToggle.ts diff --git a/index.html b/index.html index cd808f2e..5e3d67e5 100644 --- a/index.html +++ b/index.html @@ -18,7 +18,7 @@ - + = { episodes: ``, skip_forward: ``, skip_backward: ``, + file: ``, }; export const Icon = memo((props: IconProps) => { diff --git a/src/components/video/DecoratedVideoPlayer.tsx b/src/components/video/DecoratedVideoPlayer.tsx index e34f51c0..700b0149 100644 --- a/src/components/video/DecoratedVideoPlayer.tsx +++ b/src/components/video/DecoratedVideoPlayer.tsx @@ -1,4 +1,5 @@ -import { MWMediaMeta } from "@/backend/metadata/types"; +import { DetailedMeta } from "@/backend/metadata/getmeta"; +import { useIsMobile } from "@/hooks/useIsMobile"; import { useCallback, useRef, useState } from "react"; import { CSSTransition } from "react-transition-group"; import { AirplayControl } from "./controls/AirplayControl"; @@ -7,21 +8,24 @@ import { ChromeCastControl } from "./controls/ChromeCastControl"; import { FullscreenControl } from "./controls/FullscreenControl"; import { LoadingControl } from "./controls/LoadingControl"; import { MiddlePauseControl } from "./controls/MiddlePauseControl"; +import { MobileCenterControl } from "./controls/MobileCenterControl"; +import { PageTitleControl } from "./controls/PageTitleControl"; import { PauseControl } from "./controls/PauseControl"; import { ProgressControl } from "./controls/ProgressControl"; +import { QualityDisplayControl } from "./controls/QualityDisplayControl"; import { SeriesSelectionControl } from "./controls/SeriesSelectionControl"; import { ShowTitleControl } from "./controls/ShowTitleControl"; import { SkipTime } from "./controls/SkipTime"; +import { SourceSelectionControl } from "./controls/SourceSelectionControl"; import { TimeControl } from "./controls/TimeControl"; import { VolumeControl } from "./controls/VolumeControl"; -import { PageTitleControl } from "./controls/PageTitleControl"; import { VideoPlayerError } from "./parts/VideoPlayerError"; import { VideoPlayerHeader } from "./parts/VideoPlayerHeader"; import { useVideoPlayerState } from "./VideoContext"; import { VideoPlayer, VideoPlayerProps } from "./VideoPlayer"; interface DecoratedVideoPlayerProps { - media?: MWMediaMeta; + media?: DetailedMeta; onGoBack?: () => void; } @@ -56,8 +60,10 @@ export function DecoratedVideoPlayer( props: VideoPlayerProps & DecoratedVideoPlayerProps ) { const top = useRef(null); + const center = useRef(null); const bottom = useRef(null); const [show, setShow] = useState(false); + const { isMobile } = useIsMobile(); const onBackdropChange = useCallback( (showing: boolean) => { @@ -68,8 +74,8 @@ export function DecoratedVideoPlayer( return ( - - + +
@@ -77,52 +83,97 @@ export function DecoratedVideoPlayer(
+ {isMobile ? ( + +
+ +
+
+ ) : ( + "" + )}
- -
- -
- - - - -
+
- +
+ {isMobile && } + +
+
+ {isMobile ? ( +
+
+
+ + +
+ +
+ ) : ( + <> + +
+ + + + + + + + )} +
diff --git a/src/components/video/VideoContext.tsx b/src/components/video/VideoContext.tsx index 8cbc8e4a..10941206 100644 --- a/src/components/video/VideoContext.tsx +++ b/src/components/video/VideoContext.tsx @@ -1,4 +1,4 @@ -import { MWStreamType } from "@/backend/helpers/streams"; +import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams"; import React, { createContext, MutableRefObject, @@ -15,16 +15,23 @@ import { interface VideoPlayerContextType { source: string | null; sourceType: MWStreamType; + quality: MWStreamQuality; state: PlayerContext; } const initial: VideoPlayerContextType = { source: null, sourceType: MWStreamType.MP4, + quality: MWStreamQuality.QUNKNOWN, state: initialPlayerState, }; type VideoPlayerContextAction = - | { type: "SET_SOURCE"; url: string; sourceType: MWStreamType } + | { + type: "SET_SOURCE"; + url: string; + sourceType: MWStreamType; + quality: MWStreamQuality; + } | { type: "UPDATE_PLAYER"; state: PlayerContext; @@ -38,6 +45,7 @@ function videoPlayerContextReducer( if (action.type === "SET_SOURCE") { video.source = action.url; video.sourceType = action.sourceType; + video.quality = action.quality; return video; } if (action.type === "UPDATE_PLAYER") { diff --git a/src/components/video/VideoPlayer.tsx b/src/components/video/VideoPlayer.tsx index d00b917a..8f33e11e 100644 --- a/src/components/video/VideoPlayer.tsx +++ b/src/components/video/VideoPlayer.tsx @@ -1,7 +1,12 @@ import { useGoBack } from "@/hooks/useGoBack"; +import { useVolumeControl } from "@/hooks/useVolumeToggle"; import { forwardRef, useContext, useEffect, useRef } from "react"; import { VideoErrorBoundary } from "./parts/VideoErrorBoundary"; -import { VideoPlayerContext, VideoPlayerContextProvider } from "./VideoContext"; +import { + useVideoPlayerState, + VideoPlayerContext, + VideoPlayerContextProvider, +} from "./VideoContext"; export interface VideoPlayerProps { autoPlay?: boolean; @@ -13,15 +18,83 @@ const VideoPlayerInternals = forwardRef< { autoPlay: boolean } >((props, ref) => { const video = useContext(VideoPlayerContext); - const didInitialize = useRef(null); + const didInitialize = useRef<{ source: string | null } | null>(null); + const { videoState } = useVideoPlayerState(); + const { toggleVolume } = useVolumeControl(); useEffect(() => { - if (didInitialize.current) return; + const value = { source: video.source }; + const hasChanged = value.source !== didInitialize.current?.source; + if (!hasChanged) return; if (!video.state.hasInitialized || !video.source) return; video.state.initPlayer(video.source, video.sourceType); - didInitialize.current = true; + didInitialize.current = value; }, [didInitialize, video]); + useEffect(() => { + let isRolling = false; + const onKeyDown = (evt: KeyboardEvent) => { + if (!videoState.isFocused) return; + if (!ref || !(ref as any)?.current) return; + const el = (ref as any).current as HTMLVideoElement; + + switch (evt.key.toLowerCase()) { + // Toggle fullscreen + case "f": + if (videoState.isFullscreen) { + videoState.exitFullscreen(); + } else { + videoState.enterFullscreen(); + } + break; + + // Skip backwards + case "arrowleft": + videoState.setTime(videoState.time - 5); + break; + + // Skip forward + case "arrowright": + videoState.setTime(videoState.time + 5); + break; + + // Pause / play + case " ": + if (videoState.isPaused) { + videoState.play(); + } else { + videoState.pause(); + } + break; + + // Mute + case "m": + toggleVolume(); + break; + + // Do a barrel Roll! + case "r": + if (isRolling) return; + isRolling = true; + el.classList.add("roll"); + setTimeout(() => { + isRolling = false; + el.classList.remove("roll"); + }, 1000); + break; + + default: + break; + } + }; + + window.addEventListener("keydown", onKeyDown); + + return () => { + window.removeEventListener("keydown", onKeyDown); + }; + }, [videoState, toggleVolume, ref]); + // muted attribute is required for safari, as they cant change the volume itself return (
); diff --git a/src/index.tsx b/src/index.tsx index 279c4048..eed53571 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -19,13 +19,11 @@ if (key) { initializeChromecast(); // TODO video todos: -// - finish captions // - chrome cast support -// - bug: mobile controls start showing when resizing -// - bug: popouts sometimes stop working when selecting different episode // - bug: unmounting player throws errors in console // - bug: safari fullscreen will make video overlap player controls -// - bug: safari progress bar is fucked (video doesnt change time but video.currentTime does change) +// - improvement: make scrapers use fuzzy matching on normalized titles +// - bug: source selection doesnt work with HLS // TODO stuff to test: // - browser: firefox, chrome, edge, safari desktop diff --git a/src/video/components/VideoPlayer.tsx b/src/video/components/VideoPlayer.tsx index 7efca5fa..4a369926 100644 --- a/src/video/components/VideoPlayer.tsx +++ b/src/video/components/VideoPlayer.tsx @@ -12,6 +12,7 @@ import { PauseAction } from "@/video/components/actions/PauseAction"; import { ProgressAction } from "@/video/components/actions/ProgressAction"; import { QualityDisplayAction } from "@/video/components/actions/QualityDisplayAction"; import { SeriesSelectionAction } from "@/video/components/actions/SeriesSelectionAction"; +import { SourceSelectionAction } from "@/video/components/actions/SourceSelectionAction"; import { CaptionsSelectionAction } from "@/video/components/actions/CaptionsSelectionAction"; import { ShowTitleAction } from "@/video/components/actions/ShowTitleAction"; import { KeyboardShortcutsAction } from "@/video/components/actions/KeyboardShortcutsAction"; @@ -77,7 +78,6 @@ export function VideoPlayer(props: Props) { [setShow] ); - // TODO source selection return ( + {/* */}
{/* */} diff --git a/src/video/components/actions/CaptionsSelectionAction.tsx b/src/video/components/actions/CaptionsSelectionAction.tsx index 79f6582c..d2bc588a 100644 --- a/src/video/components/actions/CaptionsSelectionAction.tsx +++ b/src/video/components/actions/CaptionsSelectionAction.tsx @@ -3,6 +3,7 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconButton"; import { useControls } from "@/video/state/logic/controls"; import { PopoutAnchor } from "@/video/components/popouts/PopoutAnchor"; +import { useIsMobile } from "@/hooks/useIsMobile"; interface Props { className?: string; @@ -11,6 +12,7 @@ interface Props { export function CaptionsSelectionAction(props: Props) { const descriptor = useVideoPlayerDescriptor(); const controls = useControls(descriptor); + const { isMobile } = useIsMobile(); return (
@@ -18,6 +20,8 @@ export function CaptionsSelectionAction(props: Props) { controls.openPopout("captions")} icon={Icons.CAPTIONS} /> diff --git a/src/video/components/actions/SourceSelectionAction.tsx b/src/video/components/actions/SourceSelectionAction.tsx new file mode 100644 index 00000000..6434bd0a --- /dev/null +++ b/src/video/components/actions/SourceSelectionAction.tsx @@ -0,0 +1,32 @@ +import { Icons } from "@/components/Icon"; +import { useVideoPlayerDescriptor } from "@/video/state/hooks"; +import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconButton"; +import { useControls } from "@/video/state/logic/controls"; +import { PopoutAnchor } from "@/video/components/popouts/PopoutAnchor"; +import { useInterface } from "@/video/state/logic/interface"; + +interface Props { + className?: string; +} + +export function SourceSelectionAction(props: Props) { + const descriptor = useVideoPlayerDescriptor(); + const videoInterface = useInterface(descriptor); + const controls = useControls(descriptor); + + return ( +
+
+ + controls.openPopout("source")} + /> + +
+
+ ); +} diff --git a/src/video/components/popouts/PopoutProviderAction.tsx b/src/video/components/popouts/PopoutProviderAction.tsx index de88057e..8898bf26 100644 --- a/src/video/components/popouts/PopoutProviderAction.tsx +++ b/src/video/components/popouts/PopoutProviderAction.tsx @@ -1,6 +1,7 @@ import { Transition } from "@/components/Transition"; import { useSyncPopouts } from "@/video/components/hooks/useSyncPopouts"; import { EpisodeSelectionPopout } from "@/video/components/popouts/EpisodeSelectionPopout"; +import { SourceSelectionPopout } from "@/video/components/popouts/SourceSelectionPopout"; import { CaptionSelectionPopout } from "@/video/components/popouts/CaptionSelectionPopout"; import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useControls } from "@/video/state/logic/controls"; @@ -21,8 +22,13 @@ function ShowPopout(props: { popoutId: string | null }) { }, [props]); if (popoutId === "episodes") return ; + if (popoutId === "source") return ; if (popoutId === "captions") return ; - return null; + return ( +
+ Unknown popout +
+ ); } function PopoutContainer(props: { videoInterface: VideoInterfaceEvent }) { diff --git a/src/video/components/popouts/SourceSelectionPopout.tsx b/src/video/components/popouts/SourceSelectionPopout.tsx new file mode 100644 index 00000000..b5e9d62a --- /dev/null +++ b/src/video/components/popouts/SourceSelectionPopout.tsx @@ -0,0 +1,205 @@ +import { useMemo, useRef, useState } from "react"; +import { Icon, Icons } from "@/components/Icon"; +import { useLoading } from "@/hooks/useLoading"; +import { Loading } from "@/components/layout/Loading"; +import { IconPatch } from "@/components/buttons/IconPatch"; +import { useVideoPlayerDescriptor } from "@/video/state/hooks"; +import { useMeta } from "@/video/state/logic/meta"; +import { useControls } from "@/video/state/logic/controls"; +import { MWStream } from "@/backend/helpers/streams"; +import { getProviders } from "@/backend/helpers/register"; +import { runProvider } from "@/backend/helpers/run"; +import { MWProviderScrapeResult } from "@/backend/helpers/provider"; +import { PopoutListEntry, PopoutSection } from "./PopoutUtils"; + +// TODO HLS does not work +export function SourceSelectionPopout() { + const descriptor = useVideoPlayerDescriptor(); + const controls = useControls(descriptor); + const meta = useMeta(descriptor); + const providers = useMemo( + () => + meta ? getProviders().filter((v) => v.type.includes(meta.meta.type)) : [], + [meta] + ); + + const [selectedProvider, setSelectedProvider] = useState(null); + const [scrapeResult, setScrapeResult] = + useState(null); + const showingProvider = !!selectedProvider; + const selectedProviderPopulated = useMemo( + () => providers.find((v) => v.id === selectedProvider) ?? null, + [providers, selectedProvider] + ); + const [runScraper, loading, error] = useLoading( + async (providerId: string) => { + const theProvider = providers.find((v) => v.id === providerId); + if (!theProvider) throw new Error("Invalid provider"); + if (!meta) throw new Error("need meta"); + return runProvider(theProvider, { + media: { + imdbId: "", // TODO get actual ids + tmdbId: "", + meta: meta.meta, + }, + progress: () => {}, + type: meta.meta.type, + episode: meta.episode?.episodeId as any, + season: meta.episode?.seasonId as any, + }); + } + ); + + function selectSource(stream: MWStream) { + controls.setSource({ + quality: stream.quality, + source: stream.streamUrl, + type: stream.type, + }); + if (meta) { + controls.setMeta({ + ...meta, + captions: stream.captions, + }); + } + controls.closePopout(); + } + + const providerRef = useRef(null); + const selectProvider = (providerId?: string) => { + if (!providerId) { + providerRef.current = null; + setSelectedProvider(null); + return; + } + + runScraper(providerId).then((v) => { + if (!providerRef.current) return; + if (v) { + const len = v.embeds.length + (v.stream ? 1 : 0); + if (len === 1) { + const realStream = v.stream; + if (!realStream) { + // TODO scrape embed + throw new Error("no embed scraper configured"); + } + selectSource(realStream); + return; + } + } + setScrapeResult(v ?? null); + }); + providerRef.current = providerId; + setSelectedProvider(providerId); + }; + + const titlePositionClass = useMemo(() => { + const offset = !showingProvider ? "left-0" : "left-10"; + return [ + "absolute w-full transition-[left,opacity] duration-200", + offset, + ].join(" "); + }, [showingProvider]); + + return ( + <> + +
+ + + {selectedProviderPopulated?.displayName ?? ""} + + + Sources + +
+
+
+ + {loading ? ( +
+ +
+ ) : error ? ( +
+
+ +

+ Something went wrong loading the embeds for this thing that + you like +

+
+
+ ) : ( + <> + {scrapeResult?.stream ? ( + { + if (scrapeResult.stream) selectSource(scrapeResult.stream); + }} + > + Native source + + ) : null} + {scrapeResult?.embeds.map((v) => ( + { + console.log("EMBED CHOSEN"); + }} + > + {v.type} + + ))} + + )} +
+ +
+ {providers.map((v) => ( + { + selectProvider(v.id); + }} + > + {v.displayName} + + ))} +
+
+
+ + ); +} diff --git a/src/video/state/init.ts b/src/video/state/init.ts index 6914befa..438841e4 100644 --- a/src/video/state/init.ts +++ b/src/video/state/init.ts @@ -2,6 +2,27 @@ import { nanoid } from "nanoid"; import { _players } from "./cache"; import { VideoPlayerState } from "./types"; +export function resetForSource(s: VideoPlayerState) { + const state = s; + state.mediaPlaying = { + isPlaying: false, + isPaused: true, + isLoading: false, + isSeeking: false, + isDragSeeking: false, + isFirstLoading: true, + hasPlayedOnce: false, + volume: 0, + }; + state.progress = { + time: 0, + duration: 0, + buffered: 0, + draggingTime: 0, + }; + state.initalized = false; +} + function initPlayer(): VideoPlayerState { return { interface: { @@ -38,6 +59,7 @@ function initPlayer(): VideoPlayerState { initalized: false, pausedWhenSeeking: false, + hlsInstance: null, stateProvider: null, wrapperElement: null, }; diff --git a/src/video/state/providers/helpers.ts b/src/video/state/providers/helpers.ts new file mode 100644 index 00000000..f21d7131 --- /dev/null +++ b/src/video/state/providers/helpers.ts @@ -0,0 +1,17 @@ +import { resetForSource } from "@/video/state/init"; +import { updateMediaPlaying } from "@/video/state/logic/mediaplaying"; +import { updateMisc } from "@/video/state/logic/misc"; +import { updateProgress } from "@/video/state/logic/progress"; +import { VideoPlayerState } from "@/video/state/types"; + +export function resetStateForSource(descriptor: string, s: VideoPlayerState) { + const state = s; + if (state.hlsInstance) { + state.hlsInstance.destroy(); + state.hlsInstance = null; + } + resetForSource(state); + updateMediaPlaying(descriptor, state); + updateProgress(descriptor, state); + updateMisc(descriptor, state); +} diff --git a/src/video/state/providers/videoStateProvider.ts b/src/video/state/providers/videoStateProvider.ts index b7cb395f..eb88f5df 100644 --- a/src/video/state/providers/videoStateProvider.ts +++ b/src/video/state/providers/videoStateProvider.ts @@ -15,6 +15,7 @@ import { } from "@/video/components/hooks/volumeStore"; import { updateError } from "@/video/state/logic/error"; import { updateMisc } from "@/video/state/logic/misc"; +import { resetStateForSource } from "@/video/state/providers/helpers"; import { getPlayerState } from "../cache"; import { updateMediaPlaying } from "../logic/mediaplaying"; import { VideoPlayerStateProvider } from "./providerTypes"; @@ -130,6 +131,7 @@ export function createVideoStateProvider( if (!source) { player.src = ""; state.source = null; + resetStateForSource(descriptor, state); updateSource(descriptor, state); return; } @@ -149,6 +151,7 @@ export function createVideoStateProvider( } const hls = new Hls({ enableWorker: false }); + state.hlsInstance = hls; hls.on(Hls.Events.ERROR, (event, data) => { if (data.fatal) { @@ -175,6 +178,7 @@ export function createVideoStateProvider( url: source.source, caption: null, }; + resetStateForSource(descriptor, state); updateSource(descriptor, state); }, setCaption(id, url) { diff --git a/src/video/state/types.ts b/src/video/state/types.ts index 41de0c72..3ca48aa9 100644 --- a/src/video/state/types.ts +++ b/src/video/state/types.ts @@ -4,6 +4,7 @@ import { MWStreamType, } from "@/backend/helpers/streams"; import { MWMediaMeta } from "@/backend/metadata/types"; +import Hls from "hls.js"; import { VideoPlayerStateProvider } from "./providers/providerTypes"; export type VideoPlayerMeta = { @@ -73,6 +74,7 @@ export type VideoPlayerState = { // backing fields pausedWhenSeeking: boolean; // when seeking, used to store if paused when started to seek + hlsInstance: null | Hls; // HLS video player instance storage stateProvider: VideoPlayerStateProvider | null; wrapperElement: HTMLDivElement | null; }; From 772be4b42dc7c071414ed388551e6bf4558ed4b8 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Thu, 9 Feb 2023 22:12:38 +0100 Subject: [PATCH 103/135] add todo --- src/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.tsx b/src/index.tsx index eed53571..488f4a1b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -24,6 +24,7 @@ initializeChromecast(); // - bug: safari fullscreen will make video overlap player controls // - improvement: make scrapers use fuzzy matching on normalized titles // - bug: source selection doesnt work with HLS +// - bug: .ass subtitle files are fucked // TODO stuff to test: // - browser: firefox, chrome, edge, safari desktop From 886ffe78efeb77570da0bf17f2f7d0da12acc58b Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Thu, 9 Feb 2023 22:13:55 +0100 Subject: [PATCH 104/135] more todos --- src/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.tsx b/src/index.tsx index 488f4a1b..202b4840 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -25,6 +25,7 @@ initializeChromecast(); // - improvement: make scrapers use fuzzy matching on normalized titles // - bug: source selection doesnt work with HLS // - bug: .ass subtitle files are fucked +// - improvement: episode watch at the ending should not startAt // TODO stuff to test: // - browser: firefox, chrome, edge, safari desktop From 8f23240ea14f6eb0844956da097060b780811c0d Mon Sep 17 00:00:00 2001 From: Jip Fr Date: Sat, 11 Feb 2023 00:43:38 +0100 Subject: [PATCH 105/135] Get started on migration --- src/state/watched/context.tsx | 2 +- src/state/watched/store.ts | 2 +- src/views/search/HomeView.tsx | 219 +++++++++++++++++++++++++++++++++- 3 files changed, 219 insertions(+), 4 deletions(-) diff --git a/src/state/watched/context.tsx b/src/state/watched/context.tsx index eff5a5c1..62da316d 100644 --- a/src/state/watched/context.tsx +++ b/src/state/watched/context.tsx @@ -44,7 +44,7 @@ interface MediaItem { }; } -interface WatchedStoreItem { +export interface WatchedStoreItem { item: MediaItem; progress: number; percentage: number; diff --git a/src/state/watched/store.ts b/src/state/watched/store.ts index aada4131..075e9e43 100644 --- a/src/state/watched/store.ts +++ b/src/state/watched/store.ts @@ -1,7 +1,7 @@ import { versionedStoreBuilder } from "@/utils/storage"; export const VideoProgressStore = versionedStoreBuilder() - .setKey("video-progress") + .setKey("video-progress-v3") .addVersion({ version: 0, }) diff --git a/src/views/search/HomeView.tsx b/src/views/search/HomeView.tsx index b5a17843..d07bdd1f 100644 --- a/src/views/search/HomeView.tsx +++ b/src/views/search/HomeView.tsx @@ -6,11 +6,19 @@ import { getIfBookmarkedFromPortable, useBookmarkContext, } from "@/state/bookmark"; -import { useWatchedContext } from "@/state/watched"; +import { + useWatchedContext, + WatchedStoreData, + WatchedStoreItem, +} from "@/state/watched"; import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; import { EditButton } from "@/components/buttons/EditButton"; -import { useState } from "react"; +import { useCallback, useState } from "react"; import { useAutoAnimate } from "@formkit/auto-animate/react"; +import { VideoProgressStore } from "@/state/watched/store"; +import { searchForMedia } from "@/backend/metadata/search"; +import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta"; +import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types"; function Bookmarks() { const { t } = useTranslation(); @@ -43,6 +51,30 @@ function Bookmarks() { ); } +interface OldMediaBase { + mediaId: number; + mediaType: MWMediaType; + percentage: number; + progress: number; + providerId: string; + title: string; + year: number; +} + +interface OldMovie extends OldMediaBase { + mediaType: MWMediaType.MOVIE; +} + +interface OldSeries extends OldMediaBase { + mediaType: MWMediaType.SERIES; + episodeId: number; + seasonId: number; +} + +interface OldData { + items: (OldMovie | OldSeries)[]; +} + function Watched() { const { t } = useTranslation(); const { getFilteredBookmarks } = useBookmarkContext(); @@ -55,6 +87,189 @@ function Watched() { (v) => !getIfBookmarkedFromPortable(bookmarks, v.item.meta) ); + /* + AAA + */ + const watchedLocalstorage = VideoProgressStore.get(); + const [watched, setWatchedReal] = useState( + watchedLocalstorage as WatchedStoreData + ); + + const setWatched = useCallback( + (data: any) => { + setWatchedReal((old) => { + let newData = data; + if (data.constructor === Function) { + newData = data(old); + } + watchedLocalstorage.save(newData); + return newData; + }); + }, + [setWatchedReal, watchedLocalstorage] + ); + + (async () => { + const oldData: OldData | null = localStorage.getItem("video-progress") + ? JSON.parse(localStorage.getItem("video-progress") || "") + : null; + + if (!oldData) return; + + const uniqueMedias: Record = {}; + oldData.items.forEach((item: any) => { + if (uniqueMedias[item.mediaId]) return; + uniqueMedias[item.mediaId] = item; + }); + + const yearsAreClose = (a: number, b: number) => { + return Math.abs(a - b) <= 1; + }; + + const mediaMetas: Record> = {}; + + Promise.all( + Object.values(uniqueMedias).map(async (item) => { + const year = Number(item.year.toString().split("-")[0]); + const data = await searchForMedia({ + searchQuery: `${item.title} ${year}`, + type: item.mediaType, + }); + const relevantItem = data.find((res) => + yearsAreClose(Number(res.year), year) + ); + if (!relevantItem) { + console.error("No item"); + return; + } + return { + id: item.mediaId, + data: relevantItem, + }; + }) + ).then(async (relevantItems) => { + console.log(relevantItems); + for (const item of relevantItems.filter(Boolean)) { + if (!item) continue; + + let keys: (string | null)[][] = [["0", "0"]]; + if (item.data.type === "series") { + const meta = await getMetaFromId(item.data.type, item.data.id); + if (!meta || !meta?.meta.seasons) return; + const seasonNumbers = [ + ...new Set( + oldData.items + .filter((watchedEntry: any) => watchedEntry.mediaId === item.id) + .map((watchedEntry: any) => watchedEntry.seasonId) + ), + ]; + const seasons = seasonNumbers + .map((num) => ({ + num, + season: meta.meta?.seasons?.[(num as number) - 1], + })) + .filter(Boolean); + keys = seasons + .map((season) => (season ? [season.num, season?.season?.id] : [])) + .filter((entry) => entry.length > 0); // Stupid TypeScript + } + + if (!mediaMetas[item.id]) mediaMetas[item.id] = {}; + await Promise.all( + keys.map(async ([key, id]) => { + if (!key) return; + mediaMetas[item.id][key] = await getMetaFromId( + item.data.type, + item.data.id, + id === "0" || id === null ? undefined : id + ); + }) + ); + } + + // We've got all the metadata you can dream of now + // Now let's convert stuff into the new format. + const newData: WatchedStoreData = JSON.parse(JSON.stringify(watched)); + + for (const oldWatched of oldData.items) { + if (oldWatched.mediaType === "movie") { + if (!mediaMetas[oldWatched.mediaId]["0"]?.meta) continue; + + const newItem: WatchedStoreItem = { + item: { + meta: mediaMetas[oldWatched.mediaId]["0"]?.meta as MWMediaMeta, + }, + progress: oldWatched.progress, + percentage: oldWatched.percentage, + watchedAt: Date.now(), // There was no watchedAt in V2 + }; + if ( + newData.items.find( + (item) => item.item.meta.id === newItem.item.meta.id + ) + ) + continue; + + oldData.items = oldData.items.filter( + (item) => JSON.stringify(item) !== JSON.stringify(oldWatched) + ); + newData.items.push(newItem); + } else if (oldWatched.mediaType === "series") { + // console.log(oldWatched); + // console.log(mediaMetas[oldWatched.mediaId][oldWatched.seasonId]); + + if (!mediaMetas[oldWatched.mediaId][oldWatched.seasonId]?.meta) + continue; + + const meta = mediaMetas[oldWatched.mediaId][oldWatched.seasonId] + ?.meta as MWMediaMeta; + + if (meta.type !== "series") return; + + // console.log(meta.seasonData); + const newItem: WatchedStoreItem = { + item: { + meta, + series: { + episode: Number(oldWatched.episodeId), + season: Number(oldWatched.seasonId), + seasonId: meta.seasonData.id, + episodeId: + meta.seasonData.episodes[Number(oldWatched.episodeId) - 1].id, + }, + }, + progress: oldWatched.progress, + percentage: oldWatched.percentage, + watchedAt: Date.now(), // There was no watchedAt in V2 + }; + + if ( + newData.items.find( + (item) => + item.item.meta.id === newItem.item.meta.id && + item.item.series?.episodeId === newItem.item.series?.episodeId + ) + ) + continue; + + oldData.items = oldData.items.filter( + (item) => JSON.stringify(item) !== JSON.stringify(oldWatched) + ); + newData.items.push(newItem); + } + } + + if (JSON.stringify(newData) !== JSON.stringify(watched)) { + localStorage.setItem("video-progress", JSON.stringify(oldData)); + setWatched(() => newData); + } + }); + })(); + + /* + AAA + */ + if (watchedItems.length === 0) return null; return ( From dd14b575eb4f1e80cca8ef2797b23097ba986548 Mon Sep 17 00:00:00 2001 From: Jip Fr Date: Sat, 11 Feb 2023 01:05:27 +0100 Subject: [PATCH 106/135] Move migration out of home into store --- src/state/watched/store.ts | 186 ++++++++++++++++++++++++++++- src/views/search/HomeView.tsx | 219 +--------------------------------- 2 files changed, 186 insertions(+), 219 deletions(-) diff --git a/src/state/watched/store.ts b/src/state/watched/store.ts index 075e9e43..4bab3c78 100644 --- a/src/state/watched/store.ts +++ b/src/state/watched/store.ts @@ -1,7 +1,35 @@ +import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta"; +import { searchForMedia } from "@/backend/metadata/search"; +import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types"; import { versionedStoreBuilder } from "@/utils/storage"; +import { WatchedStoreData, WatchedStoreItem } from "./context"; + +interface OldMediaBase { + mediaId: number; + mediaType: MWMediaType; + percentage: number; + progress: number; + providerId: string; + title: string; + year: number; +} + +interface OldMovie extends OldMediaBase { + mediaType: MWMediaType.MOVIE; +} + +interface OldSeries extends OldMediaBase { + mediaType: MWMediaType.SERIES; + episodeId: number; + seasonId: number; +} + +interface OldData { + items: (OldMovie | OldSeries)[]; +} export const VideoProgressStore = versionedStoreBuilder() - .setKey("video-progress-v3") + .setKey("video-progress") .addVersion({ version: 0, }) @@ -15,7 +43,11 @@ export const VideoProgressStore = versionedStoreBuilder() }) .addVersion({ version: 2, - migrate() { + migrate(old: OldData) { + requestAnimationFrame(() => { + // eslint-disable-next-line no-use-before-define + migrateV2(old); + }); return { items: [], }; @@ -27,3 +59,153 @@ export const VideoProgressStore = versionedStoreBuilder() }, }) .build(); + +async function migrateV2(old: OldData) { + const oldData = old; + if (!oldData) return; + + const uniqueMedias: Record = {}; + oldData.items.forEach((item: any) => { + if (uniqueMedias[item.mediaId]) return; + uniqueMedias[item.mediaId] = item; + }); + + const yearsAreClose = (a: number, b: number) => { + return Math.abs(a - b) <= 1; + }; + + const mediaMetas: Record> = {}; + + const relevantItems = await Promise.all( + Object.values(uniqueMedias).map(async (item) => { + const year = Number(item.year.toString().split("-")[0]); + const data = await searchForMedia({ + searchQuery: `${item.title} ${year}`, + type: item.mediaType, + }); + const relevantItem = data.find((res) => + yearsAreClose(Number(res.year), year) + ); + if (!relevantItem) { + console.error("No item"); + return; + } + return { + id: item.mediaId, + data: relevantItem, + }; + }) + ); + + for (const item of relevantItems.filter(Boolean)) { + if (!item) continue; + + let keys: (string | null)[][] = [["0", "0"]]; + if (item.data.type === "series") { + const meta = await getMetaFromId(item.data.type, item.data.id); + if (!meta || !meta?.meta.seasons) return; + const seasonNumbers = [ + ...new Set( + oldData.items + .filter((watchedEntry: any) => watchedEntry.mediaId === item.id) + .map((watchedEntry: any) => watchedEntry.seasonId) + ), + ]; + const seasons = seasonNumbers + .map((num) => ({ + num, + season: meta.meta?.seasons?.[(num as number) - 1], + })) + .filter(Boolean); + keys = seasons + .map((season) => (season ? [season.num, season?.season?.id] : [])) + .filter((entry) => entry.length > 0); // Stupid TypeScript + } + + if (!mediaMetas[item.id]) mediaMetas[item.id] = {}; + await Promise.all( + keys.map(async ([key, id]) => { + if (!key) return; + mediaMetas[item.id][key] = await getMetaFromId( + item.data.type, + item.data.id, + id === "0" || id === null ? undefined : id + ); + }) + ); + } + + // We've got all the metadata you can dream of now + // Now let's convert stuff into the new format. + interface WatchedStoreDataWithVersion extends WatchedStoreData { + "--version": number; + } + const newData: WatchedStoreDataWithVersion = { + ...oldData, + items: [], + "--version": 2, + }; + + for (const oldWatched of oldData.items) { + if (oldWatched.mediaType === "movie") { + if (!mediaMetas[oldWatched.mediaId]["0"]?.meta) continue; + + const newItem: WatchedStoreItem = { + item: { + meta: mediaMetas[oldWatched.mediaId]["0"]?.meta as MWMediaMeta, + }, + progress: oldWatched.progress, + percentage: oldWatched.percentage, + watchedAt: Date.now(), // There was no watchedAt in V2 + }; + + oldData.items = oldData.items.filter( + (item) => JSON.stringify(item) !== JSON.stringify(oldWatched) + ); + newData.items.push(newItem); + } else if (oldWatched.mediaType === "series") { + if (!mediaMetas[oldWatched.mediaId][oldWatched.seasonId]?.meta) continue; + + const meta = mediaMetas[oldWatched.mediaId][oldWatched.seasonId] + ?.meta as MWMediaMeta; + + if (meta.type !== "series") return; + + const newItem: WatchedStoreItem = { + item: { + meta, + series: { + episode: Number(oldWatched.episodeId), + season: Number(oldWatched.seasonId), + seasonId: meta.seasonData.id, + episodeId: + meta.seasonData.episodes[Number(oldWatched.episodeId) - 1].id, + }, + }, + progress: oldWatched.progress, + percentage: oldWatched.percentage, + watchedAt: Date.now(), // There was no watchedAt in V2 + }; + + if ( + newData.items.find( + (item) => + item.item.meta.id === newItem.item.meta.id && + item.item.series?.episodeId === newItem.item.series?.episodeId + ) + ) + continue; + + oldData.items = oldData.items.filter( + (item) => JSON.stringify(item) !== JSON.stringify(oldWatched) + ); + newData.items.push(newItem); + } + } + + console.log(JSON.stringify(old), JSON.stringify(newData)); + if (JSON.stringify(old.items) !== JSON.stringify(newData.items)) { + console.log(newData); + VideoProgressStore.get().save(newData); + } +} diff --git a/src/views/search/HomeView.tsx b/src/views/search/HomeView.tsx index d07bdd1f..b5a17843 100644 --- a/src/views/search/HomeView.tsx +++ b/src/views/search/HomeView.tsx @@ -6,19 +6,11 @@ import { getIfBookmarkedFromPortable, useBookmarkContext, } from "@/state/bookmark"; -import { - useWatchedContext, - WatchedStoreData, - WatchedStoreItem, -} from "@/state/watched"; +import { useWatchedContext } from "@/state/watched"; import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; import { EditButton } from "@/components/buttons/EditButton"; -import { useCallback, useState } from "react"; +import { useState } from "react"; import { useAutoAnimate } from "@formkit/auto-animate/react"; -import { VideoProgressStore } from "@/state/watched/store"; -import { searchForMedia } from "@/backend/metadata/search"; -import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta"; -import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types"; function Bookmarks() { const { t } = useTranslation(); @@ -51,30 +43,6 @@ function Bookmarks() { ); } -interface OldMediaBase { - mediaId: number; - mediaType: MWMediaType; - percentage: number; - progress: number; - providerId: string; - title: string; - year: number; -} - -interface OldMovie extends OldMediaBase { - mediaType: MWMediaType.MOVIE; -} - -interface OldSeries extends OldMediaBase { - mediaType: MWMediaType.SERIES; - episodeId: number; - seasonId: number; -} - -interface OldData { - items: (OldMovie | OldSeries)[]; -} - function Watched() { const { t } = useTranslation(); const { getFilteredBookmarks } = useBookmarkContext(); @@ -87,189 +55,6 @@ function Watched() { (v) => !getIfBookmarkedFromPortable(bookmarks, v.item.meta) ); - /* - AAA - */ - const watchedLocalstorage = VideoProgressStore.get(); - const [watched, setWatchedReal] = useState( - watchedLocalstorage as WatchedStoreData - ); - - const setWatched = useCallback( - (data: any) => { - setWatchedReal((old) => { - let newData = data; - if (data.constructor === Function) { - newData = data(old); - } - watchedLocalstorage.save(newData); - return newData; - }); - }, - [setWatchedReal, watchedLocalstorage] - ); - - (async () => { - const oldData: OldData | null = localStorage.getItem("video-progress") - ? JSON.parse(localStorage.getItem("video-progress") || "") - : null; - - if (!oldData) return; - - const uniqueMedias: Record = {}; - oldData.items.forEach((item: any) => { - if (uniqueMedias[item.mediaId]) return; - uniqueMedias[item.mediaId] = item; - }); - - const yearsAreClose = (a: number, b: number) => { - return Math.abs(a - b) <= 1; - }; - - const mediaMetas: Record> = {}; - - Promise.all( - Object.values(uniqueMedias).map(async (item) => { - const year = Number(item.year.toString().split("-")[0]); - const data = await searchForMedia({ - searchQuery: `${item.title} ${year}`, - type: item.mediaType, - }); - const relevantItem = data.find((res) => - yearsAreClose(Number(res.year), year) - ); - if (!relevantItem) { - console.error("No item"); - return; - } - return { - id: item.mediaId, - data: relevantItem, - }; - }) - ).then(async (relevantItems) => { - console.log(relevantItems); - for (const item of relevantItems.filter(Boolean)) { - if (!item) continue; - - let keys: (string | null)[][] = [["0", "0"]]; - if (item.data.type === "series") { - const meta = await getMetaFromId(item.data.type, item.data.id); - if (!meta || !meta?.meta.seasons) return; - const seasonNumbers = [ - ...new Set( - oldData.items - .filter((watchedEntry: any) => watchedEntry.mediaId === item.id) - .map((watchedEntry: any) => watchedEntry.seasonId) - ), - ]; - const seasons = seasonNumbers - .map((num) => ({ - num, - season: meta.meta?.seasons?.[(num as number) - 1], - })) - .filter(Boolean); - keys = seasons - .map((season) => (season ? [season.num, season?.season?.id] : [])) - .filter((entry) => entry.length > 0); // Stupid TypeScript - } - - if (!mediaMetas[item.id]) mediaMetas[item.id] = {}; - await Promise.all( - keys.map(async ([key, id]) => { - if (!key) return; - mediaMetas[item.id][key] = await getMetaFromId( - item.data.type, - item.data.id, - id === "0" || id === null ? undefined : id - ); - }) - ); - } - - // We've got all the metadata you can dream of now - // Now let's convert stuff into the new format. - const newData: WatchedStoreData = JSON.parse(JSON.stringify(watched)); - - for (const oldWatched of oldData.items) { - if (oldWatched.mediaType === "movie") { - if (!mediaMetas[oldWatched.mediaId]["0"]?.meta) continue; - - const newItem: WatchedStoreItem = { - item: { - meta: mediaMetas[oldWatched.mediaId]["0"]?.meta as MWMediaMeta, - }, - progress: oldWatched.progress, - percentage: oldWatched.percentage, - watchedAt: Date.now(), // There was no watchedAt in V2 - }; - if ( - newData.items.find( - (item) => item.item.meta.id === newItem.item.meta.id - ) - ) - continue; - - oldData.items = oldData.items.filter( - (item) => JSON.stringify(item) !== JSON.stringify(oldWatched) - ); - newData.items.push(newItem); - } else if (oldWatched.mediaType === "series") { - // console.log(oldWatched); - // console.log(mediaMetas[oldWatched.mediaId][oldWatched.seasonId]); - - if (!mediaMetas[oldWatched.mediaId][oldWatched.seasonId]?.meta) - continue; - - const meta = mediaMetas[oldWatched.mediaId][oldWatched.seasonId] - ?.meta as MWMediaMeta; - - if (meta.type !== "series") return; - - // console.log(meta.seasonData); - const newItem: WatchedStoreItem = { - item: { - meta, - series: { - episode: Number(oldWatched.episodeId), - season: Number(oldWatched.seasonId), - seasonId: meta.seasonData.id, - episodeId: - meta.seasonData.episodes[Number(oldWatched.episodeId) - 1].id, - }, - }, - progress: oldWatched.progress, - percentage: oldWatched.percentage, - watchedAt: Date.now(), // There was no watchedAt in V2 - }; - - if ( - newData.items.find( - (item) => - item.item.meta.id === newItem.item.meta.id && - item.item.series?.episodeId === newItem.item.series?.episodeId - ) - ) - continue; - - oldData.items = oldData.items.filter( - (item) => JSON.stringify(item) !== JSON.stringify(oldWatched) - ); - newData.items.push(newItem); - } - } - - if (JSON.stringify(newData) !== JSON.stringify(watched)) { - localStorage.setItem("video-progress", JSON.stringify(oldData)); - setWatched(() => newData); - } - }); - })(); - - /* - AAA - */ - if (watchedItems.length === 0) return null; return ( From 942a6cc9c072fb1a1e2bb2de5cb3d1a9b6e8fdf0 Mon Sep 17 00:00:00 2001 From: Jip Fr Date: Sun, 12 Feb 2023 00:41:55 +0100 Subject: [PATCH 107/135] Made type-safe versioned store, migrated to it Co-authored-by: mrjvs --- src/index.tsx | 10 +- src/state/bookmark/context.tsx | 35 +-- src/state/bookmark/store.ts | 13 +- src/state/bookmark/types.ts | 5 + src/state/watched/context.tsx | 57 +--- src/state/watched/migrations/v2.ts | 170 ++++++++++ src/state/watched/store.ts | 204 +----------- src/state/watched/types.ts | 22 ++ src/utils/storage.ts | 364 ++++++++++------------ src/video/components/hooks/volumeStore.ts | 11 +- 10 files changed, 407 insertions(+), 484 deletions(-) create mode 100644 src/state/bookmark/types.ts create mode 100644 src/state/watched/migrations/v2.ts create mode 100644 src/state/watched/types.ts diff --git a/src/index.tsx b/src/index.tsx index 202b4840..467a898b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -9,6 +9,7 @@ import "@/setup/i18n"; import "@/setup/index.css"; import "@/backend"; import { initializeChromecast } from "./setup/chromecast"; +import { initializeStores } from "./utils/storage"; // initialize const key = @@ -42,12 +43,19 @@ initializeChromecast(); // TODO general todos: // - localize everything (fix loading screen text (series vs movies)) +const LazyLoadedApp = React.lazy(async () => { + await initializeStores(); + return { + default: App, + }; +}); + ReactDOM.render( - + diff --git a/src/state/bookmark/context.tsx b/src/state/bookmark/context.tsx index 65485a7b..6252ceac 100644 --- a/src/state/bookmark/context.tsx +++ b/src/state/bookmark/context.tsx @@ -1,17 +1,8 @@ import { MWMediaMeta } from "@/backend/metadata/types"; -import { - createContext, - ReactNode, - useCallback, - useContext, - useMemo, - useState, -} from "react"; +import { useStore } from "@/utils/storage"; +import { createContext, ReactNode, useContext, useMemo } from "react"; import { BookmarkStore } from "./store"; - -interface BookmarkStoreData { - bookmarks: MWMediaMeta[]; -} +import { BookmarkStoreData } from "./types"; interface BookmarkStoreDataWrapper { setItemBookmark(media: MWMediaMeta, bookedmarked: boolean): void; @@ -36,25 +27,7 @@ function getBookmarkIndexFromMedia( } export function BookmarkContextProvider(props: { children: ReactNode }) { - const bookmarkLocalstorage = BookmarkStore.get(); - const [bookmarkStorage, setBookmarkStore] = useState( - bookmarkLocalstorage as BookmarkStoreData - ); - - const setBookmarked = useCallback( - (data: any) => { - setBookmarkStore((old) => { - const old2 = JSON.parse(JSON.stringify(old)); - let newData = data; - if (data.constructor === Function) { - newData = data(old2); - } - bookmarkLocalstorage.save(newData); - return newData; - }); - }, - [bookmarkLocalstorage, setBookmarkStore] - ); + const [bookmarkStorage, setBookmarked] = useStore(BookmarkStore); const contextValue = useMemo( () => ({ diff --git a/src/state/bookmark/store.ts b/src/state/bookmark/store.ts index 089a6693..3b7a3a92 100644 --- a/src/state/bookmark/store.ts +++ b/src/state/bookmark/store.ts @@ -1,17 +1,18 @@ -import { versionedStoreBuilder } from "@/utils/storage"; +import { createVersionedStore } from "@/utils/storage"; +import { BookmarkStoreData } from "./types"; -export const BookmarkStore = versionedStoreBuilder() +export const BookmarkStore = createVersionedStore() .setKey("mw-bookmarks") .addVersion({ version: 0, - }) - .addVersion({ - version: 1, migrate() { return { - bookmarks: [], + bookmarks: [], // TODO migrate bookmarks }; }, + }) + .addVersion({ + version: 1, create() { return { bookmarks: [], diff --git a/src/state/bookmark/types.ts b/src/state/bookmark/types.ts new file mode 100644 index 00000000..05cb3641 --- /dev/null +++ b/src/state/bookmark/types.ts @@ -0,0 +1,5 @@ +import { MWMediaMeta } from "@/backend/metadata/types"; + +export interface BookmarkStoreData { + bookmarks: MWMediaMeta[]; +} diff --git a/src/state/watched/context.tsx b/src/state/watched/context.tsx index 62da316d..f45baf71 100644 --- a/src/state/watched/context.tsx +++ b/src/state/watched/context.tsx @@ -1,5 +1,6 @@ import { DetailedMeta } from "@/backend/metadata/getmeta"; -import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types"; +import { MWMediaType } from "@/backend/metadata/types"; +import { useStore } from "@/utils/storage"; import { createContext, ReactNode, @@ -7,9 +8,9 @@ import { useContext, useMemo, useRef, - useState, } from "react"; import { VideoProgressStore } from "./store"; +import { StoreMediaItem, WatchedStoreItem, WatchedStoreData } from "./types"; const FIVETEEN_MINUTES = 15 * 60; const FIVE_MINUTES = 5 * 60; @@ -34,29 +35,8 @@ function shouldSave( return true; } -interface MediaItem { - meta: MWMediaMeta; - series?: { - episodeId: string; - seasonId: string; - episode: number; - season: number; - }; -} - -export interface WatchedStoreItem { - item: MediaItem; - progress: number; - percentage: number; - watchedAt: number; -} - -export interface WatchedStoreData { - items: WatchedStoreItem[]; -} - interface WatchedStoreDataWrapper { - updateProgress(media: MediaItem, progress: number, total: number): void; + updateProgress(media: StoreMediaItem, progress: number, total: number): void; getFilteredWatched(): WatchedStoreItem[]; removeProgress(id: string): void; watched: WatchedStoreData; @@ -72,7 +52,7 @@ const WatchedContext = createContext({ }); WatchedContext.displayName = "WatchedContext"; -function isSameEpisode(media: MediaItem, v: MediaItem) { +function isSameEpisode(media: StoreMediaItem, v: StoreMediaItem) { return ( media.meta.id === v.meta.id && (!media.series || @@ -82,24 +62,7 @@ function isSameEpisode(media: MediaItem, v: MediaItem) { } export function WatchedContextProvider(props: { children: ReactNode }) { - const watchedLocalstorage = VideoProgressStore.get(); - const [watched, setWatchedReal] = useState( - watchedLocalstorage as WatchedStoreData - ); - - const setWatched = useCallback( - (data: any) => { - setWatchedReal((old) => { - let newData = data; - if (data.constructor === Function) { - newData = data(old); - } - watchedLocalstorage.save(newData); - return newData; - }); - }, - [setWatchedReal, watchedLocalstorage] - ); + const [watched, setWatched] = useStore(VideoProgressStore); const contextValue = useMemo( () => ({ @@ -110,7 +73,11 @@ export function WatchedContextProvider(props: { children: ReactNode }) { return newData; }); }, - updateProgress(media: MediaItem, progress: number, total: number): void { + updateProgress( + media: StoreMediaItem, + progress: number, + total: number + ): void { setWatched((data: WatchedStoreData) => { const newData = { ...data }; let item = newData.items.find((v) => isSameEpisode(media, v.item)); @@ -176,7 +143,7 @@ export function useWatchedContext() { } function isSameEpisodeMeta( - media: MediaItem, + media: StoreMediaItem, mediaTwo: DetailedMeta | null, episodeId?: string ) { diff --git a/src/state/watched/migrations/v2.ts b/src/state/watched/migrations/v2.ts new file mode 100644 index 00000000..ff0115e6 --- /dev/null +++ b/src/state/watched/migrations/v2.ts @@ -0,0 +1,170 @@ +import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta"; +import { searchForMedia } from "@/backend/metadata/search"; +import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types"; +import { WatchedStoreData, WatchedStoreItem } from "../types"; + +interface OldMediaBase { + mediaId: number; + mediaType: MWMediaType; + percentage: number; + progress: number; + providerId: string; + title: string; + year: number; +} + +interface OldMovie extends OldMediaBase { + mediaType: MWMediaType.MOVIE; +} + +interface OldSeries extends OldMediaBase { + mediaType: MWMediaType.SERIES; + episodeId: number; + seasonId: number; +} + +export interface OldData { + items: (OldMovie | OldSeries)[]; +} + +export async function migrateV2(old: OldData) { + const oldData = old; + if (!oldData) return; + + const uniqueMedias: Record = {}; + oldData.items.forEach((item: any) => { + if (uniqueMedias[item.mediaId]) return; + uniqueMedias[item.mediaId] = item; + }); + + const yearsAreClose = (a: number, b: number) => { + return Math.abs(a - b) <= 1; + }; + + const mediaMetas: Record> = {}; + + const relevantItems = await Promise.all( + Object.values(uniqueMedias).map(async (item) => { + const year = Number(item.year.toString().split("-")[0]); + const data = await searchForMedia({ + searchQuery: `${item.title} ${year}`, + type: item.mediaType, + }); + const relevantItem = data.find((res) => + yearsAreClose(Number(res.year), year) + ); + if (!relevantItem) { + console.error("No item"); + return; + } + return { + id: item.mediaId, + data: relevantItem, + }; + }) + ); + + for (const item of relevantItems.filter(Boolean)) { + if (!item) continue; + + let keys: (string | null)[][] = [["0", "0"]]; + if (item.data.type === "series") { + // TODO sort episodes by season & episode so it shows the "highest" episode as last + const meta = await getMetaFromId(item.data.type, item.data.id); + if (!meta || !meta?.meta.seasons) return; + const seasonNumbers = [ + ...new Set( + oldData.items + .filter((watchedEntry: any) => watchedEntry.mediaId === item.id) + .map((watchedEntry: any) => watchedEntry.seasonId) + ), + ]; + const seasons = seasonNumbers.map((num) => ({ + num, + season: meta.meta?.seasons?.[(num as number) - 1], + })); + keys = seasons + .map((season) => (season ? [season.num, season?.season?.id] : [])) + .filter((entry) => entry.length > 0); + } + + if (!mediaMetas[item.id]) mediaMetas[item.id] = {}; + await Promise.all( + keys.map(async ([key, id]) => { + if (!key) return; + mediaMetas[item.id][key] = await getMetaFromId( + item.data.type, + item.data.id, + id === "0" || id === null ? undefined : id + ); + }) + ); + } + + // We've got all the metadata you can dream of now + // Now let's convert stuff into the new format. + const newData: WatchedStoreData = { + ...oldData, + items: [], + }; + + for (const oldWatched of oldData.items) { + if (oldWatched.mediaType === "movie") { + if (!mediaMetas[oldWatched.mediaId]["0"]?.meta) continue; + + const newItem: WatchedStoreItem = { + item: { + meta: mediaMetas[oldWatched.mediaId]["0"]?.meta as MWMediaMeta, + }, + progress: oldWatched.progress, + percentage: oldWatched.percentage, + watchedAt: Date.now(), // There was no watchedAt in V2 + }; + + oldData.items = oldData.items.filter( + (item) => JSON.stringify(item) !== JSON.stringify(oldWatched) + ); + newData.items.push(newItem); + } else if (oldWatched.mediaType === "series") { + if (!mediaMetas[oldWatched.mediaId][oldWatched.seasonId]?.meta) continue; + + const meta = mediaMetas[oldWatched.mediaId][oldWatched.seasonId] + ?.meta as MWMediaMeta; + + if (meta.type !== "series") return; + + const newItem: WatchedStoreItem = { + item: { + meta, + series: { + episode: Number(oldWatched.episodeId), + season: Number(oldWatched.seasonId), + seasonId: meta.seasonData.id, + episodeId: + meta.seasonData.episodes[Number(oldWatched.episodeId) - 1].id, + }, + }, + progress: oldWatched.progress, + percentage: oldWatched.percentage, + watchedAt: Date.now(), // There was no watchedAt in V2 + // Put watchedAt in the future to show last episode as most recently + }; + + if ( + newData.items.find( + (item) => + item.item.meta.id === newItem.item.meta.id && + item.item.series?.episodeId === newItem.item.series?.episodeId + ) + ) + continue; + + oldData.items = oldData.items.filter( + (item) => JSON.stringify(item) !== JSON.stringify(oldWatched) + ); + newData.items.push(newItem); + } + } + + return newData; +} diff --git a/src/state/watched/store.ts b/src/state/watched/store.ts index 4bab3c78..56fac9ae 100644 --- a/src/state/watched/store.ts +++ b/src/state/watched/store.ts @@ -1,57 +1,25 @@ -import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta"; -import { searchForMedia } from "@/backend/metadata/search"; -import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types"; -import { versionedStoreBuilder } from "@/utils/storage"; -import { WatchedStoreData, WatchedStoreItem } from "./context"; +import { createVersionedStore } from "@/utils/storage"; +import { migrateV2, OldData } from "./migrations/v2"; +import { WatchedStoreData } from "./types"; -interface OldMediaBase { - mediaId: number; - mediaType: MWMediaType; - percentage: number; - progress: number; - providerId: string; - title: string; - year: number; -} - -interface OldMovie extends OldMediaBase { - mediaType: MWMediaType.MOVIE; -} - -interface OldSeries extends OldMediaBase { - mediaType: MWMediaType.SERIES; - episodeId: number; - seasonId: number; -} - -interface OldData { - items: (OldMovie | OldSeries)[]; -} - -export const VideoProgressStore = versionedStoreBuilder() +export const VideoProgressStore = createVersionedStore() .setKey("video-progress") .addVersion({ version: 0, - }) - .addVersion({ - version: 1, migrate() { return { - items: [], + items: [], // dont migrate from version 0 to version 1, unmigratable }; }, }) .addVersion({ - version: 2, - migrate(old: OldData) { - requestAnimationFrame(() => { - // eslint-disable-next-line no-use-before-define - migrateV2(old); - }); - return { - items: [], - }; + version: 1, + async migrate(old: OldData) { + return migrateV2(old); }, + }) + .addVersion({ + version: 2, create() { return { items: [], @@ -59,153 +27,3 @@ export const VideoProgressStore = versionedStoreBuilder() }, }) .build(); - -async function migrateV2(old: OldData) { - const oldData = old; - if (!oldData) return; - - const uniqueMedias: Record = {}; - oldData.items.forEach((item: any) => { - if (uniqueMedias[item.mediaId]) return; - uniqueMedias[item.mediaId] = item; - }); - - const yearsAreClose = (a: number, b: number) => { - return Math.abs(a - b) <= 1; - }; - - const mediaMetas: Record> = {}; - - const relevantItems = await Promise.all( - Object.values(uniqueMedias).map(async (item) => { - const year = Number(item.year.toString().split("-")[0]); - const data = await searchForMedia({ - searchQuery: `${item.title} ${year}`, - type: item.mediaType, - }); - const relevantItem = data.find((res) => - yearsAreClose(Number(res.year), year) - ); - if (!relevantItem) { - console.error("No item"); - return; - } - return { - id: item.mediaId, - data: relevantItem, - }; - }) - ); - - for (const item of relevantItems.filter(Boolean)) { - if (!item) continue; - - let keys: (string | null)[][] = [["0", "0"]]; - if (item.data.type === "series") { - const meta = await getMetaFromId(item.data.type, item.data.id); - if (!meta || !meta?.meta.seasons) return; - const seasonNumbers = [ - ...new Set( - oldData.items - .filter((watchedEntry: any) => watchedEntry.mediaId === item.id) - .map((watchedEntry: any) => watchedEntry.seasonId) - ), - ]; - const seasons = seasonNumbers - .map((num) => ({ - num, - season: meta.meta?.seasons?.[(num as number) - 1], - })) - .filter(Boolean); - keys = seasons - .map((season) => (season ? [season.num, season?.season?.id] : [])) - .filter((entry) => entry.length > 0); // Stupid TypeScript - } - - if (!mediaMetas[item.id]) mediaMetas[item.id] = {}; - await Promise.all( - keys.map(async ([key, id]) => { - if (!key) return; - mediaMetas[item.id][key] = await getMetaFromId( - item.data.type, - item.data.id, - id === "0" || id === null ? undefined : id - ); - }) - ); - } - - // We've got all the metadata you can dream of now - // Now let's convert stuff into the new format. - interface WatchedStoreDataWithVersion extends WatchedStoreData { - "--version": number; - } - const newData: WatchedStoreDataWithVersion = { - ...oldData, - items: [], - "--version": 2, - }; - - for (const oldWatched of oldData.items) { - if (oldWatched.mediaType === "movie") { - if (!mediaMetas[oldWatched.mediaId]["0"]?.meta) continue; - - const newItem: WatchedStoreItem = { - item: { - meta: mediaMetas[oldWatched.mediaId]["0"]?.meta as MWMediaMeta, - }, - progress: oldWatched.progress, - percentage: oldWatched.percentage, - watchedAt: Date.now(), // There was no watchedAt in V2 - }; - - oldData.items = oldData.items.filter( - (item) => JSON.stringify(item) !== JSON.stringify(oldWatched) - ); - newData.items.push(newItem); - } else if (oldWatched.mediaType === "series") { - if (!mediaMetas[oldWatched.mediaId][oldWatched.seasonId]?.meta) continue; - - const meta = mediaMetas[oldWatched.mediaId][oldWatched.seasonId] - ?.meta as MWMediaMeta; - - if (meta.type !== "series") return; - - const newItem: WatchedStoreItem = { - item: { - meta, - series: { - episode: Number(oldWatched.episodeId), - season: Number(oldWatched.seasonId), - seasonId: meta.seasonData.id, - episodeId: - meta.seasonData.episodes[Number(oldWatched.episodeId) - 1].id, - }, - }, - progress: oldWatched.progress, - percentage: oldWatched.percentage, - watchedAt: Date.now(), // There was no watchedAt in V2 - }; - - if ( - newData.items.find( - (item) => - item.item.meta.id === newItem.item.meta.id && - item.item.series?.episodeId === newItem.item.series?.episodeId - ) - ) - continue; - - oldData.items = oldData.items.filter( - (item) => JSON.stringify(item) !== JSON.stringify(oldWatched) - ); - newData.items.push(newItem); - } - } - - console.log(JSON.stringify(old), JSON.stringify(newData)); - if (JSON.stringify(old.items) !== JSON.stringify(newData.items)) { - console.log(newData); - VideoProgressStore.get().save(newData); - } -} diff --git a/src/state/watched/types.ts b/src/state/watched/types.ts new file mode 100644 index 00000000..a3246c38 --- /dev/null +++ b/src/state/watched/types.ts @@ -0,0 +1,22 @@ +import { MWMediaMeta } from "@/backend/metadata/types"; + +export interface StoreMediaItem { + meta: MWMediaMeta; + series?: { + episodeId: string; + seasonId: string; + episode: number; + season: number; + }; +} + +export interface WatchedStoreItem { + item: StoreMediaItem; + progress: number; + percentage: number; + watchedAt: number; +} + +export interface WatchedStoreData { + items: WatchedStoreItem[]; +} diff --git a/src/utils/storage.ts b/src/utils/storage.ts index 9e56e651..b7bed37f 100644 --- a/src/utils/storage.ts +++ b/src/utils/storage.ts @@ -1,232 +1,188 @@ -// TODO make type and react safe!! -/* - it needs to be react-ified by having a save function not on the instance itself. - also type safety is important, this is all spaghetti with "any" everywhere -*/ - -function buildStoreObject(d: any) { - const data: any = { - versions: d.versions, - currentVersion: d.maxVersion, - id: d.storageString, +import { useEffect, useState } from "react"; + +interface StoreVersion { + version: number; + migrate?(data: A): any; + create?: () => A; +} +interface StoreRet { + save: (data: T) => void; + get: () => T; + _raw: () => any; + onChange: (cb: (data: T) => void) => { + destroy: () => void; }; +} - function update(this: any, obj2: any) { - let obj = obj2; - if (!obj) throw new Error("object to update is not an object"); - - // repeat until object fully updated - if (obj["--version"] === undefined) obj["--version"] = 0; - while (obj["--version"] !== this.currentVersion) { - // get version - let version: any = obj["--version"] || 0; - if (version.constructor !== Number || version < 0) version = -42; - // invalid on purpose so it will reset - else { - version = ((version as number) + 1).toString(); - } - - // check if version exists - if (!this.versions[version]) { - console.error( - `Version not found for storage item in store ${this.id}, resetting` - ); - obj = null; - break; - } - - // update object - obj = this.versions[version].update(obj); - } +export interface StoreBuilder { + setKey: (key: string) => StoreBuilder; + addVersion: (ver: StoreVersion) => StoreBuilder; + build: () => StoreRet; +} - // if resulting obj is null, use latest version as init object - if (obj === null) { - console.error( - `Storage item for store ${this.id} has been reset due to faulty updates` - ); - return this.versions[this.currentVersion.toString()].init(); - } +interface InternalStoreData { + versions: StoreVersion[]; + key: string | null; +} - // updates succesful, return - return obj; - } +const storeCallbacks: Record void)[]> = {}; +const stores: Record, InternalStoreData]> = {}; - function get(this: any) { - // get from storage api - const store = this; - let gottenData: any = localStorage.getItem(this.id); - - // parse json if item exists - if (gottenData) { - try { - gottenData = JSON.parse(gottenData); - if (!gottenData.constructor) { - console.error( - `Storage item for store ${this.id} has not constructor` - ); - throw new Error("storage item has no constructor"); - } - if (gottenData.constructor !== Object) { - console.error(`Storage item for store ${this.id} is not an object`); - throw new Error("storage item is not an object"); - } - } catch (_) { - // if errored, set to null so it generates new one, see below - console.error(`Failed to parse storage item for store ${this.id}`); - gottenData = null; - } - } +export async function initializeStores() { + // migrate all stores + for (const [store, internal] of Object.values(stores)) { + const versions = internal.versions.sort((a, b) => a.version - b.version); - // if item doesnt exist, generate from version init - if (!gottenData) { - gottenData = this.versions[this.currentVersion.toString()].init(); + const data = store._raw(); + const dataVersion = + data["--version"] && typeof data["--version"] === "number" + ? data["--version"] + : 0; + + // Find which versions need to be used for migrations + const relevantVersions = versions.filter((v) => v.version >= dataVersion); + + // Migrate over each version + let mostRecentData = data; + for (const version of relevantVersions) { + if (version.migrate) + mostRecentData = await version.migrate(mostRecentData); } - // update the data if needed - gottenData = this.update(gottenData); + store.save(mostRecentData); + } +} - // add a save object to return value - gottenData.save = function save(newData: any) { - const dataToStore = newData || gottenData; - localStorage.setItem(store.id, JSON.stringify(dataToStore)); +function buildStorageObject(store: InternalStoreData): StoreRet { + const key = store.key ?? ""; + const latestVersion = store.versions.sort((a, b) => b.version - a.version)[0]; + + function onChange(cb: (data: T) => void) { + if (!storeCallbacks[key]) storeCallbacks[key] = []; + storeCallbacks[key].push(cb); + return { + destroy() { + // remove function pointer from callbacks + storeCallbacks[key] = storeCallbacks[key].filter((v) => v === cb); + }, }; + } - // add instance helpers - Object.entries(d.instanceHelpers).forEach(([name, helper]: any) => { - if (gottenData[name] !== undefined) - throw new Error( - `helper name: ${name} on instance of store ${this.id} is reserved` - ); - gottenData[name] = helper.bind(gottenData); - }); - - // return data - return gottenData; + function makeRaw() { + const data = latestVersion.create?.() ?? {}; + data["--version"] = latestVersion.version; + return data; } - // add functions to store - data.get = get.bind(data); - data.update = update.bind(data); + function getRaw() { + const item = localStorage.getItem(key); + if (!item) return makeRaw(); + try { + return JSON.parse(item); + } catch (err) { + // we assume user has fucked with the data, give them a fresh store + console.error(`FAILED TO PARSE LOCALSTORAGE FOR KEY ${key}`, err); + return makeRaw(); + } + } - // add static helpers - Object.entries(d.staticHelpers).forEach(([name, helper]: any) => { - if (data[name] !== undefined) - throw new Error(`helper name: ${name} on store ${data.id} is reserved`); - data[name] = helper.bind({}); - }); + function save(data: T) { + const withVersion: any = { ...data }; + withVersion["--version"] = latestVersion.version; + localStorage.setItem(key, JSON.stringify(withVersion)); - return data; -} + if (!storeCallbacks[key]) storeCallbacks[key] = []; + storeCallbacks[key].forEach((v) => v(structuredClone(data))); + } -/* - * Builds a versioned store - * - * manages versioning of localstorage items - */ -export function versionedStoreBuilder(): any { return { - _data: { - versionList: [], - maxVersion: 0, - versions: {}, - storageString: undefined, - instanceHelpers: {}, - staticHelpers: {}, + get() { + const data = getRaw(); + delete data["--version"]; + return data as T; }, - - setKey(str: string) { - this._data.storageString = str; - return this; + _raw() { + return getRaw(); }, + onChange, + save, + }; +} - addVersion({ version, migrate, create }: any) { - // input checking - if (version < 0) throw new Error("Cannot add version below 0 in store"); - if (version > 0 && !migrate) - throw new Error( - `Missing migration on version ${version} (needed for any version above 0)` - ); - - // update max version list - if (version > this._data.maxVersion) this._data.maxVersion = version; - // add to version list - this._data.versionList.push(version); - - // register version - this._data.versions[version.toString()] = { - version, // version number - update: migrate - ? (data: any) => { - // update function, and increment version - const newData = migrate(data); - newData["--version"] = version; // eslint-disable-line no-param-reassign - return newData; - } - : undefined, - init: create - ? () => { - // return an initial object - const data = create(); - data["--version"] = version; - return data; - } - : undefined, - }; - return this; - }, +function assertStore(store: InternalStoreData) { + const versionListSorted = store.versions.sort( + (a, b) => a.version - b.version + ); + versionListSorted.forEach((v, i, arr) => { + if (i === 0) return; + if (v.version !== arr[i - 1].version + 1) + throw new Error("Version list of store is not incremental"); + }); + versionListSorted.forEach((v) => { + if (v.version < 0) throw new Error("Versions cannot be negative"); + }); + + // version zero must exist + if (versionListSorted[0]?.version !== 0) + throw new Error("Version 0 doesn't exist in version list of store"); - registerHelper({ name, helper, type }: any) { - // type - let helperType: string = type; - if (!helperType) helperType = "instance"; - - // input checking - if (!name || name.constructor !== String) { - throw new Error("helper name is not a string"); - } - if (!helper || helper.constructor !== Function) { - throw new Error("helper function is not a function"); - } - if (!["instance", "static"].includes(helperType)) { - throw new Error("helper type must be either 'instance' or 'static'"); - } - - // register helper - if (helperType === "instance") - this._data.instanceHelpers[name as string] = helper; - else if (helperType === "static") - this._data.staticHelpers[name as string] = helper; + // max version must have create function + if (!store.versions[store.versions.length - 1].create) + throw new Error(`Missing create function on latest version of store`); + + // check storage string + if (!store.key) throw new Error("storage key not set in store"); + + // check if all parts have migratio + const migrations = [...versionListSorted]; + migrations.pop(); + migrations.forEach((v) => { + if (!v.migrate) + throw new Error(`Migration missing on version ${v.version}`); + }); +} + +export function createVersionedStore(): StoreBuilder { + const _data: InternalStoreData = { + versions: [], + key: null, + }; + return { + setKey(key) { + _data.key = key; + return this; + }, + addVersion(ver) { + _data.versions.push(ver); return this; }, - build() { - // check if version list doesnt skip versions - const versionListSorted = this._data.versionList.sort( - (a: number, b: number) => a - b - ); - versionListSorted.forEach((v: any, i: number, arr: any[]) => { - if (i === 0) return; - if (v !== arr[i - 1] + 1) - throw new Error("Version list of store is not incremental"); - }); - - // version zero must exist - if (versionListSorted[0] !== 0) - throw new Error("Version 0 doesn't exist in version list of store"); - - // max version must have init function - if (!this._data.versions[this._data.maxVersion.toString()].init) - throw new Error( - `Missing create function on version ${this._data.maxVersion} (needed for latest version of store)` - ); - - // check storage string - if (!this._data.storageString) - throw new Error("storage key not set in store"); - - // build versioned store - return buildStoreObject(this._data); + assertStore(_data); + const storageObject = buildStorageObject(_data); + stores[_data.key ?? ""] = [storageObject, _data]; + return storageObject; }, }; } + +export function useStore( + store: StoreRet +): [T, (cb: (old: T) => T) => void] { + const [data, setData] = useState(store.get()); + useEffect(() => { + const { destroy } = store.onChange((newData) => { + setData(newData); + }); + return () => { + destroy(); + }; + }, [store]); + + function setNewData(cb: (old: T) => T) { + const newData = cb(data); + store.save(newData); + } + + return [data, setNewData]; +} diff --git a/src/video/components/hooks/volumeStore.ts b/src/video/components/hooks/volumeStore.ts index 3b328810..e577c09d 100644 --- a/src/video/components/hooks/volumeStore.ts +++ b/src/video/components/hooks/volumeStore.ts @@ -1,6 +1,10 @@ -import { versionedStoreBuilder } from "@/utils/storage"; +import { createVersionedStore } from "@/utils/storage"; -export const volumeStore = versionedStoreBuilder() +interface VolumeStoreData { + volume: number; +} + +export const volumeStore = createVersionedStore() .setKey("mw-volume") .addVersion({ version: 0, @@ -18,8 +22,7 @@ export function getStoredVolume(): number { } export function setStoredVolume(volume: number) { - const store = volumeStore.get(); - store.save({ + volumeStore.save({ volume, }); } From dcc158e7052857ed12503b0f021aafbd8749a381 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sun, 12 Feb 2023 13:06:30 +0100 Subject: [PATCH 108/135] source reset bug fixes (HLS fix & volume fix) --- src/video/state/init.ts | 2 +- src/video/state/providers/videoStateProvider.ts | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/video/state/init.ts b/src/video/state/init.ts index 438841e4..c13021b7 100644 --- a/src/video/state/init.ts +++ b/src/video/state/init.ts @@ -12,7 +12,7 @@ export function resetForSource(s: VideoPlayerState) { isDragSeeking: false, isFirstLoading: true, hasPlayedOnce: false, - volume: 0, + volume: state.mediaPlaying.volume, // volume settings needs to persist through resets }; state.progress = { time: 0, diff --git a/src/video/state/providers/videoStateProvider.ts b/src/video/state/providers/videoStateProvider.ts index eb88f5df..9791f53f 100644 --- a/src/video/state/providers/videoStateProvider.ts +++ b/src/video/state/providers/videoStateProvider.ts @@ -129,18 +129,22 @@ export function createVideoStateProvider( }, setSource(source) { if (!source) { + resetStateForSource(descriptor, state); player.src = ""; state.source = null; - resetStateForSource(descriptor, state); updateSource(descriptor, state); return; } + // reset before assign new one so the old HLS instance gets destroyed + resetStateForSource(descriptor, state); + if (source?.type === MWStreamType.HLS) { if (player.canPlayType("application/vnd.apple.mpegurl")) { + // HLS supported natively by browser player.src = source.source; } else { - // HLS support + // HLS through HLS.js if (!Hls.isSupported()) { state.error = { name: `Not supported`, @@ -168,6 +172,7 @@ export function createVideoStateProvider( hls.loadSource(source.source); } } else if (source.type === MWStreamType.MP4) { + // standard MP4 stream player.src = source.source; } @@ -178,7 +183,6 @@ export function createVideoStateProvider( url: source.source, caption: null, }; - resetStateForSource(descriptor, state); updateSource(descriptor, state); }, setCaption(id, url) { From e569f15661957dc0a5b347ce4e69629a054d878e Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sun, 12 Feb 2023 13:16:18 +0100 Subject: [PATCH 109/135] fix restoring of time when changing source --- .../controllers/ProgressListenerController.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/video/components/controllers/ProgressListenerController.tsx b/src/video/components/controllers/ProgressListenerController.tsx index 27434bd5..2739f412 100644 --- a/src/video/components/controllers/ProgressListenerController.tsx +++ b/src/video/components/controllers/ProgressListenerController.tsx @@ -16,10 +16,15 @@ export function ProgressListenerController(props: Props) { const progress = useProgress(descriptor); const controls = useControls(descriptor); const didInitialize = useRef(null); + const lastTime = useRef(props.startAt ?? 0); // time updates (throttled) const updateTime = useMemo( - () => throttle((a: number, b: number) => props.onProgress?.(a, b), 1000), + () => + throttle((a: number, b: number) => { + lastTime.current = a; + props.onProgress?.(a, b); + }, 1000), [props] ); useEffect(() => { @@ -37,11 +42,16 @@ export function ProgressListenerController(props: Props) { useEffect(() => { if (didInitialize.current) return; if (mediaPlaying.isFirstLoading || Number.isNaN(progress.duration)) return; - if (props.startAt !== undefined) { - controls.setTime(props.startAt); - } + controls.setTime(lastTime.current); didInitialize.current = true; }, [didInitialize, props, progress, mediaPlaying, controls]); + useEffect(() => { + // if it initialized, but media starts loading for the first time again. + // reset initalized so it will restore time again + if (didInitialize.current && mediaPlaying.isFirstLoading) + didInitialize.current = null; + }, [mediaPlaying]); + return null; } From bd48d929b9b15a5990a0496155c93437b4b0db9a Mon Sep 17 00:00:00 2001 From: Jip Fr Date: Sun, 12 Feb 2023 14:03:50 +0100 Subject: [PATCH 110/135] Migrate bookmarks from v2 > v3 --- src/state/bookmark/store.ts | 8 ++-- src/state/watched/migrations/v2.ts | 65 ++++++++++++++++++++++++------ src/state/watched/store.ts | 4 +- 3 files changed, 58 insertions(+), 19 deletions(-) diff --git a/src/state/bookmark/store.ts b/src/state/bookmark/store.ts index 3b7a3a92..5be45812 100644 --- a/src/state/bookmark/store.ts +++ b/src/state/bookmark/store.ts @@ -1,14 +1,14 @@ import { createVersionedStore } from "@/utils/storage"; +import { migrateV1Bookmarks } from "../watched/migrations/v2"; import { BookmarkStoreData } from "./types"; export const BookmarkStore = createVersionedStore() .setKey("mw-bookmarks") .addVersion({ version: 0, - migrate() { - return { - bookmarks: [], // TODO migrate bookmarks - }; + migrate(oldBookmarks) { + console.log(oldBookmarks); + return migrateV1Bookmarks(oldBookmarks); }, }) .addVersion({ diff --git a/src/state/watched/migrations/v2.ts b/src/state/watched/migrations/v2.ts index ff0115e6..c079b32d 100644 --- a/src/state/watched/migrations/v2.ts +++ b/src/state/watched/migrations/v2.ts @@ -27,16 +27,14 @@ export interface OldData { items: (OldMovie | OldSeries)[]; } -export async function migrateV2(old: OldData) { - const oldData = old; - if (!oldData) return; - - const uniqueMedias: Record = {}; - oldData.items.forEach((item: any) => { - if (uniqueMedias[item.mediaId]) return; - uniqueMedias[item.mediaId] = item; - }); +interface OldBookmarks { + bookmarks: (OldMovie | OldSeries)[]; +} +async function getMetas( + uniqueMedias: Record, + oldData?: OldData +): Promise> | undefined> { const yearsAreClose = (a: number, b: number) => { return Math.abs(a - b) <= 1; }; @@ -74,14 +72,16 @@ export async function migrateV2(old: OldData) { if (!meta || !meta?.meta.seasons) return; const seasonNumbers = [ ...new Set( - oldData.items - .filter((watchedEntry: any) => watchedEntry.mediaId === item.id) - .map((watchedEntry: any) => watchedEntry.seasonId) + oldData?.items + ? oldData.items + .filter((watchedEntry: any) => watchedEntry.mediaId === item.id) + .map((watchedEntry: any) => watchedEntry.seasonId) + : ["0"] ), ]; const seasons = seasonNumbers.map((num) => ({ num, - season: meta.meta?.seasons?.[(num as number) - 1], + season: meta.meta?.seasons?.[Math.max(0, (num as number) - 1)], })); keys = seasons .map((season) => (season ? [season.num, season?.season?.id] : [])) @@ -101,6 +101,45 @@ export async function migrateV2(old: OldData) { ); } + return mediaMetas; +} + +export async function migrateV1Bookmarks(old: OldBookmarks) { + const oldData = old; + if (!oldData) return; + + const uniqueMedias: Record = {}; + oldData.bookmarks.forEach((item: any) => { + if (uniqueMedias[item.mediaId]) return; + uniqueMedias[item.mediaId] = item; + }); + + const mediaMetas = await getMetas(uniqueMedias); + if (!mediaMetas) return; + + const bookmarks = Object.keys(mediaMetas) + .map((key) => mediaMetas[key]["0"]) + .map((t) => t?.meta) + .filter(Boolean); + + return { + bookmarks, + }; +} + +export async function migrateV2Videos(old: OldData) { + const oldData = old; + if (!oldData) return; + + const uniqueMedias: Record = {}; + oldData.items.forEach((item: any) => { + if (uniqueMedias[item.mediaId]) return; + uniqueMedias[item.mediaId] = item; + }); + + const mediaMetas = await getMetas(uniqueMedias, oldData); + if (!mediaMetas) return; + // We've got all the metadata you can dream of now // Now let's convert stuff into the new format. const newData: WatchedStoreData = { diff --git a/src/state/watched/store.ts b/src/state/watched/store.ts index 56fac9ae..84eefd67 100644 --- a/src/state/watched/store.ts +++ b/src/state/watched/store.ts @@ -1,5 +1,5 @@ import { createVersionedStore } from "@/utils/storage"; -import { migrateV2, OldData } from "./migrations/v2"; +import { migrateV2Videos, OldData } from "./migrations/v2"; import { WatchedStoreData } from "./types"; export const VideoProgressStore = createVersionedStore() @@ -15,7 +15,7 @@ export const VideoProgressStore = createVersionedStore() .addVersion({ version: 1, async migrate(old: OldData) { - return migrateV2(old); + return migrateV2Videos(old); }, }) .addVersion({ From 424ec25c5ac254df102a98158655bb63631b2d61 Mon Sep 17 00:00:00 2001 From: Jip Fr Date: Sun, 12 Feb 2023 14:04:21 +0100 Subject: [PATCH 111/135] Add OldBookmarks type to migration wrapper --- src/state/bookmark/store.ts | 5 ++--- src/state/watched/migrations/v2.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/state/bookmark/store.ts b/src/state/bookmark/store.ts index 5be45812..11bec1c5 100644 --- a/src/state/bookmark/store.ts +++ b/src/state/bookmark/store.ts @@ -1,13 +1,12 @@ import { createVersionedStore } from "@/utils/storage"; -import { migrateV1Bookmarks } from "../watched/migrations/v2"; +import { migrateV1Bookmarks, OldBookmarks } from "../watched/migrations/v2"; import { BookmarkStoreData } from "./types"; export const BookmarkStore = createVersionedStore() .setKey("mw-bookmarks") .addVersion({ version: 0, - migrate(oldBookmarks) { - console.log(oldBookmarks); + migrate(oldBookmarks: OldBookmarks) { return migrateV1Bookmarks(oldBookmarks); }, }) diff --git a/src/state/watched/migrations/v2.ts b/src/state/watched/migrations/v2.ts index c079b32d..946334e8 100644 --- a/src/state/watched/migrations/v2.ts +++ b/src/state/watched/migrations/v2.ts @@ -27,7 +27,7 @@ export interface OldData { items: (OldMovie | OldSeries)[]; } -interface OldBookmarks { +export interface OldBookmarks { bookmarks: (OldMovie | OldSeries)[]; } From 4a0392d1f004295ca5cfbfc1929e08120d61f9e4 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sun, 12 Feb 2023 15:58:11 +0100 Subject: [PATCH 112/135] chromecasting humble beginnings Co-authored-by: James Hawkins --- .eslintrc.js | 1 + src/components/Icon.tsx | 20 +- src/components/layout/Spinner.tsx | 4 +- src/components/media/MediaCard.tsx | 2 +- src/video/components/VideoPlayer.tsx | 6 +- src/video/components/VideoPlayerBase.tsx | 2 + .../components/actions/ChromecastAction.tsx | 12 + .../components/actions/LoadingAction.tsx | 5 +- .../components/internal/CastingInternal.tsx | 65 +++++ .../internal/VideoElementInternal.tsx | 16 +- .../components/parts/VideoPlayerHeader.tsx | 7 +- src/video/state/init.ts | 8 + src/video/state/logic/controls.ts | 3 + src/video/state/logic/misc.ts | 4 + .../state/providers/castingStateProvider.ts | 236 ++++++++++++++++++ src/video/state/providers/providerTypes.ts | 1 + src/video/state/providers/utils.ts | 15 +- .../state/providers/videoStateProvider.ts | 13 +- src/video/state/types.ts | 9 + 19 files changed, 414 insertions(+), 15 deletions(-) create mode 100644 src/video/components/actions/ChromecastAction.tsx create mode 100644 src/video/components/internal/CastingInternal.tsx create mode 100644 src/video/state/providers/castingStateProvider.ts diff --git a/.eslintrc.js b/.eslintrc.js index 5feb5ad4..1556850a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -49,6 +49,7 @@ module.exports = { "no-eval": "off", "no-await-in-loop": "off", "no-nested-ternary": "off", + "prefer-destructuring": "off", "react/jsx-filename-extension": [ "error", { extensions: [".js", ".tsx", ".jsx"] } diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 9b88a83a..bf0a0ae2 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -1,4 +1,4 @@ -import { memo } from "react"; +import { memo, useEffect, useRef } from "react"; export enum Icons { SEARCH = "search", @@ -33,6 +33,7 @@ export enum Icons { FILE = "file", CAPTIONS = "captions", LINK = "link", + CASTING = "casting", } export interface IconProps { @@ -73,9 +74,26 @@ const iconList: Record = { file: ``, captions: ``, link: ``, + casting: "", }; +function ChromeCastButton() { + const ref = useRef(null); + + useEffect(() => { + const tag = document.createElement("google-cast-launcher"); + tag.setAttribute("id", "castbutton"); + ref.current?.appendChild(tag); + }, []); + + return
; +} + export const Icon = memo((props: IconProps) => { + if (props.icon === Icons.CASTING) { + return ; + } + return ( ; + return
; } diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 6a19d88b..42d76eb9 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -43,7 +43,7 @@ function MediaCardContent({ }`} >
- {/* */} +
@@ -149,9 +150,8 @@ export function VideoPlayer(props: Props) { - {/* */}
- {/* */} + diff --git a/src/video/components/VideoPlayerBase.tsx b/src/video/components/VideoPlayerBase.tsx index d0656e40..1f1fbba7 100644 --- a/src/video/components/VideoPlayerBase.tsx +++ b/src/video/components/VideoPlayerBase.tsx @@ -1,3 +1,4 @@ +import { CastingInternal } from "@/video/components/internal/CastingInternal"; import { WrapperRegisterInternal } from "@/video/components/internal/WrapperRegisterInternal"; import { VideoErrorBoundary } from "@/video/components/parts/VideoErrorBoundary"; import { useInterface } from "@/video/state/logic/interface"; @@ -42,6 +43,7 @@ function VideoPlayerBaseWithState(props: VideoPlayerBaseProps) { ].join(" ")} > +
{children}
diff --git a/src/video/components/actions/ChromecastAction.tsx b/src/video/components/actions/ChromecastAction.tsx new file mode 100644 index 00000000..123a6130 --- /dev/null +++ b/src/video/components/actions/ChromecastAction.tsx @@ -0,0 +1,12 @@ +import { Icons } from "@/components/Icon"; +import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconButton"; + +interface Props { + className?: string; +} + +export function ChromecastAction(props: Props) { + return ( + + ); +} diff --git a/src/video/components/actions/LoadingAction.tsx b/src/video/components/actions/LoadingAction.tsx index 0bbe6072..ce46356d 100644 --- a/src/video/components/actions/LoadingAction.tsx +++ b/src/video/components/actions/LoadingAction.tsx @@ -1,14 +1,17 @@ import { Spinner } from "@/components/layout/Spinner"; import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useMediaPlaying } from "@/video/state/logic/mediaplaying"; +import { useMisc } from "@/video/state/logic/misc"; export function LoadingAction() { const descriptor = useVideoPlayerDescriptor(); const mediaPlaying = useMediaPlaying(descriptor); + const misc = useMisc(descriptor); const isLoading = mediaPlaying.isFirstLoading || mediaPlaying.isLoading; + const shouldShow = !misc.isCasting; - if (!isLoading) return null; + if (!isLoading || !shouldShow) return null; return ; } diff --git a/src/video/components/internal/CastingInternal.tsx b/src/video/components/internal/CastingInternal.tsx new file mode 100644 index 00000000..72979083 --- /dev/null +++ b/src/video/components/internal/CastingInternal.tsx @@ -0,0 +1,65 @@ +import { useChromecastAvailable } from "@/hooks/useChromecastAvailable"; +import { getPlayerState } from "@/video/state/cache"; +import { useVideoPlayerDescriptor } from "@/video/state/hooks"; +import { updateMisc, useMisc } from "@/video/state/logic/misc"; +import { createCastingStateProvider } from "@/video/state/providers/castingStateProvider"; +import { setProvider, unsetStateProvider } from "@/video/state/providers/utils"; +import { useEffect, useMemo, useRef } from "react"; + +export function CastingInternal() { + const descriptor = useVideoPlayerDescriptor(); + const misc = useMisc(descriptor); + const lastValue = useRef(false); + const available = useChromecastAvailable(); + + const isCasting = useMemo(() => misc.isCasting, [misc]); + + useEffect(() => { + if (lastValue.current === isCasting) return; + if (!isCasting) return; + lastValue.current = isCasting; + const provider = createCastingStateProvider(descriptor); + setProvider(descriptor, provider); + const { destroy } = provider.providerStart(); + return () => { + unsetStateProvider(descriptor, provider.getId()); + destroy(); + }; + }, [descriptor, isCasting]); + + useEffect(() => { + const state = getPlayerState(descriptor); + if (!available) return; + + state.casting.instance = cast.framework.CastContext.getInstance(); + state.casting.instance.setOptions({ + receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID, + autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED, + }); + + state.casting.player = new cast.framework.RemotePlayer(); + state.casting.controller = new cast.framework.RemotePlayerController( + state.casting.player + ); + + function connectionChanged(e: cast.framework.RemotePlayerChangedEvent) { + if (e.field === "isConnected") { + state.casting.isCasting = e.value; + updateMisc(descriptor, state); + } + } + state.casting.controller.addEventListener( + cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED, + connectionChanged + ); + + return () => { + state.casting.controller?.removeEventListener( + cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED, + connectionChanged + ); + }; + }, [available, descriptor]); + + return null; +} diff --git a/src/video/components/internal/VideoElementInternal.tsx b/src/video/components/internal/VideoElementInternal.tsx index 2236db04..f819bf8f 100644 --- a/src/video/components/internal/VideoElementInternal.tsx +++ b/src/video/components/internal/VideoElementInternal.tsx @@ -10,7 +10,7 @@ interface Props { autoPlay?: boolean; } -export function VideoElementInternal(props: Props) { +function VideoElement(props: Props) { const descriptor = useVideoPlayerDescriptor(); const mediaPlaying = useMediaPlaying(descriptor); const source = useSource(descriptor); @@ -18,6 +18,7 @@ export function VideoElementInternal(props: Props) { const ref = useRef(null); const initalized = useMemo(() => !!misc.wrapperInitialized, [misc]); + const stateProviderId = useMemo(() => misc.stateProviderId, [misc]); useEffect(() => { if (!initalized) return; @@ -26,10 +27,10 @@ export function VideoElementInternal(props: Props) { setProvider(descriptor, provider); const { destroy } = provider.providerStart(); return () => { - unsetStateProvider(descriptor); + unsetStateProvider(descriptor, provider.getId()); destroy(); }; - }, [descriptor, initalized]); + }, [descriptor, initalized, stateProviderId]); // this element is remotely controlled by a state provider return ( @@ -46,3 +47,12 @@ export function VideoElementInternal(props: Props) { ); } + +export function VideoElementInternal(props: Props) { + const descriptor = useVideoPlayerDescriptor(); + const misc = useMisc(descriptor); + + // this element is remotely controlled by a state provider + if (misc.stateProviderId !== "video") return null; + return ; +} diff --git a/src/video/components/parts/VideoPlayerHeader.tsx b/src/video/components/parts/VideoPlayerHeader.tsx index 3eece234..771be45a 100644 --- a/src/video/components/parts/VideoPlayerHeader.tsx +++ b/src/video/components/parts/VideoPlayerHeader.tsx @@ -7,6 +7,7 @@ import { useBookmarkContext, } from "@/state/bookmark"; import { AirplayAction } from "@/video/components/actions/AirplayAction"; +import { ChromecastAction } from "@/video/components/actions/ChromecastAction"; interface VideoPlayerHeaderProps { media?: MWMediaMeta; @@ -55,9 +56,11 @@ export function VideoPlayerHeader(props: VideoPlayerHeaderProps) { )}
{props.showControls ? ( - + <> + + + ) : ( - // chromecontrol )}
diff --git a/src/video/state/init.ts b/src/video/state/init.ts index c13021b7..13118a1d 100644 --- a/src/video/state/init.ts +++ b/src/video/state/init.ts @@ -51,12 +51,20 @@ function initPlayer(): VideoPlayerState { draggingTime: 0, }, + casting: { + isCasting: false, + controller: null, + instance: null, + player: null, + }, + meta: null, source: null, error: null, canAirplay: false, initalized: false, + stateProviderId: "video", pausedWhenSeeking: false, hlsInstance: null, diff --git a/src/video/state/logic/controls.ts b/src/video/state/logic/controls.ts index b4367526..56c89a10 100644 --- a/src/video/state/logic/controls.ts +++ b/src/video/state/logic/controls.ts @@ -22,6 +22,9 @@ export function useControls( return { // state provider controls + getId() { + return state.stateProvider?.getId() ?? ""; + }, pause() { state.stateProvider?.pause(); }, diff --git a/src/video/state/logic/misc.ts b/src/video/state/logic/misc.ts index 97c26855..a30a146a 100644 --- a/src/video/state/logic/misc.ts +++ b/src/video/state/logic/misc.ts @@ -7,6 +7,8 @@ export type VideoMiscError = { canAirplay: boolean; wrapperInitialized: boolean; initalized: boolean; + isCasting: boolean; + stateProviderId: string; }; function getMiscFromState(state: VideoPlayerState): VideoMiscError { @@ -14,6 +16,8 @@ function getMiscFromState(state: VideoPlayerState): VideoMiscError { canAirplay: state.canAirplay, wrapperInitialized: !!state.wrapperElement, initalized: state.initalized, + isCasting: state.casting.isCasting, + stateProviderId: state.stateProviderId, }; } diff --git a/src/video/state/providers/castingStateProvider.ts b/src/video/state/providers/castingStateProvider.ts new file mode 100644 index 00000000..69806a39 --- /dev/null +++ b/src/video/state/providers/castingStateProvider.ts @@ -0,0 +1,236 @@ +import fscreen from "fscreen"; +import { + canChangeVolume, + canFullscreen, + canFullscreenAnyElement, + canWebkitFullscreen, +} from "@/utils/detectFeatures"; +import { updateSource } from "@/video/state/logic/source"; +import { + getStoredVolume, + setStoredVolume, +} from "@/video/components/hooks/volumeStore"; +import { resetStateForSource } from "@/video/state/providers/helpers"; +import { updateInterface } from "@/video/state/logic/interface"; +import { getPlayerState } from "../cache"; +import { updateMediaPlaying } from "../logic/mediaplaying"; +import { VideoPlayerStateProvider } from "./providerTypes"; +import { updateProgress } from "../logic/progress"; + +// TODO startAt when switching state providers +// TODO cast -> uncast -> cast will break +// TODO chromecast button has incorrect hitbox and badly styled +// TODO casting text middle of screen +export function createCastingStateProvider( + descriptor: string +): VideoPlayerStateProvider { + const state = getPlayerState(descriptor); + const ins = state.casting.instance; + const player = state.casting.player; + const controller = state.casting.controller; + + return { + getId() { + return "casting"; + }, + play() { + if (state.mediaPlaying.isPaused) controller?.playOrPause(); + }, + pause() { + if (state.mediaPlaying.isPlaying) controller?.playOrPause(); + }, + exitFullscreen() { + if (!fscreen.fullscreenElement) return; + fscreen.exitFullscreen(); + }, + enterFullscreen() { + if (!canFullscreen() || fscreen.fullscreenElement) return; + if (canFullscreenAnyElement()) { + if (state.wrapperElement) + fscreen.requestFullscreen(state.wrapperElement); + return; + } + if (canWebkitFullscreen()) { + (player as any).webkitEnterFullscreen(); + } + }, + startAirplay() { + // no airplay while casting + }, + setTime(t) { + // clamp time between 0 and max duration + let time = Math.min(t, player?.duration ?? 0); + time = Math.max(0, time); + + if (Number.isNaN(time)) return; + + // update state + if (player) player.currentTime = time; + state.progress.time = time; + controller?.seek(); + updateProgress(descriptor, state); + }, + setSeeking(active) { + state.mediaPlaying.isSeeking = active; + state.mediaPlaying.isDragSeeking = active; + updateMediaPlaying(descriptor, state); + + // if it was playing when starting to seek, play again + if (!active) { + if (!state.pausedWhenSeeking) this.play(); + return; + } + + // when seeking we pause the video + // this variables isnt reactive, just used so the state can be remembered next unseek + state.pausedWhenSeeking = state.mediaPlaying.isPaused; + this.pause(); + }, + async setVolume(v) { + // clamp time between 0 and 1 + let volume = Math.min(v, 1); + volume = Math.max(0, volume); + + // update state + if ((await canChangeVolume()) && player) player.volumeLevel = volume; + state.mediaPlaying.volume = volume; + controller?.setVolumeLevel(); + updateMediaPlaying(descriptor, state); + + // update localstorage + setStoredVolume(volume); + }, + setSource(source) { + if (!source) { + resetStateForSource(descriptor, state); + controller?.stop(); + state.source = null; + updateSource(descriptor, state); + return; + } + + const movieMeta = new chrome.cast.media.MovieMediaMetadata(); + movieMeta.title = state.meta?.meta.title ?? ""; + + // TODO contentId? + const mediaInfo = new chrome.cast.media.MediaInfo("hello", "video/mp4"); + (mediaInfo as any).contentUrl = source?.source; + mediaInfo.streamType = chrome.cast.media.StreamType.BUFFERED; + mediaInfo.metadata = movieMeta; + + const request = new chrome.cast.media.LoadRequest(mediaInfo); + request.autoplay = true; + + const session = ins?.getCurrentSession(); + session?.loadMedia(request); + + // update state + state.source = { + quality: source.quality, + type: source.type, + url: source.source, + caption: null, + }; + resetStateForSource(descriptor, state); + updateSource(descriptor, state); + }, + setCaption(id, url) { + if (state.source) { + state.source.caption = { + id, + url, + }; + updateSource(descriptor, state); + } + }, + clearCaption() { + if (state.source) { + state.source.caption = null; + updateSource(descriptor, state); + } + }, + providerStart() { + this.setVolume(getStoredVolume()); + + const listenToEvents = async ( + e: cast.framework.RemotePlayerChangedEvent + ) => { + switch (e.field) { + case "volumeLevel": + if (await canChangeVolume()) { + state.mediaPlaying.volume = e.value; + updateMediaPlaying(descriptor, state); + } + break; + case "currentTime": + state.progress.time = e.value; + updateProgress(descriptor, state); + break; + case "mediaInfo": + state.progress.duration = e.value.duration; + updateProgress(descriptor, state); + break; + case "playerState": + state.mediaPlaying.isLoading = e.value === "BUFFERING"; + updateMediaPlaying(descriptor, state); + break; + case "isPaused": + state.mediaPlaying.isPaused = e.value; + state.mediaPlaying.isPlaying = !e.value; + if (!e.value) state.mediaPlaying.hasPlayedOnce = true; + updateMediaPlaying(descriptor, state); + break; + case "isMuted": + state.mediaPlaying.volume = e.value ? 1 : 0; + // TODO better mute handling + updateMediaPlaying(descriptor, state); + break; + case "displayStatus": + case "canSeek": + case "title": + break; + default: + console.log(e.type, e.field, e.value); + break; + } + }; + const fullscreenchange = () => { + state.interface.isFullscreen = !!document.fullscreenElement; + updateInterface(descriptor, state); + }; + const isFocused = (evt: any) => { + state.interface.isFocused = evt.type !== "mouseleave"; + updateInterface(descriptor, state); + }; + + controller?.addEventListener( + cast.framework.RemotePlayerEventType.ANY_CHANGE, + listenToEvents + ); + state.wrapperElement?.addEventListener("click", isFocused); + state.wrapperElement?.addEventListener("mouseenter", isFocused); + state.wrapperElement?.addEventListener("mouseleave", isFocused); + fscreen.addEventListener("fullscreenchange", fullscreenchange); + + if (state.source) + this.setSource({ + quality: state.source.quality, + source: state.source.url, + type: state.source.type, + }); + + return { + destroy: () => { + controller?.removeEventListener( + cast.framework.RemotePlayerEventType.ANY_CHANGE, + listenToEvents + ); + state.wrapperElement?.removeEventListener("click", isFocused); + state.wrapperElement?.removeEventListener("mouseenter", isFocused); + state.wrapperElement?.removeEventListener("mouseleave", isFocused); + fscreen.removeEventListener("fullscreenchange", fullscreenchange); + }, + }; + }, + }; +} diff --git a/src/video/state/providers/providerTypes.ts b/src/video/state/providers/providerTypes.ts index c3024e7c..e34b950d 100644 --- a/src/video/state/providers/providerTypes.ts +++ b/src/video/state/providers/providerTypes.ts @@ -18,6 +18,7 @@ export type VideoPlayerStateController = { startAirplay(): void; setCaption(id: string, url: string): void; clearCaption(): void; + getId(): string; }; export type VideoPlayerStateProvider = VideoPlayerStateController & { diff --git a/src/video/state/providers/utils.ts b/src/video/state/providers/utils.ts index 51c41a65..273d9fff 100644 --- a/src/video/state/providers/utils.ts +++ b/src/video/state/providers/utils.ts @@ -9,15 +9,28 @@ export function setProvider( const state = getPlayerState(descriptor); state.stateProvider = provider; state.initalized = true; + state.stateProviderId = provider.getId(); updateMisc(descriptor, state); } /** * Note: This only sets the state provider to null. it does not destroy the listener */ -export function unsetStateProvider(descriptor: string) { +export function unsetStateProvider( + descriptor: string, + stateProviderId: string +) { const state = getPlayerState(descriptor); + // dont do anything if state provider doesnt match the thing to unset + if ( + !state.stateProvider || + state.stateProvider?.getId() !== stateProviderId + ) { + state.stateProviderId = "video"; // go back to video when casting stops + return; + } state.stateProvider = null; + state.stateProviderId = "video"; // go back to video when casting stops } export function handleBuffered(time: number, buffered: TimeRanges): number { diff --git a/src/video/state/providers/videoStateProvider.ts b/src/video/state/providers/videoStateProvider.ts index 9791f53f..9788bda7 100644 --- a/src/video/state/providers/videoStateProvider.ts +++ b/src/video/state/providers/videoStateProvider.ts @@ -60,6 +60,9 @@ export function createVideoStateProvider( const state = getPlayerState(descriptor); return { + getId() { + return "video"; + }, play() { player.play(); }, @@ -130,7 +133,8 @@ export function createVideoStateProvider( setSource(source) { if (!source) { resetStateForSource(descriptor, state); - player.src = ""; + player.removeAttribute("src"); + player.load(); state.source = null; updateSource(descriptor, state); return; @@ -302,6 +306,13 @@ export function createVideoStateProvider( canAirplay ); + if (state.source) + this.setSource({ + quality: state.source.quality, + source: state.source.url, + type: state.source.type, + }); + return { destroy: () => { player.removeEventListener("pause", pause); diff --git a/src/video/state/types.ts b/src/video/state/types.ts index 3ca48aa9..ff378c52 100644 --- a/src/video/state/types.ts +++ b/src/video/state/types.ts @@ -64,9 +64,18 @@ export type VideoPlayerState = { }; }; + // casting state + casting: { + isCasting: boolean; + controller: cast.framework.RemotePlayerController | null; + player: cast.framework.RemotePlayer | null; + instance: cast.framework.CastContext | null; + }; + // misc canAirplay: boolean; initalized: boolean; + stateProviderId: string; error: null | { name: string; description: string; From bf3bca9b53bc3e5a2f26ca03ee014c44004eb71c Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sun, 12 Feb 2023 16:03:39 +0100 Subject: [PATCH 113/135] update image links in readme --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 813cfc95..d554c5f6 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@

movie-web

-GitHub Workflow Status -GitHub license -GitHub forks -GitHub stars
+GitHub Workflow Status +GitHub license +GitHub forks +GitHub stars
Discord Server

@@ -37,7 +37,7 @@ To run this project locally for contributing or testing, run the following comma
note: must use yarn to install packages and run NodeJS 16
```bash -git clone https://github.com/JamesHawkinss/movie-web +git clone https://github.com/movie-web/movie-web cd movie-web yarn install yarn start @@ -47,10 +47,10 @@ To build production files, simply run `yarn build`. You'll need to deploy a cloudflare service worker as well. Check the [selfhosting guide](https://github.com/movie-web/movie-web/blob/dev/SELFHOSTING.md) on how to run the service worker. Afterwards you can make a `.env` file and put in the URL. (see `example.env` for an example) -

Contributing - GitHub issues -GitHub pull requests

+

Contributing - GitHub issues +GitHub pull requests

-Check out [this project's issues](https://github.com/JamesHawkinss/movie-web/issues) for inspiration for contribution. Pull requests are always welcome. +Check out [this project's issues](https://github.com/movie-web/movie-web/issues) for inspiration for contribution. Pull requests are always welcome. **All pull requests must be merged into the `dev` branch. it will then be deployed with the next version** @@ -58,7 +58,7 @@ Check out [this project's issues](https://github.com/JamesHawkinss/movie-web/iss This project would not be possible without our amazing contributors and the community. -GitHub contributors +GitHub contributors
@JamesHawkinss for original concept. From df5f1a5fdbe641c4914b65f99fd608b9b49b8a40 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sun, 12 Feb 2023 16:23:55 +0100 Subject: [PATCH 114/135] migration error handling --- src/utils/storage.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/utils/storage.ts b/src/utils/storage.ts index b7bed37f..349a8707 100644 --- a/src/utils/storage.ts +++ b/src/utils/storage.ts @@ -44,9 +44,16 @@ export async function initializeStores() { // Migrate over each version let mostRecentData = data; - for (const version of relevantVersions) { - if (version.migrate) - mostRecentData = await version.migrate(mostRecentData); + try { + for (const version of relevantVersions) { + if (version.migrate) + mostRecentData = await version.migrate(mostRecentData); + } + } catch (err) { + console.error(`FAILED TO MIGRATE STORE ${internal.key}`, err); + // reset store to lastest version create + mostRecentData = + relevantVersions[relevantVersions.length - 1].create?.() ?? {}; } store.save(mostRecentData); From 224de7657842108a4443315d995dbc920bee58a2 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sun, 12 Feb 2023 16:27:27 +0100 Subject: [PATCH 115/135] more todos --- src/video/components/actions/LoadingAction.tsx | 1 + src/views/media/MediaView.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/src/video/components/actions/LoadingAction.tsx b/src/video/components/actions/LoadingAction.tsx index ce46356d..f79f5370 100644 --- a/src/video/components/actions/LoadingAction.tsx +++ b/src/video/components/actions/LoadingAction.tsx @@ -3,6 +3,7 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useMediaPlaying } from "@/video/state/logic/mediaplaying"; import { useMisc } from "@/video/state/logic/misc"; +// TODO pausing before first frame will infinitely show spinner until unpaused export function LoadingAction() { const descriptor = useVideoPlayerDescriptor(); const mediaPlaying = useMediaPlaying(descriptor); diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index dfd417b5..6e7d19b5 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -251,6 +251,7 @@ export function MediaView() { stream={stream} selected={selected} onChangeStream={(sId, eId) => { + // TODO changing episode breaks useGoBack history.replace( `/media/${encodeURIComponent(params.media)}/${encodeURIComponent( sId From eaf5730415df632b8e4e27a9a623297f1cf5f47f Mon Sep 17 00:00:00 2001 From: Jip Fr Date: Sun, 12 Feb 2023 23:41:47 +0100 Subject: [PATCH 116/135] Add v3 prompt thingie --- src/views/search/HomeView.tsx | 46 +++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/src/views/search/HomeView.tsx b/src/views/search/HomeView.tsx index b5a17843..c9b48ceb 100644 --- a/src/views/search/HomeView.tsx +++ b/src/views/search/HomeView.tsx @@ -9,8 +9,10 @@ import { import { useWatchedContext } from "@/state/watched"; import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; import { EditButton } from "@/components/buttons/EditButton"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { useAutoAnimate } from "@formkit/auto-animate/react"; +import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconButton"; +import { useHistory, useLocation } from "react-router-dom"; function Bookmarks() { const { t } = useTranslation(); @@ -79,9 +81,49 @@ function Watched() { ); } +function NewDomainInfo() { + const location = useLocation(); + const history = useHistory(); + + return ( +
+ { + const queryParams = new URLSearchParams(location.search); + queryParams.delete("redirected"); + history.replace({ + search: queryParams.toString(), + }); + }} + /> +

Hey there!

+

+ Welcome to the long-awaited shiny new update of movie-web. This awesome + updates includes an awesome new look, updated functionality, and even a + fully custom-built video player. +

+

+ We also have a new domain! Please be sure to update your bookmarks, as + the old domain is going to stop working on{" "} + May 31st, 2023. The new domain is{" "} + movie-web.app +

+
+ ); +} + export function HomeView() { + const location = useLocation(); + + const showNewDomainInfo = useMemo(() => { + return location.search.includes("redirected=1"); + }, [location.search]); + return ( -
+
+ {showNewDomainInfo ? : ""}
From 75762aca48292203b1fe7ba81e59103798ba55fd Mon Sep 17 00:00:00 2001 From: Jip Fr Date: Sun, 12 Feb 2023 23:45:11 +0100 Subject: [PATCH 117/135] Goodbye year --- src/views/search/HomeView.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/views/search/HomeView.tsx b/src/views/search/HomeView.tsx index c9b48ceb..5f730b92 100644 --- a/src/views/search/HomeView.tsx +++ b/src/views/search/HomeView.tsx @@ -106,9 +106,8 @@ function NewDomainInfo() {

We also have a new domain! Please be sure to update your bookmarks, as - the old domain is going to stop working on{" "} - May 31st, 2023. The new domain is{" "} - movie-web.app + the old domain is going to stop working on May 31st. + The new domain is movie-web.app

); From 4d4626806d4faf9f1ac20879a33dcb276e815f56 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 18 Feb 2023 14:00:38 +0100 Subject: [PATCH 118/135] fuzzy matching for title Co-authored-by: Jip Frijlink --- src/backend/providers/flixhq.ts | 4 ++-- src/backend/providers/superstream/index.ts | 4 ++-- src/utils/titleMatch.ts | 7 +++++++ 3 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 src/utils/titleMatch.ts diff --git a/src/backend/providers/flixhq.ts b/src/backend/providers/flixhq.ts index 493a1c3b..c8a89400 100644 --- a/src/backend/providers/flixhq.ts +++ b/src/backend/providers/flixhq.ts @@ -1,3 +1,4 @@ +import { compareTitle } from "@/utils/titleMatch"; import { proxiedFetch } from "../helpers/fetch"; import { registerProvider } from "../helpers/register"; import { MWStreamQuality, MWStreamType } from "../helpers/streams"; @@ -19,9 +20,8 @@ registerProvider({ baseURL: flixHqBase, } ); - // TODO fuzzy match or normalize title before comparison const foundItem = searchResults.results.find((v: any) => { - return v.title === media.meta.title && v.releaseDate === media.meta.year; + return compareTitle(v.title, media.meta.title) && v.releaseDate === media.meta.year; }); if (!foundItem) throw new Error("No watchable item found"); const flixId = foundItem.id; diff --git a/src/backend/providers/superstream/index.ts b/src/backend/providers/superstream/index.ts index c0861452..0f20fb2d 100644 --- a/src/backend/providers/superstream/index.ts +++ b/src/backend/providers/superstream/index.ts @@ -10,6 +10,7 @@ import { MWStreamQuality, MWStreamType, } from "@/backend/helpers/streams"; +import { compareTitle } from "@/utils/titleMatch"; const nanoid = customAlphabet("0123456789abcdef", 32); @@ -128,10 +129,9 @@ registerProvider({ const searchRes = (await get(searchQuery, true)).data; progress(33); - // TODO: add fuzzy search and normalise strings before matching const superstreamEntry = searchRes.find( (res: any) => - res.title === media.meta.title && res.year === Number(media.meta.year) + compareTitle(res.title, media.meta.title) && res.year === Number(media.meta.year) ); if (!superstreamEntry) throw new Error("No entry found on SuperStream"); diff --git a/src/utils/titleMatch.ts b/src/utils/titleMatch.ts new file mode 100644 index 00000000..cb69c790 --- /dev/null +++ b/src/utils/titleMatch.ts @@ -0,0 +1,7 @@ +function normalizeTitle(title: string): string { + return title.trim().toLowerCase().replace(/[\'\"\:]/g, "").replace(/[^a-zA-Z0-9]+/g, "_"); +} + +export function compareTitle(a: string, b: string): boolean { + return normalizeTitle(a) === normalizeTitle(b); +} From ad518a6508cce904c998c23e490473255c5d602b Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 18 Feb 2023 14:03:48 +0100 Subject: [PATCH 119/135] more fuzzy matching for migrations Co-authored-by: Jip Frijlink --- src/state/watched/migrations/v2.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/state/watched/migrations/v2.ts b/src/state/watched/migrations/v2.ts index 946334e8..5b2ee2f5 100644 --- a/src/state/watched/migrations/v2.ts +++ b/src/state/watched/migrations/v2.ts @@ -1,6 +1,7 @@ import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta"; import { searchForMedia } from "@/backend/metadata/search"; import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types"; +import { compareTitle } from "@/utils/titleMatch"; import { WatchedStoreData, WatchedStoreItem } from "../types"; interface OldMediaBase { @@ -49,10 +50,10 @@ async function getMetas( type: item.mediaType, }); const relevantItem = data.find((res) => - yearsAreClose(Number(res.year), year) + yearsAreClose(Number(res.year), year) && compareTitle(res.title, item.title) ); if (!relevantItem) { - console.error("No item"); + console.error("No item found for migration: " + item.title); return; } return { From 4f682d55a98c17c61158911989d2a427965652df Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 18 Feb 2023 20:01:19 +0100 Subject: [PATCH 120/135] translations :tada: Co-authored-by: Jip Frijlink --- package.json | 2 - src/backend/providers/netfilm.ts | 2 +- src/components/SearchBar.tsx | 7 +- src/components/buttons/EditButton.tsx | 4 +- src/components/layout/ErrorBoundary.tsx | 24 +- src/components/layout/Seasons.tsx | 109 - src/components/media/MediaCard.tsx | 34 +- src/index.tsx | 3 - src/setup/index.css | 5 + src/setup/locales/en/translation.json | 48 +- .../actions/CaptionsSelectionAction.tsx | 4 +- .../components/actions/LoadingAction.tsx | 1 - .../actions/SeriesSelectionAction.tsx | 4 +- .../actions/SourceSelectionAction.tsx | 7 +- .../hooks/useCurrentSeriesEpisodeInfo.ts | 9 +- .../components/parts/VideoErrorBoundary.tsx | 14 +- .../components/parts/VideoPlayerHeader.tsx | 5 +- .../popouts/CaptionSelectionPopout.tsx | 9 +- .../popouts/EpisodeSelectionPopout.tsx | 71 +- .../popouts/SourceSelectionPopout.tsx | 11 +- .../state/providers/videoStateProvider.ts | 1 + src/views/media/MediaErrorView.tsx | 33 +- src/views/media/MediaView.tsx | 17 +- src/views/notfound/NotFoundView.tsx | 3 +- src/views/search/SearchLoadingView.tsx | 13 +- src/views/search/SearchView.tsx | 2 +- yarn.lock | 5786 ++++++++--------- 27 files changed, 3075 insertions(+), 3153 deletions(-) delete mode 100644 src/components/layout/Seasons.tsx diff --git a/package.json b/package.json index b95b317c..2b912e81 100644 --- a/package.json +++ b/package.json @@ -73,8 +73,6 @@ "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "7.29.4", "eslint-plugin-react-hooks": "4.3.0", - "i": "^0.3.7", - "npm": "^9.2.0", "postcss": "^8.4.20", "prettier": "^2.5.1", "prettier-plugin-tailwindcss": "^0.1.7", diff --git a/src/backend/providers/netfilm.ts b/src/backend/providers/netfilm.ts index 58ba298d..c33caa1b 100644 --- a/src/backend/providers/netfilm.ts +++ b/src/backend/providers/netfilm.ts @@ -20,7 +20,7 @@ registerProvider({ type: [MWMediaType.MOVIE, MWMediaType.SERIES], async scrape({ media, episode, progress }) { - // // search for relevant item + // search for relevant item const searchResponse = await proxiedFetch( `/api/search?keyword=${encodeURIComponent(media.meta.title)}`, { diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index 8e97138d..ae332e8c 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -67,12 +67,7 @@ export function SearchBarInput(props: SearchBarProps) { id: MWMediaType.SERIES, name: t("searchBar.series"), icon: Icons.CLAPPER_BOARD, - }, - // { - // id: MWMediaType.ANIME, - // name: "Anime", - // icon: Icons.DRAGON, - // }, + } ]} onClick={() => setDropdownOpen((old) => !old)} > diff --git a/src/components/buttons/EditButton.tsx b/src/components/buttons/EditButton.tsx index a72eb8f8..0b91e5ed 100644 --- a/src/components/buttons/EditButton.tsx +++ b/src/components/buttons/EditButton.tsx @@ -1,6 +1,7 @@ import { Icon, Icons } from "@/components/Icon"; import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useCallback } from "react"; +import { useTranslation } from "react-i18next"; import { ButtonControl } from "./ButtonControl"; export interface EditButtonProps { @@ -9,6 +10,7 @@ export interface EditButtonProps { } export function EditButton(props: EditButtonProps) { + const { t } = useTranslation() const [parent] = useAutoAnimate(); const onClick = useCallback(() => { @@ -22,7 +24,7 @@ export function EditButton(props: EditButtonProps) { > {props.editing ? ( - Stop editing + {t("media.stopEditing")} ) : ( )} diff --git a/src/components/layout/ErrorBoundary.tsx b/src/components/layout/ErrorBoundary.tsx index 5f6adacb..a5bf4399 100644 --- a/src/components/layout/ErrorBoundary.tsx +++ b/src/components/layout/ErrorBoundary.tsx @@ -4,6 +4,7 @@ import { Icons } from "@/components/Icon"; import { Link } from "@/components/text/Link"; import { Title } from "@/components/text/Title"; import { conf } from "@/setup/config"; +import { Trans, useTranslation } from "react-i18next"; interface ErrorShowcaseProps { error: { @@ -35,29 +36,24 @@ interface ErrorMessageProps { } export function ErrorMessage(props: ErrorMessageProps) { + const { t } = useTranslation() + return (
- Whoops, it broke + {t("media.errors.genericTitle")} {props.children ? (

{props.children}

) : (

- The app encountered an error and wasn't able to recover, please - report it to the{" "} - - Discord server - {" "} - or on{" "} - - GitHub - - . + + + +

)}
diff --git a/src/components/layout/Seasons.tsx b/src/components/layout/Seasons.tsx deleted file mode 100644 index 1f31a1b9..00000000 --- a/src/components/layout/Seasons.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { useEffect, useState } from "react"; -import { useHistory } from "react-router-dom"; -import { useTranslation } from "react-i18next"; -import { IconPatch } from "@/components/buttons/IconPatch"; -import { Dropdown, OptionItem } from "@/components/Dropdown"; -import { Icons } from "@/components/Icon"; -import { WatchedEpisode } from "@/components/media/WatchedEpisodeButton"; -import { useLoading } from "@/hooks/useLoading"; -import { serializePortableMedia } from "@/hooks/usePortableMedia"; - -export interface SeasonsProps { - media: any; -} - -export function LoadingSeasons(props: { error?: boolean }) { - const { t } = useTranslation(); - - return ( -
-
-
-
- {!props.error ? ( - <> -
-
-
- - ) : ( -
- -

{t("seasons.failed")}

-
- )} -
- ); -} - -export function Seasons(props: SeasonsProps) { - // const { t } = useTranslation(); - // const [searchSeasons, loading, error, success] = useLoading( - // (portableMedia: MWPortableMedia) => getSeasonDataFromMedia(portableMedia) - // ); - // const history = useHistory(); - // const [seasons, setSeasons] = useState({ seasons: [] }); - // const seasonSelected = props.media.seasonId as string; - // const episodeSelected = props.media.episodeId as string; - // useEffect(() => { - // (async () => { - // const seasonData = await searchSeasons(props.media); - // setSeasons(seasonData); - // })(); - // }, [searchSeasons, props.media]); - // function navigateToSeasonAndEpisode(seasonId: string, episodeId: string) { - // const newMedia: MWMedia = { ...props.media }; - // newMedia.episodeId = episodeId; - // newMedia.seasonId = seasonId; - // history.replace( - // `/media/${newMedia.mediaType}/${serializePortableMedia( - // convertMediaToPortable(newMedia) - // )}` - // ); - // } - // const mapSeason = (season: MWMediaSeason) => ({ - // id: season.id, - // name: season.title || `${t("seasons.season", { season: season.sort })}`, - // }); - // const options = seasons.seasons.map(mapSeason); - // const foundSeason = seasons.seasons.find( - // (season) => season.id === seasonSelected - // ); - // const selectedItem = foundSeason ? mapSeason(foundSeason) : null; - // return ( - // <> - // {loading ? : null} - // {error ? : null} - // {success && seasons.seasons.length ? ( - // <> - // - // navigateToSeasonAndEpisode( - // seasonItem.id, - // seasons.seasons.find((s) => s.id === seasonItem.id)?.episodes[0] - // .id as string - // ) - // } - // /> - // {seasons.seasons - // .find((s) => s.id === seasonSelected) - // ?.episodes.map((v) => ( - // navigateToSeasonAndEpisode(seasonSelected, v.id)} - // /> - // ))} - // - // ) : null} - // - // ); -} diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 42d76eb9..a67dba21 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -1,4 +1,5 @@ import { Link } from "react-router-dom"; +import { useTranslation } from "react-i18next"; import { DotList } from "@/components/text/DotList"; import { MWMediaMeta } from "@/backend/metadata/types"; import { JWMediaToId } from "@/backend/metadata/justwatch"; @@ -27,20 +28,19 @@ function MediaCardContent({ closable, onClose, }: MediaCardProps) { + const { t } = useTranslation(); const percentageString = `${Math.round(percentage ?? 0).toFixed(0)}%`; const canLink = linkable && !closable; return (

- S{series.season} E{series.episode} + {t("seasons.seasonAndEpisode", { + season: series.season, + episode: series.episode + })}

) : null} @@ -59,14 +62,12 @@ function MediaCardContent({ {percentage !== undefined ? ( <>
@@ -82,9 +83,8 @@ function MediaCardContent({ ) : null}
diff --git a/src/index.tsx b/src/index.tsx index 467a898b..97f8db56 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -21,12 +21,9 @@ initializeChromecast(); // TODO video todos: // - chrome cast support -// - bug: unmounting player throws errors in console // - bug: safari fullscreen will make video overlap player controls // - improvement: make scrapers use fuzzy matching on normalized titles -// - bug: source selection doesnt work with HLS // - bug: .ass subtitle files are fucked -// - improvement: episode watch at the ending should not startAt // TODO stuff to test: // - browser: firefox, chrome, edge, safari desktop diff --git a/src/setup/index.css b/src/setup/index.css index cd69edd9..c6ca4617 100644 --- a/src/setup/index.css +++ b/src/setup/index.css @@ -44,3 +44,8 @@ body[data-no-select] { -webkit-box-orient: vertical; overflow: hidden; } + +google-cast-launcher { + @apply pointer-events-auto m-2 text-white flex items-center justify-center p-2; + @apply transition-[background-color,transform] duration-100 rounded-full bg-denim-600 bg-opacity-0 hover:bg-opacity-50 active:bg-denim-500 active:bg-opacity-100 active:scale-110; +} diff --git a/src/setup/locales/en/translation.json b/src/setup/locales/en/translation.json index f83d56f7..388c7838 100644 --- a/src/setup/locales/en/translation.json +++ b/src/setup/locales/en/translation.json @@ -3,26 +3,34 @@ "name": "movie-web" }, "search": { - "loading": "Fetching your favourite shows...", + "loading_series": "Fetching your favourite series...", + "loading_movie": "Fetching your favourite movies...", + "loading": "Loading...", "allResults": "That's all we have!", "noResults": "We couldn't find anything!", "allFailed": "Failed to find media, try again!", "headingTitle": "Search results", - "headingLink": "Back to home", "bookmarks": "Bookmarks", "continueWatching": "Continue Watching", "title": "What do you want to watch?", "placeholder": "What do you want to watch?" }, "media": { - "invalidUrl": "Your URL may be invalid", - "arrowText": "Go back" + "movie": "Movie", + "series": "Series", + "stopEditing": "Stop editing", + "errors": { + "genericTitle": "Whoops, it broke!", + "failedMeta": "Failed to load meta", + "mediaFailed": "We failed to request the media you asked for, check your internet connection and try again.", + "videoFailed": "We encountered an error while playing the video you requested. If this keeps happening please report the issue to the <0>Discord server or on <1>GitHub." + } }, "seasons": { - "season": "Season {{season}}", - "failed": "Failed to get season data" + "seasonAndEpisode": "S{{season}} E{{episode}}" }, "notFound": { + "genericTitle": "Not found", "backArrow": "Back to home", "media": { "title": "Couldn't find that media", @@ -42,7 +50,31 @@ "series": "Series", "Search": "Search" }, - "errorBoundary": { - "text": "The app encountered an error and wasn't able to recover, please report it to the" + "videoPlayer": { + "findingBestVideo": "Finding the best video for you", + "noVideos": "Whoops, couldn't find any videos for you", + "loading": "Loading...", + "backToHome": "Back to home", + "seasonAndEpisode": "S{{season}} E{{episode}}", + "buttons": { + "episodes": "Episodes", + "source": "Source", + "captions": "Captions" + }, + "popouts": { + "sources": "Sources", + "seasons": "Seasons", + "captions": "Captions", + "episode": "E{{index}} - {{title}}", + "noCaptions": "No captions", + "linkedCaptions": "Linked captions", + "errors": { + "loadingWentWong": "Something went wrong loading the episodes for {{seasonTitle}}", + "embedsError": "Something went wrong loading the embeds for this thing that you like" + } + }, + "errors": { + "fatalError": "The video player encounted a fatal error, please report it to the <0>Discord server or on <1>GitHub." + } } } diff --git a/src/video/components/actions/CaptionsSelectionAction.tsx b/src/video/components/actions/CaptionsSelectionAction.tsx index d2bc588a..96a13fdc 100644 --- a/src/video/components/actions/CaptionsSelectionAction.tsx +++ b/src/video/components/actions/CaptionsSelectionAction.tsx @@ -4,12 +4,14 @@ import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconB import { useControls } from "@/video/state/logic/controls"; import { PopoutAnchor } from "@/video/components/popouts/PopoutAnchor"; import { useIsMobile } from "@/hooks/useIsMobile"; +import { useTranslation } from "react-i18next"; interface Props { className?: string; } export function CaptionsSelectionAction(props: Props) { + const { t } = useTranslation() const descriptor = useVideoPlayerDescriptor(); const controls = useControls(descriptor); const { isMobile } = useIsMobile(); @@ -20,7 +22,7 @@ export function CaptionsSelectionAction(props: Props) { controls.openPopout("captions")} icon={Icons.CAPTIONS} diff --git a/src/video/components/actions/LoadingAction.tsx b/src/video/components/actions/LoadingAction.tsx index f79f5370..ce46356d 100644 --- a/src/video/components/actions/LoadingAction.tsx +++ b/src/video/components/actions/LoadingAction.tsx @@ -3,7 +3,6 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useMediaPlaying } from "@/video/state/logic/mediaplaying"; import { useMisc } from "@/video/state/logic/misc"; -// TODO pausing before first frame will infinitely show spinner until unpaused export function LoadingAction() { const descriptor = useVideoPlayerDescriptor(); const mediaPlaying = useMediaPlaying(descriptor); diff --git a/src/video/components/actions/SeriesSelectionAction.tsx b/src/video/components/actions/SeriesSelectionAction.tsx index 6c68d8ae..f048db26 100644 --- a/src/video/components/actions/SeriesSelectionAction.tsx +++ b/src/video/components/actions/SeriesSelectionAction.tsx @@ -6,12 +6,14 @@ import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconB import { useControls } from "@/video/state/logic/controls"; import { PopoutAnchor } from "@/video/components/popouts/PopoutAnchor"; import { useInterface } from "@/video/state/logic/interface"; +import { useTranslation } from "react-i18next"; interface Props { className?: string; } export function SeriesSelectionAction(props: Props) { + const { t } = useTranslation() const descriptor = useVideoPlayerDescriptor(); const meta = useMeta(descriptor); const videoInterface = useInterface(descriptor); @@ -26,7 +28,7 @@ export function SeriesSelectionAction(props: Props) { controls.openPopout("episodes")} /> diff --git a/src/video/components/actions/SourceSelectionAction.tsx b/src/video/components/actions/SourceSelectionAction.tsx index 6434bd0a..3058a6d2 100644 --- a/src/video/components/actions/SourceSelectionAction.tsx +++ b/src/video/components/actions/SourceSelectionAction.tsx @@ -4,12 +4,14 @@ import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconB import { useControls } from "@/video/state/logic/controls"; import { PopoutAnchor } from "@/video/components/popouts/PopoutAnchor"; import { useInterface } from "@/video/state/logic/interface"; +import { useTranslation } from "react-i18next"; interface Props { className?: string; } export function SourceSelectionAction(props: Props) { + const { t } = useTranslation() const descriptor = useVideoPlayerDescriptor(); const videoInterface = useInterface(descriptor); const controls = useControls(descriptor); @@ -20,8 +22,9 @@ export function SourceSelectionAction(props: Props) { controls.openPopout("source")} /> diff --git a/src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts b/src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts index 77afa249..7e62a0e4 100644 --- a/src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts +++ b/src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts @@ -1,9 +1,11 @@ import { MWMediaType } from "@/backend/metadata/types"; import { useMeta } from "@/video/state/logic/meta"; import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; export function useCurrentSeriesEpisodeInfo(descriptor: string) { const meta = useMeta(descriptor); + const {t} = useTranslation() const currentSeasonInfo = useMemo(() => { return meta?.seasons?.find( @@ -22,8 +24,11 @@ export function useCurrentSeriesEpisodeInfo(descriptor: string) { ); if (!isSeries) return { isSeries: false }; - - const humanizedEpisodeId = `S${currentSeasonInfo?.number} E${currentEpisodeInfo?.number}`; + + const humanizedEpisodeId = t("videoPlayer.seasonAndEpisode", { + season: currentSeasonInfo?.number, + episode: currentEpisodeInfo?.number + }); return { isSeries: true, diff --git a/src/video/components/parts/VideoErrorBoundary.tsx b/src/video/components/parts/VideoErrorBoundary.tsx index 205e27ae..7db5c1d4 100644 --- a/src/video/components/parts/VideoErrorBoundary.tsx +++ b/src/video/components/parts/VideoErrorBoundary.tsx @@ -3,6 +3,7 @@ import { ErrorMessage } from "@/components/layout/ErrorBoundary"; import { Link } from "@/components/text/Link"; import { conf } from "@/setup/config"; import { Component, ReactNode } from "react"; +import { Trans } from "react-i18next"; import { VideoPlayerHeader } from "./VideoPlayerHeader"; interface ErrorBoundaryState { @@ -67,15 +68,10 @@ export class VideoErrorBoundary extends Component< />
- The video player encounted a fatal error, please report it to the{" "} - - Discord server - {" "} - or on{" "} - - GitHub - - . + + + +
); diff --git a/src/video/components/parts/VideoPlayerHeader.tsx b/src/video/components/parts/VideoPlayerHeader.tsx index 771be45a..81b1e5c8 100644 --- a/src/video/components/parts/VideoPlayerHeader.tsx +++ b/src/video/components/parts/VideoPlayerHeader.tsx @@ -8,6 +8,7 @@ import { } from "@/state/bookmark"; import { AirplayAction } from "@/video/components/actions/AirplayAction"; import { ChromecastAction } from "@/video/components/actions/ChromecastAction"; +import { useTranslation } from "react-i18next"; interface VideoPlayerHeaderProps { media?: MWMediaMeta; @@ -21,6 +22,8 @@ export function VideoPlayerHeader(props: VideoPlayerHeaderProps) { ? getIfBookmarkedFromPortable(bookmarkStore.bookmarks, props.media) : false; const showDivider = props.media && props.onClick; + const { t } = useTranslation(); + return (
@@ -31,7 +34,7 @@ export function VideoPlayerHeader(props: VideoPlayerHeaderProps) { className="flex cursor-pointer items-center py-1 text-white opacity-50 transition-opacity hover:opacity-100" > - Back to home + {t("videoPlayer.backToHome")} ) : null} {showDivider ? ( diff --git a/src/video/components/popouts/CaptionSelectionPopout.tsx b/src/video/components/popouts/CaptionSelectionPopout.tsx index e98bc669..995b86ac 100644 --- a/src/video/components/popouts/CaptionSelectionPopout.tsx +++ b/src/video/components/popouts/CaptionSelectionPopout.tsx @@ -7,6 +7,7 @@ import { useControls } from "@/video/state/logic/controls"; import { useMeta } from "@/video/state/logic/meta"; import { useSource } from "@/video/state/logic/source"; import { useMemo, useRef } from "react"; +import { useTranslation } from "react-i18next"; import { PopoutListEntry, PopoutSection } from "./PopoutUtils"; function makeCaptionId(caption: MWCaption, isLinked: boolean): string { @@ -14,6 +15,8 @@ function makeCaptionId(caption: MWCaption, isLinked: boolean): string { } export function CaptionSelectionPopout() { + const { t } = useTranslation() + const descriptor = useVideoPlayerDescriptor(); const meta = useMeta(descriptor); const source = useSource(descriptor); @@ -38,7 +41,7 @@ export function CaptionSelectionPopout() { return ( <> -
Captions
+
{t("videoPlayer.popouts.captions")}
@@ -49,13 +52,13 @@ export function CaptionSelectionPopout() { controls.closePopout(); }} > - No captions + {t("videoPlayer.popouts.noCaptions")}

- Linked captions + {t("videoPlayer.popouts.linkedCaptions")}

diff --git a/src/video/components/popouts/EpisodeSelectionPopout.tsx b/src/video/components/popouts/EpisodeSelectionPopout.tsx index e60ddd99..7947d855 100644 --- a/src/video/components/popouts/EpisodeSelectionPopout.tsx +++ b/src/video/components/popouts/EpisodeSelectionPopout.tsx @@ -12,11 +12,14 @@ import { useMeta } from "@/video/state/logic/meta"; import { useControls } from "@/video/state/logic/controls"; import { useWatchedContext } from "@/state/watched"; import { PopoutListEntry, PopoutSection } from "./PopoutUtils"; +import { useTranslation } from "react-i18next"; export function EpisodeSelectionPopout() { const params = useParams<{ media: string; }>(); + const { t } = useTranslation() + const descriptor = useVideoPlayerDescriptor(); const meta = useMeta(descriptor); const controls = useControls(descriptor); @@ -119,7 +122,7 @@ export function EpisodeSelectionPopout() { isPickingSeason ? "opacity-1" : "opacity-0", ].join(" ")} > - Seasons + {t("videoPlayer.popouts.seasons")}
@@ -134,15 +137,15 @@ export function EpisodeSelectionPopout() { > {currentSeasonInfo ? meta?.seasons?.map?.((season) => ( - setSeason(season.id)} - isOnDarkBackground - > - {season.title} - - )) + setSeason(season.id)} + isOnDarkBackground + > + {season.title} + + )) : "No season"} @@ -158,8 +161,9 @@ export function EpisodeSelectionPopout() { className="text-xl text-bink-600" />

- Something went wrong loading the episodes for{" "} - {currentSeasonInfo?.title?.toLowerCase()} + {t("videoPLayer.popouts.errors.loadingWentWrong", { + seasonTitle: currentSeasonInfo?.title?.toLowerCase() + })}

@@ -167,26 +171,29 @@ export function EpisodeSelectionPopout() {
{currentSeasonEpisodes && currentSeasonInfo ? currentSeasonEpisodes.map((e) => ( - { - if (e.id === meta?.episode?.episodeId) - controls.closePopout(); - else setCurrent(currentSeasonInfo.id, e.id); - }} - percentageCompleted={ - watched.items.find( - (item) => - item.item?.series?.seasonId === - currentSeasonInfo.id && - item.item?.series?.episodeId === e.id - )?.percentage - } - > - E{e.number} - {e.title} - - )) + { + if (e.id === meta?.episode?.episodeId) + controls.closePopout(); + else setCurrent(currentSeasonInfo.id, e.id); + }} + percentageCompleted={ + watched.items.find( + (item) => + item.item?.series?.seasonId === + currentSeasonInfo.id && + item.item?.series?.episodeId === e.id + )?.percentage + } + > + {t("videoPlayer.popouts.episode", { + index: e.number, + title: e.title + })} + + )) : "No episodes"}
)} diff --git a/src/video/components/popouts/SourceSelectionPopout.tsx b/src/video/components/popouts/SourceSelectionPopout.tsx index b5e9d62a..0f353094 100644 --- a/src/video/components/popouts/SourceSelectionPopout.tsx +++ b/src/video/components/popouts/SourceSelectionPopout.tsx @@ -11,9 +11,11 @@ import { getProviders } from "@/backend/helpers/register"; import { runProvider } from "@/backend/helpers/run"; import { MWProviderScrapeResult } from "@/backend/helpers/provider"; import { PopoutListEntry, PopoutSection } from "./PopoutUtils"; +import { useTranslation } from "react-i18next"; -// TODO HLS does not work export function SourceSelectionPopout() { + const { t } = useTranslation() + const descriptor = useVideoPlayerDescriptor(); const controls = useControls(descriptor); const meta = useMeta(descriptor); @@ -42,7 +44,7 @@ export function SourceSelectionPopout() { tmdbId: "", meta: meta.meta, }, - progress: () => {}, + progress: () => { }, type: meta.meta.type, episode: meta.episode?.episodeId as any, season: meta.episode?.seasonId as any, @@ -129,7 +131,7 @@ export function SourceSelectionPopout() { !showingProvider ? "opacity-1" : "opacity-0", ].join(" ")} > - Sources + {t("videoPlayer.popouts.sources")}
@@ -154,8 +156,7 @@ export function SourceSelectionPopout() { className="text-xl text-bink-600" />

- Something went wrong loading the embeds for this thing that - you like + {t("videoPlayer.popouts.errors.embedsError")}

diff --git a/src/video/state/providers/videoStateProvider.ts b/src/video/state/providers/videoStateProvider.ts index 9788bda7..ae009aae 100644 --- a/src/video/state/providers/videoStateProvider.ts +++ b/src/video/state/providers/videoStateProvider.ts @@ -249,6 +249,7 @@ export function createVideoStateProvider( }; const canplay = () => { state.mediaPlaying.isFirstLoading = false; + state.mediaPlaying.isLoading = false; updateMediaPlaying(descriptor, state); }; const fullscreenchange = () => { diff --git a/src/views/media/MediaErrorView.tsx b/src/views/media/MediaErrorView.tsx index 47b4863c..140a3dbe 100644 --- a/src/views/media/MediaErrorView.tsx +++ b/src/views/media/MediaErrorView.tsx @@ -5,48 +5,23 @@ import { useGoBack } from "@/hooks/useGoBack"; import { conf } from "@/setup/config"; import { VideoPlayerHeader } from "@/video/components/parts/VideoPlayerHeader"; import { Helmet } from "react-helmet"; +import { Trans, useTranslation } from "react-i18next"; export function MediaFetchErrorView() { + const { t } = useTranslation() const goBack = useGoBack(); return (
- Failed to load meta + {t("media.errors.failedMeta")}

- We failed to request the media you asked for, check your internet - connection and try again. -

-
-
- ); -} - -export function MediaPlaybackErrorView(props: { media?: MWMediaMeta }) { - const goBack = useGoBack(); - - return ( -
-
- -
- -

- We encountered an error while playing the video you requested. If this - keeps happening please report the issue to the - - Discord server - {" "} - or on{" "} - - GitHub - - . + {t("media.errors.mediaFailed")}

diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index 6e7d19b5..f12869f4 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -22,19 +22,22 @@ import { useWatchedItem } from "@/state/watched"; import { MediaFetchErrorView } from "./MediaErrorView"; import { MediaScrapeLog } from "./MediaScrapeLog"; import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView"; +import { useTranslation } from "react-i18next"; function MediaViewLoading(props: { onGoBack(): void }) { + const { t } = useTranslation() + return (
- Loading... + {t("videoPlayer.loading")}
-

Finding the best video for you

+

{t("videoPlaye.findingBestVideo")}

); @@ -48,6 +51,7 @@ interface MediaViewScrapingProps { } function MediaViewScraping(props: MediaViewScrapingProps) { const { eventLog, stream, pending } = useScrape(props.meta, props.selected); + const { t } = useTranslation() useEffect(() => { if (stream) { @@ -68,21 +72,20 @@ function MediaViewScraping(props: MediaViewScrapingProps) { <>

- Finding the best video for you + {t("videoPlayer.findingBestVideo")}

) : ( <>

- Whoops, could't find any videos for you + {t("videoPlayer.noVideos")}

)}
diff --git a/src/views/notfound/NotFoundView.tsx b/src/views/notfound/NotFoundView.tsx index 83890e63..a1dfcab0 100644 --- a/src/views/notfound/NotFoundView.tsx +++ b/src/views/notfound/NotFoundView.tsx @@ -13,12 +13,13 @@ export function NotFoundWrapper(props: { children?: ReactNode; video?: boolean; }) { + const { t } = useTranslation() const goBack = useGoBack(); return (
- Not found + {t("notFound.genericTitle")} {props.video ? (
diff --git a/src/views/search/SearchLoadingView.tsx b/src/views/search/SearchLoadingView.tsx index 54cbeef9..1ae0d89c 100644 --- a/src/views/search/SearchLoadingView.tsx +++ b/src/views/search/SearchLoadingView.tsx @@ -1,12 +1,17 @@ import { useTranslation } from "react-i18next"; import { Loading } from "@/components/layout/Loading"; +import { MWQuery } from "@/backend/metadata/types"; +import { useSearchQuery } from "@/hooks/useSearchQuery"; export function SearchLoadingView() { const { t } = useTranslation(); + const [query] = useSearchQuery() return ( - + <> + + ); } diff --git a/src/views/search/SearchView.tsx b/src/views/search/SearchView.tsx index 93c11c63..b65892f8 100644 --- a/src/views/search/SearchView.tsx +++ b/src/views/search/SearchView.tsx @@ -24,7 +24,7 @@ export function SearchView() { <>
- movie-web + {t("global.name")} diff --git a/yarn.lock b/yarn.lock index 74778e8b..a4ef64b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,103 +3,103 @@ "@babel/runtime-corejs3@^7.10.2": - "integrity" "sha512-tqeujPiuEfcH067mx+7otTQWROVMKHXEaOQcAeNV5dDdbPWvPcFA8/W9LXw2NfjNmOetqLl03dfnG2WALPlsRQ==" - "resolved" "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.20.6.tgz" - "version" "7.20.6" + version "7.20.6" + resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.20.6.tgz" + integrity sha512-tqeujPiuEfcH067mx+7otTQWROVMKHXEaOQcAeNV5dDdbPWvPcFA8/W9LXw2NfjNmOetqLl03dfnG2WALPlsRQ== dependencies: - "core-js-pure" "^3.25.1" - "regenerator-runtime" "^0.13.11" + core-js-pure "^3.25.1" + regenerator-runtime "^0.13.11" "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.14.5", "@babel/runtime@^7.18.9", "@babel/runtime@^7.19.4", "@babel/runtime@^7.20.6", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": - "integrity" "sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==" - "resolved" "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz" - "version" "7.20.6" + version "7.20.6" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz" + integrity sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA== dependencies: - "regenerator-runtime" "^0.13.11" + regenerator-runtime "^0.13.11" "@colors/colors@1.5.0": - "version" "1.5.0" + version "1.5.0" "@esbuild/linux-x64@0.16.5": - "integrity" "sha512-vsOwzKN+4NenUTyuoWLmg5dAuO8JKuLD9MXSeENA385XucuOZbblmOMwwgPlHsgVRtSjz38riqPJU2ALI/CWYQ==" - "resolved" "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.5.tgz" - "version" "0.16.5" + version "0.16.5" + resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.5.tgz" + integrity sha512-vsOwzKN+4NenUTyuoWLmg5dAuO8JKuLD9MXSeENA385XucuOZbblmOMwwgPlHsgVRtSjz38riqPJU2ALI/CWYQ== "@eslint/eslintrc@^1.3.3": - "integrity" "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==" - "resolved" "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz" - "version" "1.3.3" - dependencies: - "ajv" "^6.12.4" - "debug" "^4.3.2" - "espree" "^9.4.0" - "globals" "^13.15.0" - "ignore" "^5.2.0" - "import-fresh" "^3.2.1" - "js-yaml" "^4.1.0" - "minimatch" "^3.1.2" - "strip-json-comments" "^3.1.1" + version "1.3.3" + resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz" + integrity sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.4.0" + globals "^13.15.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" "@formkit/auto-animate@^1.0.0-beta.5": - "integrity" "sha512-WoSwyhAZPOe6RB/IgicOtCHtrWwEpfKIZ/H/nxpKfnZL9CB6hhhBGU5bCdMRw7YpAUF2CDlQa+WWh+gCqz5lDg==" - "resolved" "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-1.0.0-beta.5.tgz" - "version" "1.0.0-beta.5" + 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== "@gar/promisify@^1.1.3": - "version" "1.1.3" + version "1.1.3" "@headlessui/react@^1.5.0": - "integrity" "sha512-UZSxOfA0CYKO7QDT5OGlFvesvlR1SKkawwSjwQJwt7XQItpzRKdE3ZUQxHcg4LEz3C0Wler2s9psdb872ynwrQ==" - "resolved" "https://registry.npmjs.org/@headlessui/react/-/react-1.7.5.tgz" - "version" "1.7.5" + version "1.7.5" + resolved "https://registry.npmjs.org/@headlessui/react/-/react-1.7.5.tgz" + integrity sha512-UZSxOfA0CYKO7QDT5OGlFvesvlR1SKkawwSjwQJwt7XQItpzRKdE3ZUQxHcg4LEz3C0Wler2s9psdb872ynwrQ== dependencies: - "client-only" "^0.0.1" + client-only "^0.0.1" "@humanwhocodes/config-array@^0.11.6": - "integrity" "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==" - "resolved" "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz" - "version" "0.11.7" + version "0.11.7" + resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz" + integrity sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw== dependencies: "@humanwhocodes/object-schema" "^1.2.1" - "debug" "^4.1.1" - "minimatch" "^3.0.5" + debug "^4.1.1" + minimatch "^3.0.5" "@humanwhocodes/module-importer@^1.0.1": - "integrity" "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==" - "resolved" "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" - "version" "1.0.1" + version "1.0.1" + resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== "@humanwhocodes/object-schema@^1.2.1": - "integrity" "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" - "resolved" "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" - "version" "1.2.1" + version "1.2.1" + resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== "@isaacs/string-locale-compare@^1.1.0": - "version" "1.1.0" + version "1.1.0" "@nodelib/fs.scandir@2.1.5": - "integrity" "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==" - "resolved" "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" - "version" "2.1.5" + version "2.1.5" + resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== dependencies: "@nodelib/fs.stat" "2.0.5" - "run-parallel" "^1.1.9" + run-parallel "^1.1.9" "@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": - "integrity" "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" - "resolved" "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" - "version" "2.0.5" + version "2.0.5" + resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== "@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": - "integrity" "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==" - "resolved" "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" - "version" "1.2.8" + version "1.2.8" + resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== dependencies: "@nodelib/fs.scandir" "2.1.5" - "fastq" "^1.6.0" + fastq "^1.6.0" "@npmcli/arborist@^6.1.5": - "version" "6.1.5" + version "6.1.5" dependencies: "@isaacs/string-locale-compare" "^1.1.0" "@npmcli/fs" "^3.1.0" @@ -111,142 +111,142 @@ "@npmcli/package-json" "^3.0.0" "@npmcli/query" "^3.0.0" "@npmcli/run-script" "^6.0.0" - "bin-links" "^4.0.1" - "cacache" "^17.0.3" - "common-ancestor-path" "^1.0.1" - "hosted-git-info" "^6.1.1" - "json-parse-even-better-errors" "^3.0.0" - "json-stringify-nice" "^1.1.4" - "minimatch" "^5.1.1" - "nopt" "^7.0.0" - "npm-install-checks" "^6.0.0" - "npm-package-arg" "^10.1.0" - "npm-pick-manifest" "^8.0.1" - "npm-registry-fetch" "^14.0.3" - "npmlog" "^7.0.1" - "pacote" "^15.0.7" - "parse-conflict-json" "^3.0.0" - "proc-log" "^3.0.0" - "promise-all-reject-late" "^1.0.0" - "promise-call-limit" "^1.0.1" - "read-package-json-fast" "^3.0.1" - "semver" "^7.3.7" - "ssri" "^10.0.1" - "treeverse" "^3.0.0" - "walk-up-path" "^1.0.0" + bin-links "^4.0.1" + cacache "^17.0.3" + common-ancestor-path "^1.0.1" + hosted-git-info "^6.1.1" + json-parse-even-better-errors "^3.0.0" + json-stringify-nice "^1.1.4" + minimatch "^5.1.1" + nopt "^7.0.0" + npm-install-checks "^6.0.0" + npm-package-arg "^10.1.0" + npm-pick-manifest "^8.0.1" + npm-registry-fetch "^14.0.3" + npmlog "^7.0.1" + pacote "^15.0.7" + parse-conflict-json "^3.0.0" + proc-log "^3.0.0" + promise-all-reject-late "^1.0.0" + promise-call-limit "^1.0.1" + read-package-json-fast "^3.0.1" + semver "^7.3.7" + ssri "^10.0.1" + treeverse "^3.0.0" + walk-up-path "^1.0.0" "@npmcli/config@^6.1.0": - "version" "6.1.0" + version "6.1.0" dependencies: "@npmcli/map-workspaces" "^3.0.0" - "ini" "^3.0.0" - "nopt" "^7.0.0" - "proc-log" "^3.0.0" - "read-package-json-fast" "^3.0.0" - "semver" "^7.3.5" - "walk-up-path" "^1.0.0" + ini "^3.0.0" + nopt "^7.0.0" + proc-log "^3.0.0" + read-package-json-fast "^3.0.0" + semver "^7.3.5" + walk-up-path "^1.0.0" "@npmcli/disparity-colors@^3.0.0": - "version" "3.0.0" + version "3.0.0" dependencies: - "ansi-styles" "^4.3.0" + ansi-styles "^4.3.0" "@npmcli/fs@^2.1.0": - "version" "2.1.2" + version "2.1.2" dependencies: "@gar/promisify" "^1.1.3" - "semver" "^7.3.5" + semver "^7.3.5" "@npmcli/fs@^3.1.0": - "version" "3.1.0" + version "3.1.0" dependencies: - "semver" "^7.3.5" + semver "^7.3.5" "@npmcli/git@^4.0.0", "@npmcli/git@^4.0.1": - "version" "4.0.3" + version "4.0.3" dependencies: "@npmcli/promise-spawn" "^6.0.0" - "lru-cache" "^7.4.4" - "mkdirp" "^1.0.4" - "npm-pick-manifest" "^8.0.0" - "proc-log" "^3.0.0" - "promise-inflight" "^1.0.1" - "promise-retry" "^2.0.1" - "semver" "^7.3.5" - "which" "^3.0.0" + lru-cache "^7.4.4" + mkdirp "^1.0.4" + npm-pick-manifest "^8.0.0" + proc-log "^3.0.0" + promise-inflight "^1.0.1" + promise-retry "^2.0.1" + semver "^7.3.5" + which "^3.0.0" "@npmcli/installed-package-contents@^2.0.0", "@npmcli/installed-package-contents@^2.0.1": - "version" "2.0.1" + version "2.0.1" dependencies: - "npm-bundled" "^3.0.0" - "npm-normalize-package-bin" "^3.0.0" + npm-bundled "^3.0.0" + npm-normalize-package-bin "^3.0.0" "@npmcli/map-workspaces@^3.0.0": - "version" "3.0.0" + version "3.0.0" dependencies: "@npmcli/name-from-folder" "^1.0.1" - "glob" "^8.0.1" - "minimatch" "^5.0.1" - "read-package-json-fast" "^3.0.0" + glob "^8.0.1" + minimatch "^5.0.1" + read-package-json-fast "^3.0.0" "@npmcli/metavuln-calculator@^5.0.0": - "version" "5.0.0" + version "5.0.0" dependencies: - "cacache" "^17.0.0" - "json-parse-even-better-errors" "^3.0.0" - "pacote" "^15.0.0" - "semver" "^7.3.5" + cacache "^17.0.0" + json-parse-even-better-errors "^3.0.0" + pacote "^15.0.0" + semver "^7.3.5" "@npmcli/move-file@^2.0.0": - "version" "2.0.1" + version "2.0.1" dependencies: - "mkdirp" "^1.0.4" - "rimraf" "^3.0.2" + mkdirp "^1.0.4" + rimraf "^3.0.2" "@npmcli/name-from-folder@^1.0.1": - "version" "1.0.1" + version "1.0.1" "@npmcli/node-gyp@^3.0.0": - "version" "3.0.0" + version "3.0.0" "@npmcli/package-json@^3.0.0": - "version" "3.0.0" + version "3.0.0" dependencies: - "json-parse-even-better-errors" "^3.0.0" + json-parse-even-better-errors "^3.0.0" "@npmcli/promise-spawn@^6.0.0", "@npmcli/promise-spawn@^6.0.1": - "version" "6.0.1" + version "6.0.1" dependencies: - "which" "^3.0.0" + which "^3.0.0" "@npmcli/query@^3.0.0": - "version" "3.0.0" + version "3.0.0" dependencies: - "postcss-selector-parser" "^6.0.10" + postcss-selector-parser "^6.0.10" "@npmcli/run-script@^6.0.0": - "version" "6.0.0" + version "6.0.0" dependencies: "@npmcli/node-gyp" "^3.0.0" "@npmcli/promise-spawn" "^6.0.0" - "node-gyp" "^9.0.0" - "read-package-json-fast" "^3.0.0" - "which" "^3.0.0" + node-gyp "^9.0.0" + read-package-json-fast "^3.0.0" + which "^3.0.0" "@swc/core-linux-x64-gnu@1.3.22": - "integrity" "sha512-FLkbiqsdXsVIFZi6iedx4rSBGX8x0vo/5aDlklSxJAAYOcQpO0QADKP5Yr65iMT1d6ABCt2d+/StpGLF7GWOcA==" - "resolved" "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.22.tgz" - "version" "1.3.22" + version "1.3.22" + resolved "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.22.tgz" + integrity sha512-FLkbiqsdXsVIFZi6iedx4rSBGX8x0vo/5aDlklSxJAAYOcQpO0QADKP5Yr65iMT1d6ABCt2d+/StpGLF7GWOcA== "@swc/core-linux-x64-musl@1.3.22": - "integrity" "sha512-giBuw+Z0Bq6fpZ0Y5TcfpcQwf9p/cE1fOQyO/K1XSTn/haQOqFi7421Jq/dFThSARZiXw1u9Om9VFbwxr8VI+A==" - "resolved" "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.22.tgz" - "version" "1.3.22" + version "1.3.22" + resolved "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.22.tgz" + integrity sha512-giBuw+Z0Bq6fpZ0Y5TcfpcQwf9p/cE1fOQyO/K1XSTn/haQOqFi7421Jq/dFThSARZiXw1u9Om9VFbwxr8VI+A== "@swc/core@^1.3.21": - "integrity" "sha512-oQ9EPEb7NgWcGIDoVfLCuffvtC4MzVtrwjqwKzFHP8FUh1fn8+2wraOjkkDXW74BB4Hgve5ykkaHix9bebB9Ww==" - "resolved" "https://registry.npmjs.org/@swc/core/-/core-1.3.22.tgz" - "version" "1.3.22" + version "1.3.22" + resolved "https://registry.npmjs.org/@swc/core/-/core-1.3.22.tgz" + integrity sha512-oQ9EPEb7NgWcGIDoVfLCuffvtC4MzVtrwjqwKzFHP8FUh1fn8+2wraOjkkDXW74BB4Hgve5ykkaHix9bebB9Ww== optionalDependencies: "@swc/core-darwin-arm64" "1.3.22" "@swc/core-darwin-x64" "1.3.22" @@ -260,888 +260,888 @@ "@swc/core-win32-x64-msvc" "1.3.22" "@tailwindcss/line-clamp@^0.4.2": - "integrity" "sha512-HFzAQuqYCjyy/SX9sLGB1lroPzmcnWv1FHkIpmypte10hptf4oPUfucryMKovZh2u0uiS9U5Ty3GghWfEJGwVw==" - "resolved" "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.2.tgz" - "version" "0.4.2" + version "0.4.2" + resolved "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.2.tgz" + integrity sha512-HFzAQuqYCjyy/SX9sLGB1lroPzmcnWv1FHkIpmypte10hptf4oPUfucryMKovZh2u0uiS9U5Ty3GghWfEJGwVw== "@tootallnate/once@2": - "version" "2.0.0" + version "2.0.0" "@types/chrome@*": - "integrity" "sha512-VSjQu1k6a/rAfuqR1Gi/oxHZj4+t6+LG+GobNI3ZWI6DQ+fmphNSF6TrLHG6BYK2bXc9Gb4c1uXFKRRVLaGl5Q==" - "resolved" "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.210.tgz" - "version" "0.0.210" + 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" "*" "@types/chromecast-caf-sender@^1.0.5": - "integrity" "sha512-8d6RRCOYYiKzDyFJKAYKOp7Eo0kUfj9imnLQj0uuh/QGSz8euL9OOeKmh8XizqTcKW5tXva6li0mRYtnvzVIcA==" - "resolved" "https://registry.npmjs.org/@types/chromecast-caf-sender/-/chromecast-caf-sender-1.0.5.tgz" - "version" "1.0.5" + version "1.0.5" + resolved "https://registry.npmjs.org/@types/chromecast-caf-sender/-/chromecast-caf-sender-1.0.5.tgz" + integrity sha512-8d6RRCOYYiKzDyFJKAYKOp7Eo0kUfj9imnLQj0uuh/QGSz8euL9OOeKmh8XizqTcKW5tXva6li0mRYtnvzVIcA== dependencies: "@types/chrome" "*" "@types/crypto-js@^4.1.1": - "integrity" "sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==" - "resolved" "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.1.tgz" - "version" "4.1.1" + version "4.1.1" + resolved "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.1.tgz" + integrity sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA== "@types/filesystem@*": - "integrity" "sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ==" - "resolved" "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.32.tgz" - "version" "0.0.32" + version "0.0.32" + resolved "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.32.tgz" + integrity sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ== dependencies: "@types/filewriter" "*" "@types/filewriter@*": - "integrity" "sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==" - "resolved" "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.29.tgz" - "version" "0.0.29" + version "0.0.29" + resolved "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.29.tgz" + integrity sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ== "@types/fscreen@^1.0.1": - "integrity" "sha512-hV2d0BreihMGtrg+EdAFOIl/O2EL5vhAheHJUztGE/lPFZIN8ZCpGFL8hCbtyi1CfhKjDRCf47sHjP+FwJ4q0Q==" - "resolved" "https://registry.npmjs.org/@types/fscreen/-/fscreen-1.0.1.tgz" - "version" "1.0.1" + version "1.0.1" + resolved "https://registry.npmjs.org/@types/fscreen/-/fscreen-1.0.1.tgz" + integrity sha512-hV2d0BreihMGtrg+EdAFOIl/O2EL5vhAheHJUztGE/lPFZIN8ZCpGFL8hCbtyi1CfhKjDRCf47sHjP+FwJ4q0Q== "@types/har-format@*": - "integrity" "sha512-o0J30wqycjF5miWDKYKKzzOU1ZTLuA42HZ4HE7/zqTOc/jTLdQ5NhYWvsRQo45Nfi1KHoRdNhteSI4BAxTF1Pg==" - "resolved" "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.10.tgz" - "version" "1.2.10" + version "1.2.10" + resolved "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.10.tgz" + integrity sha512-o0J30wqycjF5miWDKYKKzzOU1ZTLuA42HZ4HE7/zqTOc/jTLdQ5NhYWvsRQo45Nfi1KHoRdNhteSI4BAxTF1Pg== "@types/history@^4.7.11": - "integrity" "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==" - "resolved" "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz" - "version" "4.7.11" + version "4.7.11" + resolved "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz" + integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== "@types/json-schema@^7.0.9": - "integrity" "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==" - "resolved" "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" - "version" "7.0.11" + version "7.0.11" + resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== "@types/json5@^0.0.29": - "integrity" "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" - "resolved" "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" - "version" "0.0.29" + version "0.0.29" + resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== "@types/lodash.throttle@^4.1.7": - "integrity" "sha512-znwGDpjCHQ4FpLLx19w4OXDqq8+OvREa05H89obtSyXyOFKL3dDjCslsmfBz0T2FU8dmf5Wx1QvogbINiGIu9g==" - "resolved" "https://registry.npmjs.org/@types/lodash.throttle/-/lodash.throttle-4.1.7.tgz" - "version" "4.1.7" + version "4.1.7" + resolved "https://registry.npmjs.org/@types/lodash.throttle/-/lodash.throttle-4.1.7.tgz" + integrity sha512-znwGDpjCHQ4FpLLx19w4OXDqq8+OvREa05H89obtSyXyOFKL3dDjCslsmfBz0T2FU8dmf5Wx1QvogbINiGIu9g== dependencies: "@types/lodash" "*" "@types/lodash@*": - "integrity" "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==" - "resolved" "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz" - "version" "4.14.191" + version "4.14.191" + resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz" + integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== "@types/node@^17.0.15", "@types/node@>= 14": - "integrity" "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==" - "resolved" "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz" - "version" "17.0.45" + version "17.0.45" + resolved "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz" + integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== "@types/prop-types@*": - "integrity" "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" - "resolved" "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz" - "version" "15.7.5" + version "15.7.5" + resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz" + integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== "@types/react-dom@^17.0.11": - "integrity" "sha512-rLVtIfbwyur2iFKykP2w0pl/1unw26b5td16d5xMgp7/yjTHomkyxPYChFoCr/FtEX1lN9wY6lFj1qvKdS5kDw==" - "resolved" "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.18.tgz" - "version" "17.0.18" + version "17.0.18" + resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.18.tgz" + integrity sha512-rLVtIfbwyur2iFKykP2w0pl/1unw26b5td16d5xMgp7/yjTHomkyxPYChFoCr/FtEX1lN9wY6lFj1qvKdS5kDw== dependencies: "@types/react" "^17" "@types/react-helmet@^6.1.6": - "integrity" "sha512-ZKcoOdW/Tg+kiUbkFCBtvDw0k3nD4HJ/h/B9yWxN4uDO8OkRksWTO+EL+z/Qu3aHTeTll3Ro0Cc/8UhwBCMG5A==" - "resolved" "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.6.tgz" - "version" "6.1.6" + version "6.1.6" + resolved "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.6.tgz" + integrity sha512-ZKcoOdW/Tg+kiUbkFCBtvDw0k3nD4HJ/h/B9yWxN4uDO8OkRksWTO+EL+z/Qu3aHTeTll3Ro0Cc/8UhwBCMG5A== dependencies: "@types/react" "*" "@types/react-router-dom@^5.3.3": - "integrity" "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==" - "resolved" "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz" - "version" "5.3.3" + version "5.3.3" + resolved "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz" + integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== dependencies: "@types/history" "^4.7.11" "@types/react" "*" "@types/react-router" "*" "@types/react-router@*", "@types/react-router@^5.1.18": - "integrity" "sha512-Fv/5kb2STAEMT3wHzdKQK2z8xKq38EDIGVrutYLmQVVLe+4orDFquU52hQrULnEHinMKv9FSA6lf9+uNT1ITtA==" - "resolved" "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.19.tgz" - "version" "5.1.19" + version "5.1.19" + resolved "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.19.tgz" + integrity sha512-Fv/5kb2STAEMT3wHzdKQK2z8xKq38EDIGVrutYLmQVVLe+4orDFquU52hQrULnEHinMKv9FSA6lf9+uNT1ITtA== dependencies: "@types/history" "^4.7.11" "@types/react" "*" "@types/react-stickynode@^4.0.0": - "integrity" "sha512-PKkmOzF6WCNuyIKrvhidGeUPLfe8htPwfEljKnQBF4bA5v74ADvXtwkjavOH8i6aCSw9J14AyDDl1Ul0VNQJUg==" - "resolved" "https://registry.npmjs.org/@types/react-stickynode/-/react-stickynode-4.0.0.tgz" - "version" "4.0.0" + version "4.0.0" + resolved "https://registry.npmjs.org/@types/react-stickynode/-/react-stickynode-4.0.0.tgz" + integrity sha512-PKkmOzF6WCNuyIKrvhidGeUPLfe8htPwfEljKnQBF4bA5v74ADvXtwkjavOH8i6aCSw9J14AyDDl1Ul0VNQJUg== dependencies: "@types/react" "*" "@types/react-transition-group@^4.4.5": - "integrity" "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==" - "resolved" "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz" - "version" "4.4.5" + version "4.4.5" + resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz" + integrity sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA== dependencies: "@types/react" "*" "@types/react@*", "@types/react@^17", "@types/react@^17.0.39": - "integrity" "sha512-vwk8QqVODi0VaZZpDXQCmEmiOuyjEFPY7Ttaw5vjM112LOq37yz1CDJGrRJwA1fYEq4Iitd5rnjd1yWAc/bT+A==" - "resolved" "https://registry.npmjs.org/@types/react/-/react-17.0.52.tgz" - "version" "17.0.52" + version "17.0.52" + resolved "https://registry.npmjs.org/@types/react/-/react-17.0.52.tgz" + integrity sha512-vwk8QqVODi0VaZZpDXQCmEmiOuyjEFPY7Ttaw5vjM112LOq37yz1CDJGrRJwA1fYEq4Iitd5rnjd1yWAc/bT+A== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" - "csstype" "^3.0.2" + csstype "^3.0.2" "@types/scheduler@*": - "integrity" "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" - "resolved" "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz" - "version" "0.16.2" + version "0.16.2" + resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz" + integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== "@types/semver@^7.3.12": - "integrity" "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==" - "resolved" "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz" - "version" "7.3.13" + version "7.3.13" + resolved "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz" + integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== "@typescript-eslint/eslint-plugin@^5.13.0": - "integrity" "sha512-YpzNv3aayRBwjs4J3oz65eVLXc9xx0PDbIRisHj+dYhvBn02MjYOD96P8YGiWEIFBrojaUjxvkaUpakD82phsA==" - "resolved" "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.46.1.tgz" - "version" "5.46.1" + version "5.46.1" + resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.46.1.tgz" + integrity sha512-YpzNv3aayRBwjs4J3oz65eVLXc9xx0PDbIRisHj+dYhvBn02MjYOD96P8YGiWEIFBrojaUjxvkaUpakD82phsA== dependencies: "@typescript-eslint/scope-manager" "5.46.1" "@typescript-eslint/type-utils" "5.46.1" "@typescript-eslint/utils" "5.46.1" - "debug" "^4.3.4" - "ignore" "^5.2.0" - "natural-compare-lite" "^1.4.0" - "regexpp" "^3.2.0" - "semver" "^7.3.7" - "tsutils" "^3.21.0" + debug "^4.3.4" + ignore "^5.2.0" + natural-compare-lite "^1.4.0" + regexpp "^3.2.0" + semver "^7.3.7" + tsutils "^3.21.0" "@typescript-eslint/parser@^5.0.0", "@typescript-eslint/parser@^5.13.0": - "integrity" "sha512-RelQ5cGypPh4ySAtfIMBzBGyrNerQcmfA1oJvPj5f+H4jI59rl9xxpn4bonC0tQvUKOEN7eGBFWxFLK3Xepneg==" - "resolved" "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.46.1.tgz" - "version" "5.46.1" + version "5.46.1" + resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.46.1.tgz" + integrity sha512-RelQ5cGypPh4ySAtfIMBzBGyrNerQcmfA1oJvPj5f+H4jI59rl9xxpn4bonC0tQvUKOEN7eGBFWxFLK3Xepneg== dependencies: "@typescript-eslint/scope-manager" "5.46.1" "@typescript-eslint/types" "5.46.1" "@typescript-eslint/typescript-estree" "5.46.1" - "debug" "^4.3.4" + debug "^4.3.4" "@typescript-eslint/scope-manager@5.46.1": - "integrity" "sha512-iOChVivo4jpwUdrJZyXSMrEIM/PvsbbDOX1y3UCKjSgWn+W89skxWaYXACQfxmIGhPVpRWK/VWPYc+bad6smIA==" - "resolved" "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.46.1.tgz" - "version" "5.46.1" + version "5.46.1" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.46.1.tgz" + integrity sha512-iOChVivo4jpwUdrJZyXSMrEIM/PvsbbDOX1y3UCKjSgWn+W89skxWaYXACQfxmIGhPVpRWK/VWPYc+bad6smIA== dependencies: "@typescript-eslint/types" "5.46.1" "@typescript-eslint/visitor-keys" "5.46.1" "@typescript-eslint/type-utils@5.46.1": - "integrity" "sha512-V/zMyfI+jDmL1ADxfDxjZ0EMbtiVqj8LUGPAGyBkXXStWmCUErMpW873zEHsyguWCuq2iN4BrlWUkmuVj84yng==" - "resolved" "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.46.1.tgz" - "version" "5.46.1" + version "5.46.1" + resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.46.1.tgz" + integrity sha512-V/zMyfI+jDmL1ADxfDxjZ0EMbtiVqj8LUGPAGyBkXXStWmCUErMpW873zEHsyguWCuq2iN4BrlWUkmuVj84yng== dependencies: "@typescript-eslint/typescript-estree" "5.46.1" "@typescript-eslint/utils" "5.46.1" - "debug" "^4.3.4" - "tsutils" "^3.21.0" + debug "^4.3.4" + tsutils "^3.21.0" "@typescript-eslint/types@5.46.1": - "integrity" "sha512-Z5pvlCaZgU+93ryiYUwGwLl9AQVB/PQ1TsJ9NZ/gHzZjN7g9IAn6RSDkpCV8hqTwAiaj6fmCcKSQeBPlIpW28w==" - "resolved" "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.46.1.tgz" - "version" "5.46.1" + version "5.46.1" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.46.1.tgz" + integrity sha512-Z5pvlCaZgU+93ryiYUwGwLl9AQVB/PQ1TsJ9NZ/gHzZjN7g9IAn6RSDkpCV8hqTwAiaj6fmCcKSQeBPlIpW28w== "@typescript-eslint/typescript-estree@5.46.1": - "integrity" "sha512-j9W4t67QiNp90kh5Nbr1w92wzt+toiIsaVPnEblB2Ih2U9fqBTyqV9T3pYWZBRt6QoMh/zVWP59EpuCjc4VRBg==" - "resolved" "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.46.1.tgz" - "version" "5.46.1" + version "5.46.1" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.46.1.tgz" + integrity sha512-j9W4t67QiNp90kh5Nbr1w92wzt+toiIsaVPnEblB2Ih2U9fqBTyqV9T3pYWZBRt6QoMh/zVWP59EpuCjc4VRBg== dependencies: "@typescript-eslint/types" "5.46.1" "@typescript-eslint/visitor-keys" "5.46.1" - "debug" "^4.3.4" - "globby" "^11.1.0" - "is-glob" "^4.0.3" - "semver" "^7.3.7" - "tsutils" "^3.21.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" "@typescript-eslint/utils@5.46.1": - "integrity" "sha512-RBdBAGv3oEpFojaCYT4Ghn4775pdjvwfDOfQ2P6qzNVgQOVrnSPe5/Pb88kv7xzYQjoio0eKHKB9GJ16ieSxvA==" - "resolved" "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.46.1.tgz" - "version" "5.46.1" + version "5.46.1" + resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.46.1.tgz" + integrity sha512-RBdBAGv3oEpFojaCYT4Ghn4775pdjvwfDOfQ2P6qzNVgQOVrnSPe5/Pb88kv7xzYQjoio0eKHKB9GJ16ieSxvA== dependencies: "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" "@typescript-eslint/scope-manager" "5.46.1" "@typescript-eslint/types" "5.46.1" "@typescript-eslint/typescript-estree" "5.46.1" - "eslint-scope" "^5.1.1" - "eslint-utils" "^3.0.0" - "semver" "^7.3.7" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + semver "^7.3.7" "@typescript-eslint/visitor-keys@5.46.1": - "integrity" "sha512-jczZ9noovXwy59KjRTk1OftT78pwygdcmCuBf8yMoWt/8O8l+6x2LSEze0E4TeepXK4MezW3zGSyoDRZK7Y9cg==" - "resolved" "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.46.1.tgz" - "version" "5.46.1" + version "5.46.1" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.46.1.tgz" + integrity sha512-jczZ9noovXwy59KjRTk1OftT78pwygdcmCuBf8yMoWt/8O8l+6x2LSEze0E4TeepXK4MezW3zGSyoDRZK7Y9cg== dependencies: "@typescript-eslint/types" "5.46.1" - "eslint-visitor-keys" "^3.3.0" + eslint-visitor-keys "^3.3.0" "@vitejs/plugin-react-swc@^3.0.0": - "integrity" "sha512-vYlodz/mjYRbxMGbHzDgR8aPR+z8n7K/enWkyBGH096xrL2DIPCuTvQVRYPTXGyy6wO7OFiMxZ3r4nKQD1sH0A==" - "resolved" "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.0.0.tgz" - "version" "3.0.0" + version "3.0.0" + resolved "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.0.0.tgz" + integrity sha512-vYlodz/mjYRbxMGbHzDgR8aPR+z8n7K/enWkyBGH096xrL2DIPCuTvQVRYPTXGyy6wO7OFiMxZ3r4nKQD1sH0A== dependencies: "@swc/core" "^1.3.21" -"abbrev@^1.0.0": - "version" "1.1.1" +abbrev@^1.0.0: + version "1.1.1" -"abbrev@^2.0.0": - "version" "2.0.0" +abbrev@^2.0.0: + version "2.0.0" -"abort-controller@^3.0.0": - "version" "3.0.0" +abort-controller@^3.0.0: + version "3.0.0" dependencies: - "event-target-shim" "^5.0.0" + event-target-shim "^5.0.0" -"acorn-jsx@^5.3.2": - "integrity" "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==" - "resolved" "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" - "version" "5.3.2" +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -"acorn-node@^1.8.2": - "integrity" "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==" - "resolved" "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz" - "version" "1.8.2" +acorn-node@^1.8.2: + version "1.8.2" + resolved "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz" + integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A== dependencies: - "acorn" "^7.0.0" - "acorn-walk" "^7.0.0" - "xtend" "^4.0.2" + acorn "^7.0.0" + acorn-walk "^7.0.0" + xtend "^4.0.2" -"acorn-walk@^7.0.0": - "integrity" "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==" - "resolved" "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz" - "version" "7.2.0" +acorn-walk@^7.0.0: + version "7.2.0" + resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz" + integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== -"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", "acorn@^8.8.0": - "integrity" "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==" - "resolved" "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz" - "version" "8.8.1" +"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.8.0: + version "8.8.1" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz" + integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== -"acorn@^7.0.0": - "integrity" "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" - "resolved" "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" - "version" "7.4.1" +acorn@^7.0.0: + version "7.4.1" + resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -"agent-base@^6.0.2", "agent-base@6": - "version" "6.0.2" +agent-base@^6.0.2, agent-base@6: + version "6.0.2" dependencies: - "debug" "4" + debug "4" -"agentkeepalive@^4.2.1": - "version" "4.2.1" +agentkeepalive@^4.2.1: + version "4.2.1" dependencies: - "debug" "^4.1.0" - "depd" "^1.1.2" - "humanize-ms" "^1.2.1" + debug "^4.1.0" + depd "^1.1.2" + humanize-ms "^1.2.1" -"aggregate-error@^3.0.0": - "version" "3.1.0" +aggregate-error@^3.0.0: + version "3.1.0" dependencies: - "clean-stack" "^2.0.0" - "indent-string" "^4.0.0" + clean-stack "^2.0.0" + indent-string "^4.0.0" -"ajv@^6.10.0", "ajv@^6.12.4": - "integrity" "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==" - "resolved" "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" - "version" "6.12.6" +ajv@^6.10.0, ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== dependencies: - "fast-deep-equal" "^3.1.1" - "fast-json-stable-stringify" "^2.0.0" - "json-schema-traverse" "^0.4.1" - "uri-js" "^4.2.2" + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" -"ansi-regex@^5.0.1": - "integrity" "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - "resolved" "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" - "version" "5.0.1" +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -"ansi-styles@^4.1.0", "ansi-styles@^4.3.0": - "integrity" "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==" - "resolved" "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" - "version" "4.3.0" +ansi-styles@^4.1.0, ansi-styles@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: - "color-convert" "^2.0.1" + color-convert "^2.0.1" -"anymatch@~3.1.2": - "integrity" "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==" - "resolved" "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" - "version" "3.1.3" +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== dependencies: - "normalize-path" "^3.0.0" - "picomatch" "^2.0.4" + normalize-path "^3.0.0" + picomatch "^2.0.4" -"aproba@^1.0.3 || ^2.0.0", "aproba@^2.0.0": - "version" "2.0.0" +"aproba@^1.0.3 || ^2.0.0", aproba@^2.0.0: + version "2.0.0" -"archy@~1.0.0": - "version" "1.0.0" +archy@~1.0.0: + version "1.0.0" -"are-we-there-yet@^3.0.0": - "version" "3.0.1" +are-we-there-yet@^3.0.0: + version "3.0.1" dependencies: - "delegates" "^1.0.0" - "readable-stream" "^3.6.0" + delegates "^1.0.0" + readable-stream "^3.6.0" -"are-we-there-yet@^4.0.0": - "version" "4.0.0" +are-we-there-yet@^4.0.0: + version "4.0.0" dependencies: - "delegates" "^1.0.0" - "readable-stream" "^4.1.0" + delegates "^1.0.0" + readable-stream "^4.1.0" -"arg@^5.0.2": - "integrity" "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" - "resolved" "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz" - "version" "5.0.2" +arg@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz" + integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== -"argparse@^2.0.1": - "integrity" "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - "resolved" "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" - "version" "2.0.1" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -"aria-query@^4.2.2": - "integrity" "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==" - "resolved" "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz" - "version" "4.2.2" +aria-query@^4.2.2: + version "4.2.2" + resolved "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz" + integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== dependencies: "@babel/runtime" "^7.10.2" "@babel/runtime-corejs3" "^7.10.2" -"array-includes@^3.1.4", "array-includes@^3.1.5": - "integrity" "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==" - "resolved" "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz" - "version" "3.1.6" - dependencies: - "call-bind" "^1.0.2" - "define-properties" "^1.1.4" - "es-abstract" "^1.20.4" - "get-intrinsic" "^1.1.3" - "is-string" "^1.0.7" - -"array-union@^2.1.0": - "integrity" "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" - "resolved" "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" - "version" "2.1.0" - -"array.prototype.flat@^1.2.5": - "integrity" "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==" - "resolved" "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz" - "version" "1.3.1" - dependencies: - "call-bind" "^1.0.2" - "define-properties" "^1.1.4" - "es-abstract" "^1.20.4" - "es-shim-unscopables" "^1.0.0" - -"array.prototype.flatmap@^1.2.5": - "integrity" "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==" - "resolved" "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz" - "version" "1.3.1" - dependencies: - "call-bind" "^1.0.2" - "define-properties" "^1.1.4" - "es-abstract" "^1.20.4" - "es-shim-unscopables" "^1.0.0" - -"ast-types-flow@^0.0.7": - "integrity" "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==" - "resolved" "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz" - "version" "0.0.7" - -"autoprefixer@^10.4.13": - "integrity" "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==" - "resolved" "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz" - "version" "10.4.13" - dependencies: - "browserslist" "^4.21.4" - "caniuse-lite" "^1.0.30001426" - "fraction.js" "^4.2.0" - "normalize-range" "^0.1.2" - "picocolors" "^1.0.0" - "postcss-value-parser" "^4.2.0" - -"axe-core@^4.4.3": - "integrity" "sha512-L3ZNbXPTxMrl0+qTXAzn9FBRvk5XdO56K8CvcCKtlxv44Aw2w2NCclGuvCWxHPw1Riiq3ncP/sxFYj2nUqdoTw==" - "resolved" "https://registry.npmjs.org/axe-core/-/axe-core-4.6.0.tgz" - "version" "4.6.0" - -"axobject-query@^2.2.0": - "integrity" "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==" - "resolved" "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz" - "version" "2.2.0" - -"balanced-match@^1.0.0": - "integrity" "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - "resolved" "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" - "version" "1.0.2" - -"base64-js@^1.3.1": - "version" "1.5.1" - -"bin-links@^4.0.1": - "version" "4.0.1" - dependencies: - "cmd-shim" "^6.0.0" - "npm-normalize-package-bin" "^3.0.0" - "read-cmd-shim" "^4.0.0" - "write-file-atomic" "^5.0.0" - -"binary-extensions@^2.0.0": - "integrity" "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" - "resolved" "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" - "version" "2.2.0" - -"binary-extensions@^2.2.0": - "version" "2.2.0" - -"brace-expansion@^1.1.7": - "integrity" "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==" - "resolved" "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" - "version" "1.1.11" - dependencies: - "balanced-match" "^1.0.0" - "concat-map" "0.0.1" - -"brace-expansion@^2.0.1": - "version" "2.0.1" - dependencies: - "balanced-match" "^1.0.0" - -"braces@^3.0.2", "braces@~3.0.2": - "integrity" "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==" - "resolved" "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" - "version" "3.0.2" +array-includes@^3.1.4, array-includes@^3.1.5: + version "3.1.6" + resolved "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz" + integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + get-intrinsic "^1.1.3" + is-string "^1.0.7" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array.prototype.flat@^1.2.5: + version "1.3.1" + resolved "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz" + integrity sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + +array.prototype.flatmap@^1.2.5: + version "1.3.1" + resolved "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz" + integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + +ast-types-flow@^0.0.7: + version "0.0.7" + resolved "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz" + integrity sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag== + +autoprefixer@^10.4.13: + version "10.4.13" + resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz" + integrity sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg== + dependencies: + browserslist "^4.21.4" + caniuse-lite "^1.0.30001426" + fraction.js "^4.2.0" + normalize-range "^0.1.2" + picocolors "^1.0.0" + postcss-value-parser "^4.2.0" + +axe-core@^4.4.3: + version "4.6.0" + resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.6.0.tgz" + integrity sha512-L3ZNbXPTxMrl0+qTXAzn9FBRvk5XdO56K8CvcCKtlxv44Aw2w2NCclGuvCWxHPw1Riiq3ncP/sxFYj2nUqdoTw== + +axobject-query@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz" + integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64-js@^1.3.1: + version "1.5.1" + +bin-links@^4.0.1: + version "4.0.1" + dependencies: + cmd-shim "^6.0.0" + npm-normalize-package-bin "^3.0.0" + read-cmd-shim "^4.0.0" + write-file-atomic "^5.0.0" + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +binary-extensions@^2.2.0: + version "2.2.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== dependencies: - "fill-range" "^7.0.1" - -"browserslist@^4.21.4", "browserslist@>= 4.21.0": - "integrity" "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==" - "resolved" "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz" - "version" "4.21.4" - dependencies: - "caniuse-lite" "^1.0.30001400" - "electron-to-chromium" "^1.4.251" - "node-releases" "^2.0.6" - "update-browserslist-db" "^1.0.9" + fill-range "^7.0.1" + +browserslist@^4.21.4, "browserslist@>= 4.21.0": + version "4.21.4" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz" + integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw== + dependencies: + caniuse-lite "^1.0.30001400" + electron-to-chromium "^1.4.251" + node-releases "^2.0.6" + update-browserslist-db "^1.0.9" -"buffer@^6.0.3": - "version" "6.0.3" +buffer@^6.0.3: + version "6.0.3" dependencies: - "base64-js" "^1.3.1" - "ieee754" "^1.2.1" + base64-js "^1.3.1" + ieee754 "^1.2.1" -"builtins@^5.0.0": - "version" "5.0.1" +builtins@^5.0.0: + version "5.0.1" dependencies: - "semver" "^7.0.0" + semver "^7.0.0" -"cacache@^16.1.0": - "version" "16.1.3" +cacache@^16.1.0: + version "16.1.3" dependencies: "@npmcli/fs" "^2.1.0" "@npmcli/move-file" "^2.0.0" - "chownr" "^2.0.0" - "fs-minipass" "^2.1.0" - "glob" "^8.0.1" - "infer-owner" "^1.0.4" - "lru-cache" "^7.7.1" - "minipass" "^3.1.6" - "minipass-collect" "^1.0.2" - "minipass-flush" "^1.0.5" - "minipass-pipeline" "^1.2.4" - "mkdirp" "^1.0.4" - "p-map" "^4.0.0" - "promise-inflight" "^1.0.1" - "rimraf" "^3.0.2" - "ssri" "^9.0.0" - "tar" "^6.1.11" - "unique-filename" "^2.0.0" - -"cacache@^17.0.0", "cacache@^17.0.3": - "version" "17.0.3" + chownr "^2.0.0" + fs-minipass "^2.1.0" + glob "^8.0.1" + infer-owner "^1.0.4" + lru-cache "^7.7.1" + minipass "^3.1.6" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + mkdirp "^1.0.4" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^9.0.0" + tar "^6.1.11" + unique-filename "^2.0.0" + +cacache@^17.0.0, cacache@^17.0.3: + version "17.0.3" dependencies: "@npmcli/fs" "^3.1.0" - "fs-minipass" "^2.1.0" - "glob" "^8.0.1" - "lru-cache" "^7.7.1" - "minipass" "^4.0.0" - "minipass-collect" "^1.0.2" - "minipass-flush" "^1.0.5" - "minipass-pipeline" "^1.2.4" - "p-map" "^4.0.0" - "promise-inflight" "^1.0.1" - "ssri" "^10.0.0" - "tar" "^6.1.11" - "unique-filename" "^3.0.0" - -"call-bind@^1.0.0", "call-bind@^1.0.2": - "integrity" "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==" - "resolved" "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" - "version" "1.0.2" - dependencies: - "function-bind" "^1.1.1" - "get-intrinsic" "^1.0.2" - -"callsites@^3.0.0": - "integrity" "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" - "resolved" "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" - "version" "3.1.0" - -"camelcase-css@^2.0.1": - "integrity" "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==" - "resolved" "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz" - "version" "2.0.1" - -"caniuse-lite@^1.0.30001400", "caniuse-lite@^1.0.30001426": - "integrity" "sha512-1MgUzEkoMO6gKfXflStpYgZDlFM7M/ck/bgfVCACO5vnAf0fXoNVHdWtqGU+MYca+4bL9Z5bpOVmR33cWW9G2A==" - "resolved" "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001439.tgz" - "version" "1.0.30001439" - -"chalk@^4.0.0", "chalk@^4.1.0", "chalk@^4.1.2": - "integrity" "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==" - "resolved" "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" - "version" "4.1.2" - dependencies: - "ansi-styles" "^4.1.0" - "supports-color" "^7.1.0" - -"chokidar@^3.5.3": - "integrity" "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==" - "resolved" "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" - "version" "3.5.3" - dependencies: - "anymatch" "~3.1.2" - "braces" "~3.0.2" - "glob-parent" "~5.1.2" - "is-binary-path" "~2.1.0" - "is-glob" "~4.0.1" - "normalize-path" "~3.0.0" - "readdirp" "~3.6.0" + fs-minipass "^2.1.0" + glob "^8.0.1" + lru-cache "^7.7.1" + minipass "^4.0.0" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + p-map "^4.0.0" + promise-inflight "^1.0.1" + ssri "^10.0.0" + tar "^6.1.11" + unique-filename "^3.0.0" + +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase-css@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz" + integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== + +caniuse-lite@^1.0.30001400, caniuse-lite@^1.0.30001426: + version "1.0.30001439" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001439.tgz" + integrity sha512-1MgUzEkoMO6gKfXflStpYgZDlFM7M/ck/bgfVCACO5vnAf0fXoNVHdWtqGU+MYca+4bL9Z5bpOVmR33cWW9G2A== + +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@^3.5.3: + version "3.5.3" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" optionalDependencies: - "fsevents" "~2.3.2" + fsevents "~2.3.2" -"chownr@^2.0.0": - "version" "2.0.0" +chownr@^2.0.0: + version "2.0.0" -"ci-info@^3.7.0": - "version" "3.7.0" +ci-info@^3.7.0: + version "3.7.0" -"cidr-regex@^3.1.1": - "version" "3.1.1" +cidr-regex@^3.1.1: + version "3.1.1" dependencies: - "ip-regex" "^4.1.0" + ip-regex "^4.1.0" -"classnames@^2.0.0": - "integrity" "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" - "resolved" "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz" - "version" "2.3.2" +classnames@^2.0.0: + version "2.3.2" + resolved "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz" + integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== -"clean-stack@^2.0.0": - "version" "2.2.0" +clean-stack@^2.0.0: + version "2.2.0" -"cli-columns@^4.0.0": - "version" "4.0.0" +cli-columns@^4.0.0: + version "4.0.0" dependencies: - "string-width" "^4.2.3" - "strip-ansi" "^6.0.1" + string-width "^4.2.3" + strip-ansi "^6.0.1" -"cli-table3@^0.6.3": - "version" "0.6.3" +cli-table3@^0.6.3: + version "0.6.3" dependencies: - "string-width" "^4.2.0" + string-width "^4.2.0" optionalDependencies: "@colors/colors" "1.5.0" -"client-only@^0.0.1": - "integrity" "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" - "resolved" "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz" - "version" "0.0.1" +client-only@^0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz" + integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== -"clone@^1.0.2": - "version" "1.0.4" +clone@^1.0.2: + version "1.0.4" -"cmd-shim@^6.0.0": - "version" "6.0.0" +cmd-shim@^6.0.0: + version "6.0.0" -"color-convert@^2.0.1": - "integrity" "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==" - "resolved" "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" - "version" "2.0.1" +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== dependencies: - "color-name" "~1.1.4" + color-name "~1.1.4" -"color-name@^1.1.4", "color-name@~1.1.4": - "integrity" "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - "resolved" "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" - "version" "1.1.4" +color-name@^1.1.4, color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -"color-support@^1.1.3": - "version" "1.1.3" +color-support@^1.1.3: + version "1.1.3" -"columnify@^1.6.0": - "version" "1.6.0" +columnify@^1.6.0: + version "1.6.0" dependencies: - "strip-ansi" "^6.0.1" - "wcwidth" "^1.0.0" + strip-ansi "^6.0.1" + wcwidth "^1.0.0" -"common-ancestor-path@^1.0.1": - "version" "1.0.1" +common-ancestor-path@^1.0.1: + version "1.0.1" -"concat-map@0.0.1": - "integrity" "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - "resolved" "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - "version" "0.0.1" +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -"confusing-browser-globals@^1.0.10": - "integrity" "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==" - "resolved" "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz" - "version" "1.0.11" +confusing-browser-globals@^1.0.10: + version "1.0.11" + resolved "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz" + integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA== -"console-control-strings@^1.1.0": - "version" "1.1.0" +console-control-strings@^1.1.0: + version "1.1.0" -"core-js-pure@^3.25.1": - "integrity" "sha512-VVXcDpp/xJ21KdULRq/lXdLzQAtX7+37LzpyfFM973il0tWSsDEoyzG38G14AjTpK9VTfiNM9jnFauq/CpaWGQ==" - "resolved" "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.26.1.tgz" - "version" "3.26.1" +core-js-pure@^3.25.1: + version "3.26.1" + resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.26.1.tgz" + integrity sha512-VVXcDpp/xJ21KdULRq/lXdLzQAtX7+37LzpyfFM973il0tWSsDEoyzG38G14AjTpK9VTfiNM9jnFauq/CpaWGQ== -"core-js@^3.6.5": - "integrity" "sha512-GutwJLBChfGCpwwhbYoqfv03LAfmiz7e7D/BNxzeMxwQf10GRSzqiOjx7AmtEk+heiD/JWmBuyBPgFtx0Sg1ww==" - "resolved" "https://registry.npmjs.org/core-js/-/core-js-3.27.1.tgz" - "version" "3.27.1" +core-js@^3.6.5: + 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": - "integrity" "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==" - "resolved" "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" - "version" "7.0.3" +cross-spawn@^7.0.2: + version "7.0.3" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== dependencies: - "path-key" "^3.1.0" - "shebang-command" "^2.0.0" - "which" "^2.0.1" + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" -"crypto-js@^4.1.1": - "integrity" "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" - "resolved" "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz" - "version" "4.1.1" +crypto-js@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz" + integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw== -"cssesc@^3.0.0": - "integrity" "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" - "resolved" "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz" - "version" "3.0.0" +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -"csstype@^3.0.2": - "integrity" "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" - "resolved" "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz" - "version" "3.1.1" +csstype@^3.0.2: + version "3.1.1" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz" + integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== -"damerau-levenshtein@^1.0.8": - "integrity" "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==" - "resolved" "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz" - "version" "1.0.8" +damerau-levenshtein@^1.0.8: + version "1.0.8" + resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz" + integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== -"debug@^2.6.9": - "integrity" "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==" - "resolved" "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" - "version" "2.6.9" +debug@^2.6.9: + version "2.6.9" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: - "ms" "2.0.0" + ms "2.0.0" -"debug@^3.2.7": - "integrity" "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==" - "resolved" "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" - "version" "3.2.7" +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: - "ms" "^2.1.1" + ms "^2.1.1" -"debug@^4.1.0", "debug@^4.3.3", "debug@4": - "version" "4.3.4" +debug@^4.1.0, debug@^4.3.3, debug@4: + version "4.3.4" dependencies: - "ms" "2.1.2" + ms "2.1.2" -"debug@^4.1.1", "debug@^4.3.2", "debug@^4.3.4": - "integrity" "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==" - "resolved" "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" - "version" "4.3.4" +debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: - "ms" "2.1.2" + ms "2.1.2" -"deep-is@^0.1.3": - "integrity" "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" - "resolved" "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" - "version" "0.1.4" +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== -"defaults@^1.0.3": - "version" "1.0.3" +defaults@^1.0.3: + version "1.0.3" dependencies: - "clone" "^1.0.2" + clone "^1.0.2" -"define-properties@^1.1.3", "define-properties@^1.1.4": - "integrity" "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==" - "resolved" "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz" - "version" "1.1.4" +define-properties@^1.1.3, define-properties@^1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz" + integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== dependencies: - "has-property-descriptors" "^1.0.0" - "object-keys" "^1.1.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" -"defined@^1.0.0": - "integrity" "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==" - "resolved" "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz" - "version" "1.0.1" +defined@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz" + integrity sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q== -"delegates@^1.0.0": - "version" "1.0.0" +delegates@^1.0.0: + version "1.0.0" -"depd@^1.1.2": - "version" "1.1.2" +depd@^1.1.2: + version "1.1.2" -"destr@^1.2.1": - "integrity" "sha512-lrbCJwD9saUQrqUfXvl6qoM+QN3W7tLV5pAOs+OqOmopCCz/JkE05MHedJR1xfk4IAnZuJXPVuN5+7jNA2ZCiA==" - "resolved" "https://registry.npmjs.org/destr/-/destr-1.2.2.tgz" - "version" "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== -"detective@^5.2.1": - "integrity" "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==" - "resolved" "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz" - "version" "5.2.1" +detective@^5.2.1: + version "5.2.1" + resolved "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz" + integrity sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw== dependencies: - "acorn-node" "^1.8.2" - "defined" "^1.0.0" - "minimist" "^1.2.6" + acorn-node "^1.8.2" + defined "^1.0.0" + minimist "^1.2.6" -"didyoumean@^1.2.2": - "integrity" "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" - "resolved" "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz" - "version" "1.2.2" +didyoumean@^1.2.2: + version "1.2.2" + resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz" + integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== -"diff@^5.1.0": - "version" "5.1.0" +diff@^5.1.0: + version "5.1.0" -"dir-glob@^3.0.1": - "integrity" "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==" - "resolved" "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" - "version" "3.0.1" +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== dependencies: - "path-type" "^4.0.0" + path-type "^4.0.0" -"dlv@^1.1.3": - "integrity" "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" - "resolved" "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz" - "version" "1.1.3" +dlv@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz" + integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== -"doctrine@^2.1.0": - "integrity" "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==" - "resolved" "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz" - "version" "2.1.0" +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== dependencies: - "esutils" "^2.0.2" + esutils "^2.0.2" -"doctrine@^3.0.0": - "integrity" "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==" - "resolved" "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" - "version" "3.0.0" +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== dependencies: - "esutils" "^2.0.2" + esutils "^2.0.2" -"dom-helpers@^5.0.1": - "integrity" "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==" - "resolved" "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz" - "version" "5.2.1" +dom-helpers@^5.0.1: + version "5.2.1" + resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== dependencies: "@babel/runtime" "^7.8.7" - "csstype" "^3.0.2" - -"electron-to-chromium@^1.4.251": - "integrity" "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==" - "resolved" "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz" - "version" "1.4.284" - -"emoji-regex@^8.0.0": - "version" "8.0.0" - -"emoji-regex@^9.2.2": - "integrity" "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" - "resolved" "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" - "version" "9.2.2" - -"encoding@^0.1.13": - "version" "0.1.13" - dependencies: - "iconv-lite" "^0.6.2" - -"env-paths@^2.2.0": - "version" "2.2.1" - -"err-code@^2.0.2": - "version" "2.0.3" - -"es-abstract@^1.19.0", "es-abstract@^1.20.4": - "integrity" "sha512-7h8MM2EQhsCA7pU/Nv78qOXFpD8Rhqd12gYiSJVkrH9+e8VuA8JlPJK/hQjjlLv6pJvx/z1iRFKzYb0XT/RuAQ==" - "resolved" "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.5.tgz" - "version" "1.20.5" - dependencies: - "call-bind" "^1.0.2" - "es-to-primitive" "^1.2.1" - "function-bind" "^1.1.1" - "function.prototype.name" "^1.1.5" - "get-intrinsic" "^1.1.3" - "get-symbol-description" "^1.0.0" - "gopd" "^1.0.1" - "has" "^1.0.3" - "has-property-descriptors" "^1.0.0" - "has-symbols" "^1.0.3" - "internal-slot" "^1.0.3" - "is-callable" "^1.2.7" - "is-negative-zero" "^2.0.2" - "is-regex" "^1.1.4" - "is-shared-array-buffer" "^1.0.2" - "is-string" "^1.0.7" - "is-weakref" "^1.0.2" - "object-inspect" "^1.12.2" - "object-keys" "^1.1.1" - "object.assign" "^4.1.4" - "regexp.prototype.flags" "^1.4.3" - "safe-regex-test" "^1.0.0" - "string.prototype.trimend" "^1.0.6" - "string.prototype.trimstart" "^1.0.6" - "unbox-primitive" "^1.0.2" - -"es-shim-unscopables@^1.0.0": - "integrity" "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==" - "resolved" "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz" - "version" "1.0.0" - dependencies: - "has" "^1.0.3" - -"es-to-primitive@^1.2.1": - "integrity" "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==" - "resolved" "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz" - "version" "1.2.1" - dependencies: - "is-callable" "^1.1.4" - "is-date-object" "^1.0.1" - "is-symbol" "^1.0.2" - -"esbuild@^0.16.3": - "integrity" "sha512-te0zG5CDzAxhnBKeddXUtK8xDnYL6jv100ekldhtUk0ALXPXcDAtuH0fAR7rbKwUdz3bOey6HVq2N+aWCKZ1cw==" - "resolved" "https://registry.npmjs.org/esbuild/-/esbuild-0.16.5.tgz" - "version" "0.16.5" + csstype "^3.0.2" + +electron-to-chromium@^1.4.251: + version "1.4.284" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz" + integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA== + +emoji-regex@^8.0.0: + version "8.0.0" + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +encoding@^0.1.13: + version "0.1.13" + dependencies: + iconv-lite "^0.6.2" + +env-paths@^2.2.0: + version "2.2.1" + +err-code@^2.0.2: + version "2.0.3" + +es-abstract@^1.19.0, es-abstract@^1.20.4: + version "1.20.5" + resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.5.tgz" + integrity sha512-7h8MM2EQhsCA7pU/Nv78qOXFpD8Rhqd12gYiSJVkrH9+e8VuA8JlPJK/hQjjlLv6pJvx/z1iRFKzYb0XT/RuAQ== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.1.3" + get-symbol-description "^1.0.0" + gopd "^1.0.1" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-symbols "^1.0.3" + internal-slot "^1.0.3" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-weakref "^1.0.2" + object-inspect "^1.12.2" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.4.3" + safe-regex-test "^1.0.0" + string.prototype.trimend "^1.0.6" + string.prototype.trimstart "^1.0.6" + unbox-primitive "^1.0.2" + +es-shim-unscopables@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz" + integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== + dependencies: + has "^1.0.3" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +esbuild@^0.16.3: + version "0.16.5" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.16.5.tgz" + integrity sha512-te0zG5CDzAxhnBKeddXUtK8xDnYL6jv100ekldhtUk0ALXPXcDAtuH0fAR7rbKwUdz3bOey6HVq2N+aWCKZ1cw== optionalDependencies: "@esbuild/android-arm" "0.16.5" "@esbuild/android-arm64" "0.16.5" @@ -1166,1346 +1166,1346 @@ "@esbuild/win32-ia32" "0.16.5" "@esbuild/win32-x64" "0.16.5" -"escalade@^3.1.1": - "integrity" "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" - "resolved" "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" - "version" "3.1.1" - -"escape-string-regexp@^4.0.0": - "integrity" "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" - "resolved" "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" - "version" "4.0.0" - -"eslint-config-airbnb-base@^15.0.0": - "integrity" "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==" - "resolved" "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz" - "version" "15.0.0" - dependencies: - "confusing-browser-globals" "^1.0.10" - "object.assign" "^4.1.2" - "object.entries" "^1.1.5" - "semver" "^6.3.0" - -"eslint-config-airbnb@19.0.4": - "integrity" "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==" - "resolved" "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz" - "version" "19.0.4" - dependencies: - "eslint-config-airbnb-base" "^15.0.0" - "object.assign" "^4.1.2" - "object.entries" "^1.1.5" - -"eslint-config-prettier@^8.6.0": - "integrity" "sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==" - "resolved" "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.6.0.tgz" - "version" "8.6.0" - -"eslint-import-resolver-node@^0.3.6": - "integrity" "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==" - "resolved" "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz" - "version" "0.3.6" - dependencies: - "debug" "^3.2.7" - "resolve" "^1.20.0" - -"eslint-import-resolver-typescript@^2.5.0": - "integrity" "sha512-00UbgGwV8bSgUv34igBDbTOtKhqoRMy9bFjNehT40bXg6585PNIct8HhXZ0SybqB9rWtXj9crcku8ndDn/gIqQ==" - "resolved" "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.7.1.tgz" - "version" "2.7.1" - dependencies: - "debug" "^4.3.4" - "glob" "^7.2.0" - "is-glob" "^4.0.3" - "resolve" "^1.22.0" - "tsconfig-paths" "^3.14.1" - -"eslint-module-utils@^2.7.3": - "integrity" "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==" - "resolved" "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz" - "version" "2.7.4" - dependencies: - "debug" "^3.2.7" - -"eslint-plugin-import@*", "eslint-plugin-import@^2.25.2", "eslint-plugin-import@^2.25.3", "eslint-plugin-import@^2.25.4": - "integrity" "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==" - "resolved" "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz" - "version" "2.26.0" - dependencies: - "array-includes" "^3.1.4" - "array.prototype.flat" "^1.2.5" - "debug" "^2.6.9" - "doctrine" "^2.1.0" - "eslint-import-resolver-node" "^0.3.6" - "eslint-module-utils" "^2.7.3" - "has" "^1.0.3" - "is-core-module" "^2.8.1" - "is-glob" "^4.0.3" - "minimatch" "^3.1.2" - "object.values" "^1.1.5" - "resolve" "^1.22.0" - "tsconfig-paths" "^3.14.1" - -"eslint-plugin-jsx-a11y@^6.5.1": - "integrity" "sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q==" - "resolved" "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz" - "version" "6.6.1" +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-config-airbnb-base@^15.0.0: + version "15.0.0" + resolved "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz" + integrity sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig== + dependencies: + confusing-browser-globals "^1.0.10" + object.assign "^4.1.2" + object.entries "^1.1.5" + semver "^6.3.0" + +eslint-config-airbnb@19.0.4: + version "19.0.4" + resolved "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz" + integrity sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew== + dependencies: + eslint-config-airbnb-base "^15.0.0" + object.assign "^4.1.2" + object.entries "^1.1.5" + +eslint-config-prettier@^8.6.0: + version "8.6.0" + resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.6.0.tgz" + integrity sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA== + +eslint-import-resolver-node@^0.3.6: + version "0.3.6" + resolved "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz" + integrity sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw== + dependencies: + debug "^3.2.7" + resolve "^1.20.0" + +eslint-import-resolver-typescript@^2.5.0: + version "2.7.1" + resolved "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.7.1.tgz" + integrity sha512-00UbgGwV8bSgUv34igBDbTOtKhqoRMy9bFjNehT40bXg6585PNIct8HhXZ0SybqB9rWtXj9crcku8ndDn/gIqQ== + dependencies: + debug "^4.3.4" + glob "^7.2.0" + is-glob "^4.0.3" + resolve "^1.22.0" + tsconfig-paths "^3.14.1" + +eslint-module-utils@^2.7.3: + version "2.7.4" + resolved "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz" + integrity sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA== + dependencies: + debug "^3.2.7" + +eslint-plugin-import@*, eslint-plugin-import@^2.25.2, eslint-plugin-import@^2.25.3, eslint-plugin-import@^2.25.4: + version "2.26.0" + resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz" + integrity sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA== + dependencies: + array-includes "^3.1.4" + array.prototype.flat "^1.2.5" + debug "^2.6.9" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.6" + eslint-module-utils "^2.7.3" + has "^1.0.3" + is-core-module "^2.8.1" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.values "^1.1.5" + resolve "^1.22.0" + tsconfig-paths "^3.14.1" + +eslint-plugin-jsx-a11y@^6.5.1: + version "6.6.1" + resolved "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz" + integrity sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q== dependencies: "@babel/runtime" "^7.18.9" - "aria-query" "^4.2.2" - "array-includes" "^3.1.5" - "ast-types-flow" "^0.0.7" - "axe-core" "^4.4.3" - "axobject-query" "^2.2.0" - "damerau-levenshtein" "^1.0.8" - "emoji-regex" "^9.2.2" - "has" "^1.0.3" - "jsx-ast-utils" "^3.3.2" - "language-tags" "^1.0.5" - "minimatch" "^3.1.2" - "semver" "^6.3.0" - -"eslint-plugin-prettier@^4.2.1": - "integrity" "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==" - "resolved" "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz" - "version" "4.2.1" - dependencies: - "prettier-linter-helpers" "^1.0.0" - -"eslint-plugin-react-hooks@^4.3.0", "eslint-plugin-react-hooks@4.3.0": - "integrity" "sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==" - "resolved" "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz" - "version" "4.3.0" - -"eslint-plugin-react@^7.28.0", "eslint-plugin-react@7.29.4": - "integrity" "sha512-CVCXajliVh509PcZYRFyu/BoUEz452+jtQJq2b3Bae4v3xBUWPLCmtmBM+ZinG4MzwmxJgJ2M5rMqhqLVn7MtQ==" - "resolved" "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.29.4.tgz" - "version" "7.29.4" - dependencies: - "array-includes" "^3.1.4" - "array.prototype.flatmap" "^1.2.5" - "doctrine" "^2.1.0" - "estraverse" "^5.3.0" - "jsx-ast-utils" "^2.4.1 || ^3.0.0" - "minimatch" "^3.1.2" - "object.entries" "^1.1.5" - "object.fromentries" "^2.0.5" - "object.hasown" "^1.1.0" - "object.values" "^1.1.5" - "prop-types" "^15.8.1" - "resolve" "^2.0.0-next.3" - "semver" "^6.3.0" - "string.prototype.matchall" "^4.0.6" - -"eslint-scope@^5.1.1": - "integrity" "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==" - "resolved" "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" - "version" "5.1.1" - dependencies: - "esrecurse" "^4.3.0" - "estraverse" "^4.1.1" - -"eslint-scope@^7.1.1": - "integrity" "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==" - "resolved" "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz" - "version" "7.1.1" - dependencies: - "esrecurse" "^4.3.0" - "estraverse" "^5.2.0" - -"eslint-utils@^3.0.0": - "integrity" "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==" - "resolved" "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz" - "version" "3.0.0" - dependencies: - "eslint-visitor-keys" "^2.0.0" - -"eslint-visitor-keys@^2.0.0": - "integrity" "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==" - "resolved" "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz" - "version" "2.1.0" - -"eslint-visitor-keys@^3.3.0": - "integrity" "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==" - "resolved" "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" - "version" "3.3.0" - -"eslint@*", "eslint@^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8", "eslint@^3 || ^4 || ^5 || ^6 || ^7 || ^8", "eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "eslint@^6.0.0 || ^7.0.0 || ^8.0.0", "eslint@^7.32.0 || ^8.2.0", "eslint@^8.10.0", "eslint@>=5", "eslint@>=7.0.0", "eslint@>=7.28.0": - "integrity" "sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg==" - "resolved" "https://registry.npmjs.org/eslint/-/eslint-8.29.0.tgz" - "version" "8.29.0" + aria-query "^4.2.2" + array-includes "^3.1.5" + ast-types-flow "^0.0.7" + axe-core "^4.4.3" + axobject-query "^2.2.0" + damerau-levenshtein "^1.0.8" + emoji-regex "^9.2.2" + has "^1.0.3" + jsx-ast-utils "^3.3.2" + language-tags "^1.0.5" + minimatch "^3.1.2" + semver "^6.3.0" + +eslint-plugin-prettier@^4.2.1: + version "4.2.1" + resolved "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz" + integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ== + dependencies: + prettier-linter-helpers "^1.0.0" + +eslint-plugin-react-hooks@^4.3.0, eslint-plugin-react-hooks@4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz" + integrity sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA== + +eslint-plugin-react@^7.28.0, eslint-plugin-react@7.29.4: + version "7.29.4" + resolved "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.29.4.tgz" + integrity sha512-CVCXajliVh509PcZYRFyu/BoUEz452+jtQJq2b3Bae4v3xBUWPLCmtmBM+ZinG4MzwmxJgJ2M5rMqhqLVn7MtQ== + dependencies: + array-includes "^3.1.4" + array.prototype.flatmap "^1.2.5" + doctrine "^2.1.0" + estraverse "^5.3.0" + jsx-ast-utils "^2.4.1 || ^3.0.0" + minimatch "^3.1.2" + object.entries "^1.1.5" + object.fromentries "^2.0.5" + object.hasown "^1.1.0" + object.values "^1.1.5" + prop-types "^15.8.1" + resolve "^2.0.0-next.3" + semver "^6.3.0" + string.prototype.matchall "^4.0.6" + +eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-scope@^7.1.1: + version "7.1.1" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz" + integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz" + integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== + dependencies: + eslint-visitor-keys "^2.0.0" + +eslint-visitor-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint-visitor-keys@^3.3.0: + version "3.3.0" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" + integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== + +eslint@*, "eslint@^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8", "eslint@^3 || ^4 || ^5 || ^6 || ^7 || ^8", "eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "eslint@^6.0.0 || ^7.0.0 || ^8.0.0", "eslint@^7.32.0 || ^8.2.0", eslint@^8.10.0, eslint@>=5, eslint@>=7.0.0, eslint@>=7.28.0: + version "8.29.0" + resolved "https://registry.npmjs.org/eslint/-/eslint-8.29.0.tgz" + integrity sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg== dependencies: "@eslint/eslintrc" "^1.3.3" "@humanwhocodes/config-array" "^0.11.6" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" - "ajv" "^6.10.0" - "chalk" "^4.0.0" - "cross-spawn" "^7.0.2" - "debug" "^4.3.2" - "doctrine" "^3.0.0" - "escape-string-regexp" "^4.0.0" - "eslint-scope" "^7.1.1" - "eslint-utils" "^3.0.0" - "eslint-visitor-keys" "^3.3.0" - "espree" "^9.4.0" - "esquery" "^1.4.0" - "esutils" "^2.0.2" - "fast-deep-equal" "^3.1.3" - "file-entry-cache" "^6.0.1" - "find-up" "^5.0.0" - "glob-parent" "^6.0.2" - "globals" "^13.15.0" - "grapheme-splitter" "^1.0.4" - "ignore" "^5.2.0" - "import-fresh" "^3.0.0" - "imurmurhash" "^0.1.4" - "is-glob" "^4.0.0" - "is-path-inside" "^3.0.3" - "js-sdsl" "^4.1.4" - "js-yaml" "^4.1.0" - "json-stable-stringify-without-jsonify" "^1.0.1" - "levn" "^0.4.1" - "lodash.merge" "^4.6.2" - "minimatch" "^3.1.2" - "natural-compare" "^1.4.0" - "optionator" "^0.9.1" - "regexpp" "^3.2.0" - "strip-ansi" "^6.0.1" - "strip-json-comments" "^3.1.0" - "text-table" "^0.2.0" - -"espree@^9.4.0": - "integrity" "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==" - "resolved" "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz" - "version" "9.4.1" - dependencies: - "acorn" "^8.8.0" - "acorn-jsx" "^5.3.2" - "eslint-visitor-keys" "^3.3.0" - -"esquery@^1.4.0": - "integrity" "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==" - "resolved" "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz" - "version" "1.4.0" - dependencies: - "estraverse" "^5.1.0" - -"esrecurse@^4.3.0": - "integrity" "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==" - "resolved" "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" - "version" "4.3.0" - dependencies: - "estraverse" "^5.2.0" - -"estraverse@^4.1.1": - "integrity" "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" - "resolved" "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" - "version" "4.3.0" - -"estraverse@^5.1.0", "estraverse@^5.2.0", "estraverse@^5.3.0": - "integrity" "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" - "resolved" "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" - "version" "5.3.0" - -"esutils@^2.0.2": - "integrity" "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - "resolved" "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" - "version" "2.0.3" - -"event-target-shim@^5.0.0": - "version" "5.0.1" - -"eventemitter3@^3.0.0": - "integrity" "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" - "resolved" "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz" - "version" "3.1.2" - -"events@^3.3.0": - "version" "3.3.0" - -"fast-deep-equal@^3.1.1", "fast-deep-equal@^3.1.3": - "integrity" "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - "resolved" "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" - "version" "3.1.3" - -"fast-diff@^1.1.2": - "integrity" "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==" - "resolved" "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz" - "version" "1.2.0" - -"fast-glob@^3.2.12", "fast-glob@^3.2.9": - "integrity" "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==" - "resolved" "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz" - "version" "3.2.12" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.1.1" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.3.0" + espree "^9.4.0" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.15.0" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-sdsl "^4.1.4" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.1" + regexpp "^3.2.0" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + +espree@^9.4.0: + version "9.4.1" + resolved "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz" + integrity sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg== + dependencies: + acorn "^8.8.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.3.0" + +esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: + version "5.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +event-target-shim@^5.0.0: + version "5.0.1" + +eventemitter3@^3.0.0: + version "3.1.2" + resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz" + integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== + +events@^3.3.0: + version "3.3.0" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-diff@^1.1.2: + version "1.2.0" + resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz" + integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== + +fast-glob@^3.2.12, fast-glob@^3.2.9: + version "3.2.12" + resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz" + 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-json-stable-stringify@^2.0.0": - "integrity" "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - "resolved" "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" - "version" "2.1.0" - -"fast-levenshtein@^2.0.6": - "integrity" "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" - "resolved" "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" - "version" "2.0.6" - -"fastest-levenshtein@^1.0.16": - "version" "1.0.16" - -"fastq@^1.6.0": - "integrity" "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==" - "resolved" "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz" - "version" "1.14.0" - dependencies: - "reusify" "^1.0.4" - -"file-entry-cache@^6.0.1": - "integrity" "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==" - "resolved" "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" - "version" "6.0.1" - dependencies: - "flat-cache" "^3.0.4" - -"fill-range@^7.0.1": - "integrity" "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==" - "resolved" "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" - "version" "7.0.1" - dependencies: - "to-regex-range" "^5.0.1" - -"find-up@^5.0.0": - "integrity" "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==" - "resolved" "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" - "version" "5.0.0" - dependencies: - "locate-path" "^6.0.0" - "path-exists" "^4.0.0" - -"flat-cache@^3.0.4": - "integrity" "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==" - "resolved" "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz" - "version" "3.0.4" - dependencies: - "flatted" "^3.1.0" - "rimraf" "^3.0.2" - -"flatted@^3.1.0": - "integrity" "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" - "resolved" "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz" - "version" "3.2.7" - -"fraction.js@^4.2.0": - "integrity" "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==" - "resolved" "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz" - "version" "4.2.0" - -"fs-minipass@^2.0.0", "fs-minipass@^2.1.0": - "version" "2.1.0" - dependencies: - "minipass" "^3.0.0" - -"fs.realpath@^1.0.0": - "integrity" "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - "resolved" "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" - "version" "1.0.0" - -"fscreen@^1.2.0": - "integrity" "sha512-hlq4+BU0hlPmwsFjwGGzZ+OZ9N/wq9Ljg/sq3pX+2CD7hrJsX9tJgWWK/wiNTFM212CLHWhicOoqwXyZGGetJg==" - "resolved" "https://registry.npmjs.org/fscreen/-/fscreen-1.2.0.tgz" - "version" "1.2.0" - -"function-bind@^1.1.1": - "integrity" "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - "resolved" "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" - "version" "1.1.1" - -"function.prototype.name@^1.1.5": - "integrity" "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==" - "resolved" "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz" - "version" "1.1.5" - dependencies: - "call-bind" "^1.0.2" - "define-properties" "^1.1.3" - "es-abstract" "^1.19.0" - "functions-have-names" "^1.2.2" - -"functions-have-names@^1.2.2": - "integrity" "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" - "resolved" "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz" - "version" "1.2.3" - -"fuse.js@^6.4.6": - "integrity" "sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==" - "resolved" "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz" - "version" "6.6.2" - -"gauge@^4.0.3": - "version" "4.0.4" - dependencies: - "aproba" "^1.0.3 || ^2.0.0" - "color-support" "^1.1.3" - "console-control-strings" "^1.1.0" - "has-unicode" "^2.0.1" - "signal-exit" "^3.0.7" - "string-width" "^4.2.3" - "strip-ansi" "^6.0.1" - "wide-align" "^1.1.5" - -"gauge@^5.0.0": - "version" "5.0.0" - dependencies: - "aproba" "^1.0.3 || ^2.0.0" - "color-support" "^1.1.3" - "console-control-strings" "^1.1.0" - "has-unicode" "^2.0.1" - "signal-exit" "^3.0.7" - "string-width" "^4.2.3" - "strip-ansi" "^6.0.1" - "wide-align" "^1.1.5" - -"get-intrinsic@^1.0.2", "get-intrinsic@^1.1.1", "get-intrinsic@^1.1.3": - "integrity" "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==" - "resolved" "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz" - "version" "1.1.3" - dependencies: - "function-bind" "^1.1.1" - "has" "^1.0.3" - "has-symbols" "^1.0.3" - -"get-symbol-description@^1.0.0": - "integrity" "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==" - "resolved" "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz" - "version" "1.0.0" - dependencies: - "call-bind" "^1.0.2" - "get-intrinsic" "^1.1.1" - -"glob-parent@^5.1.2": - "integrity" "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==" - "resolved" "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" - "version" "5.1.2" - dependencies: - "is-glob" "^4.0.1" - -"glob-parent@^6.0.2": - "integrity" "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==" - "resolved" "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" - "version" "6.0.2" - dependencies: - "is-glob" "^4.0.3" - -"glob-parent@~5.1.2": - "integrity" "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==" - "resolved" "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" - "version" "5.1.2" - dependencies: - "is-glob" "^4.0.1" - -"glob@^7.1.3", "glob@^7.2.0": - "integrity" "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==" - "resolved" "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" - "version" "7.2.3" - dependencies: - "fs.realpath" "^1.0.0" - "inflight" "^1.0.4" - "inherits" "2" - "minimatch" "^3.1.1" - "once" "^1.3.0" - "path-is-absolute" "^1.0.0" - -"glob@^7.1.4": - "version" "7.2.3" - dependencies: - "fs.realpath" "^1.0.0" - "inflight" "^1.0.4" - "inherits" "2" - "minimatch" "^3.1.1" - "once" "^1.3.0" - "path-is-absolute" "^1.0.0" - -"glob@^8.0.1": - "version" "8.0.3" - dependencies: - "fs.realpath" "^1.0.0" - "inflight" "^1.0.4" - "inherits" "2" - "minimatch" "^5.0.1" - "once" "^1.3.0" - -"globals@^13.15.0": - "integrity" "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==" - "resolved" "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz" - "version" "13.19.0" - dependencies: - "type-fest" "^0.20.2" - -"globby@^11.1.0": - "integrity" "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==" - "resolved" "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" - "version" "11.1.0" - dependencies: - "array-union" "^2.1.0" - "dir-glob" "^3.0.1" - "fast-glob" "^3.2.9" - "ignore" "^5.2.0" - "merge2" "^1.4.1" - "slash" "^3.0.0" - -"gopd@^1.0.1": - "integrity" "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==" - "resolved" "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz" - "version" "1.0.1" - dependencies: - "get-intrinsic" "^1.1.3" - -"graceful-fs@^4.2.10", "graceful-fs@^4.2.6": - "version" "4.2.10" - -"grapheme-splitter@^1.0.4": - "integrity" "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" - "resolved" "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz" - "version" "1.0.4" - -"has-bigints@^1.0.1", "has-bigints@^1.0.2": - "integrity" "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" - "resolved" "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz" - "version" "1.0.2" - -"has-flag@^4.0.0": - "integrity" "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - "resolved" "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" - "version" "4.0.0" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastest-levenshtein@^1.0.16: + version "1.0.16" + +fastq@^1.6.0: + version "1.14.0" + resolved "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz" + integrity sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg== + dependencies: + reusify "^1.0.4" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.1.0: + version "3.2.7" + resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz" + integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== + +fraction.js@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz" + integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== + +fs-minipass@^2.0.0, fs-minipass@^2.1.0: + version "2.1.0" + dependencies: + minipass "^3.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fscreen@^1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/fscreen/-/fscreen-1.2.0.tgz" + integrity sha512-hlq4+BU0hlPmwsFjwGGzZ+OZ9N/wq9Ljg/sq3pX+2CD7hrJsX9tJgWWK/wiNTFM212CLHWhicOoqwXyZGGetJg== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +function.prototype.name@^1.1.5: + version "1.1.5" + resolved "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz" + integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + functions-have-names "^1.2.2" + +functions-have-names@^1.2.2: + version "1.2.3" + resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +fuse.js@^6.4.6: + version "6.6.2" + resolved "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz" + integrity sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA== + +gauge@^4.0.3: + version "4.0.4" + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.3" + console-control-strings "^1.1.0" + has-unicode "^2.0.1" + signal-exit "^3.0.7" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.5" + +gauge@^5.0.0: + version "5.0.0" + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.3" + console-control-strings "^1.1.0" + has-unicode "^2.0.1" + signal-exit "^3.0.7" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.5" + +get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz" + integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@^7.1.3, glob@^7.2.0: + version "7.2.3" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.1.4: + version "7.2.3" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^8.0.1: + version "8.0.3" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + +globals@^13.15.0: + version "13.19.0" + resolved "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz" + integrity sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ== + dependencies: + type-fest "^0.20.2" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graceful-fs@^4.2.10, graceful-fs@^4.2.6: + version "4.2.10" + +grapheme-splitter@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz" + integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== + +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -"has-property-descriptors@^1.0.0": - "integrity" "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==" - "resolved" "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz" - "version" "1.0.0" - dependencies: - "get-intrinsic" "^1.1.1" - -"has-symbols@^1.0.2", "has-symbols@^1.0.3": - "integrity" "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - "resolved" "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" - "version" "1.0.3" - -"has-tostringtag@^1.0.0": - "integrity" "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==" - "resolved" "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz" - "version" "1.0.0" - dependencies: - "has-symbols" "^1.0.2" - -"has-unicode@^2.0.1": - "version" "2.0.1" - -"has@^1.0.3": - "integrity" "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==" - "resolved" "https://registry.npmjs.org/has/-/has-1.0.3.tgz" - "version" "1.0.3" - dependencies: - "function-bind" "^1.1.1" - -"history@^4.9.0": - "integrity" "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==" - "resolved" "https://registry.npmjs.org/history/-/history-4.10.1.tgz" - "version" "4.10.1" +has-property-descriptors@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz" + integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + dependencies: + get-intrinsic "^1.1.1" + +has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + +has-unicode@^2.0.1: + version "2.0.1" + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +history@^4.9.0: + version "4.10.1" + resolved "https://registry.npmjs.org/history/-/history-4.10.1.tgz" + integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== dependencies: "@babel/runtime" "^7.1.2" - "loose-envify" "^1.2.0" - "resolve-pathname" "^3.0.0" - "tiny-invariant" "^1.0.2" - "tiny-warning" "^1.0.0" - "value-equal" "^1.0.1" + loose-envify "^1.2.0" + resolve-pathname "^3.0.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + value-equal "^1.0.1" -"hls.js@^1.0.7": - "integrity" "sha512-SPjm8ix0xe6cYzwDvdVGh2QvQPDkCYrGWpZu6bRaKNNVyEGWM9uF0pooh/Lqj/g8QBQgPFEx1vHzW8SyMY9rqg==" - "resolved" "https://registry.npmjs.org/hls.js/-/hls.js-1.2.9.tgz" - "version" "1.2.9" +hls.js@^1.0.7: + version "1.2.9" + resolved "https://registry.npmjs.org/hls.js/-/hls.js-1.2.9.tgz" + integrity sha512-SPjm8ix0xe6cYzwDvdVGh2QvQPDkCYrGWpZu6bRaKNNVyEGWM9uF0pooh/Lqj/g8QBQgPFEx1vHzW8SyMY9rqg== -"hoist-non-react-statics@^3.1.0": - "integrity" "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==" - "resolved" "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" - "version" "3.3.2" +hoist-non-react-statics@^3.1.0: + version "3.3.2" + resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== dependencies: - "react-is" "^16.7.0" + react-is "^16.7.0" -"hosted-git-info@^6.0.0", "hosted-git-info@^6.1.1": - "version" "6.1.1" +hosted-git-info@^6.0.0, hosted-git-info@^6.1.1: + version "6.1.1" dependencies: - "lru-cache" "^7.5.1" + lru-cache "^7.5.1" -"html-parse-stringify@^3.0.1": - "integrity" "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==" - "resolved" "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz" - "version" "3.0.1" +html-parse-stringify@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz" + integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== dependencies: - "void-elements" "3.1.0" + void-elements "3.1.0" -"http-cache-semantics@^4.1.0": - "version" "4.1.0" +http-cache-semantics@^4.1.0: + version "4.1.0" -"http-proxy-agent@^5.0.0": - "version" "5.0.0" +http-proxy-agent@^5.0.0: + version "5.0.0" dependencies: "@tootallnate/once" "2" - "agent-base" "6" - "debug" "4" + agent-base "6" + debug "4" -"https-proxy-agent@^5.0.0": - "version" "5.0.1" +https-proxy-agent@^5.0.0: + version "5.0.1" dependencies: - "agent-base" "6" - "debug" "4" + agent-base "6" + debug "4" -"humanize-ms@^1.2.1": - "version" "1.2.1" +humanize-ms@^1.2.1: + version "1.2.1" dependencies: - "ms" "^2.0.0" + ms "^2.0.0" -"i@^0.3.7": - "integrity" "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==" - "resolved" "https://registry.npmjs.org/i/-/i-0.3.7.tgz" - "version" "0.3.7" +i@^0.3.7: + version "0.3.7" + resolved "https://registry.npmjs.org/i/-/i-0.3.7.tgz" + integrity sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q== -"i18next-browser-languagedetector@^7.0.1": - "integrity" "sha512-Pa5kFwaczXJAeHE56CHG2aWzFBMJNUNghf0Pm4SwSrEMps/PTKqW90EYWlIvhuYStf3Sn1K0vw+gH3+TLdkH1g==" - "resolved" "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.0.1.tgz" - "version" "7.0.1" +i18next-browser-languagedetector@^7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.0.1.tgz" + integrity sha512-Pa5kFwaczXJAeHE56CHG2aWzFBMJNUNghf0Pm4SwSrEMps/PTKqW90EYWlIvhuYStf3Sn1K0vw+gH3+TLdkH1g== dependencies: "@babel/runtime" "^7.19.4" -"i18next@^22.4.5", "i18next@>= 19.0.0": - "integrity" "sha512-Kc+Ow0guRetUq+kv02tj0Yof9zveROPBAmJ8UxxNODLVBRSwsM4iD0Gw3BEieOmkWemF6clU3K1fbnCuTqiN2Q==" - "resolved" "https://registry.npmjs.org/i18next/-/i18next-22.4.5.tgz" - "version" "22.4.5" +i18next@^22.4.5, "i18next@>= 19.0.0": + version "22.4.5" + resolved "https://registry.npmjs.org/i18next/-/i18next-22.4.5.tgz" + integrity sha512-Kc+Ow0guRetUq+kv02tj0Yof9zveROPBAmJ8UxxNODLVBRSwsM4iD0Gw3BEieOmkWemF6clU3K1fbnCuTqiN2Q== dependencies: "@babel/runtime" "^7.20.6" -"iconv-lite@^0.6.2": - "integrity" "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==" - "resolved" "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" - "version" "0.6.3" +iconv-lite@^0.6.2: + version "0.6.3" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== dependencies: - "safer-buffer" ">= 2.1.2 < 3.0.0" + safer-buffer ">= 2.1.2 < 3.0.0" -"ieee754@^1.2.1": - "version" "1.2.1" +ieee754@^1.2.1: + version "1.2.1" -"ignore-walk@^6.0.0": - "version" "6.0.0" +ignore-walk@^6.0.0: + version "6.0.0" dependencies: - "minimatch" "^5.0.1" + minimatch "^5.0.1" -"ignore@^5.2.0": - "integrity" "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==" - "resolved" "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz" - "version" "5.2.1" +ignore@^5.2.0: + version "5.2.1" + resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz" + integrity sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA== -"import-fresh@^3.0.0", "import-fresh@^3.2.1": - "integrity" "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==" - "resolved" "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" - "version" "3.3.0" +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== dependencies: - "parent-module" "^1.0.0" - "resolve-from" "^4.0.0" + parent-module "^1.0.0" + resolve-from "^4.0.0" -"imurmurhash@^0.1.4": - "integrity" "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" - "resolved" "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" - "version" "0.1.4" +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== -"indent-string@^4.0.0": - "version" "4.0.0" +indent-string@^4.0.0: + version "4.0.0" -"infer-owner@^1.0.4": - "version" "1.0.4" +infer-owner@^1.0.4: + version "1.0.4" -"inflight@^1.0.4": - "integrity" "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==" - "resolved" "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" - "version" "1.0.6" +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== dependencies: - "once" "^1.3.0" - "wrappy" "1" + once "^1.3.0" + wrappy "1" -"inherits@^2.0.3", "inherits@2": - "integrity" "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - "resolved" "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" - "version" "2.0.4" +inherits@^2.0.3, inherits@2: + version "2.0.4" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -"ini@^3.0.0", "ini@^3.0.1": - "version" "3.0.1" +ini@^3.0.0, ini@^3.0.1: + version "3.0.1" -"init-package-json@^4.0.1": - "version" "4.0.1" +init-package-json@^4.0.1: + version "4.0.1" dependencies: - "npm-package-arg" "^10.0.0" - "promzard" "^0.3.0" - "read" "^1.0.7" - "read-package-json" "^6.0.0" - "semver" "^7.3.5" - "validate-npm-package-license" "^3.0.4" - "validate-npm-package-name" "^5.0.0" + npm-package-arg "^10.0.0" + promzard "^0.3.0" + read "^1.0.7" + read-package-json "^6.0.0" + semver "^7.3.5" + validate-npm-package-license "^3.0.4" + validate-npm-package-name "^5.0.0" -"internal-slot@^1.0.3": - "integrity" "sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==" - "resolved" "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.4.tgz" - "version" "1.0.4" +internal-slot@^1.0.3: + version "1.0.4" + resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.4.tgz" + integrity sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ== dependencies: - "get-intrinsic" "^1.1.3" - "has" "^1.0.3" - "side-channel" "^1.0.4" + get-intrinsic "^1.1.3" + has "^1.0.3" + side-channel "^1.0.4" -"ip-regex@^4.1.0": - "version" "4.3.0" +ip-regex@^4.1.0: + version "4.3.0" -"ip@^2.0.0": - "version" "2.0.0" +ip@^2.0.0: + version "2.0.0" -"is-bigint@^1.0.1": - "integrity" "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==" - "resolved" "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz" - "version" "1.0.4" +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== dependencies: - "has-bigints" "^1.0.1" + has-bigints "^1.0.1" -"is-binary-path@~2.1.0": - "integrity" "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==" - "resolved" "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" - "version" "2.1.0" +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== dependencies: - "binary-extensions" "^2.0.0" + binary-extensions "^2.0.0" -"is-boolean-object@^1.1.0": - "integrity" "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==" - "resolved" "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz" - "version" "1.1.2" +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== dependencies: - "call-bind" "^1.0.2" - "has-tostringtag" "^1.0.0" + call-bind "^1.0.2" + has-tostringtag "^1.0.0" -"is-callable@^1.1.4", "is-callable@^1.2.7": - "integrity" "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" - "resolved" "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" - "version" "1.2.7" +is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -"is-cidr@^4.0.2": - "version" "4.0.2" +is-cidr@^4.0.2: + version "4.0.2" dependencies: - "cidr-regex" "^3.1.1" + cidr-regex "^3.1.1" -"is-core-module@^2.8.1", "is-core-module@^2.9.0": - "integrity" "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==" - "resolved" "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz" - "version" "2.11.0" +is-core-module@^2.8.1, is-core-module@^2.9.0: + version "2.11.0" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz" + integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== dependencies: - "has" "^1.0.3" + has "^1.0.3" -"is-date-object@^1.0.1": - "integrity" "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==" - "resolved" "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz" - "version" "1.0.5" +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== dependencies: - "has-tostringtag" "^1.0.0" + has-tostringtag "^1.0.0" -"is-extglob@^2.1.1": - "integrity" "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" - "resolved" "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" - "version" "2.1.1" +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== -"is-fullwidth-code-point@^3.0.0": - "version" "3.0.0" +is-fullwidth-code-point@^3.0.0: + version "3.0.0" -"is-glob@^4.0.0", "is-glob@^4.0.1", "is-glob@^4.0.3", "is-glob@~4.0.1": - "integrity" "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==" - "resolved" "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" - "version" "4.0.3" +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: - "is-extglob" "^2.1.1" + is-extglob "^2.1.1" -"is-lambda@^1.0.1": - "version" "1.0.1" +is-lambda@^1.0.1: + version "1.0.1" -"is-negative-zero@^2.0.2": - "integrity" "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==" - "resolved" "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz" - "version" "2.0.2" +is-negative-zero@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== -"is-number-object@^1.0.4": - "integrity" "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==" - "resolved" "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz" - "version" "1.0.7" +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== dependencies: - "has-tostringtag" "^1.0.0" + has-tostringtag "^1.0.0" -"is-number@^7.0.0": - "integrity" "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - "resolved" "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" - "version" "7.0.0" +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -"is-path-inside@^3.0.3": - "integrity" "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==" - "resolved" "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" - "version" "3.0.3" +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== -"is-regex@^1.1.4": - "integrity" "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==" - "resolved" "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz" - "version" "1.1.4" +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== dependencies: - "call-bind" "^1.0.2" - "has-tostringtag" "^1.0.0" + call-bind "^1.0.2" + has-tostringtag "^1.0.0" -"is-shared-array-buffer@^1.0.2": - "integrity" "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==" - "resolved" "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz" - "version" "1.0.2" +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== dependencies: - "call-bind" "^1.0.2" + call-bind "^1.0.2" -"is-string@^1.0.5", "is-string@^1.0.7": - "integrity" "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==" - "resolved" "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz" - "version" "1.0.7" +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== dependencies: - "has-tostringtag" "^1.0.0" + has-tostringtag "^1.0.0" -"is-symbol@^1.0.2", "is-symbol@^1.0.3": - "integrity" "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==" - "resolved" "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz" - "version" "1.0.4" +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== dependencies: - "has-symbols" "^1.0.2" + has-symbols "^1.0.2" -"is-weakref@^1.0.2": - "integrity" "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==" - "resolved" "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz" - "version" "1.0.2" +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== dependencies: - "call-bind" "^1.0.2" + call-bind "^1.0.2" -"isarray@0.0.1": - "integrity" "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" - "resolved" "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" - "version" "0.0.1" +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== -"isexe@^2.0.0": - "integrity" "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - "resolved" "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" - "version" "2.0.0" +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -"js-sdsl@^4.1.4": - "integrity" "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==" - "resolved" "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz" - "version" "4.2.0" +js-sdsl@^4.1.4: + version "4.2.0" + resolved "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz" + integrity sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ== "js-tokens@^3.0.0 || ^4.0.0": - "integrity" "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - "resolved" "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" - "version" "4.0.0" + version "4.0.0" + resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -"js-yaml@^4.1.0": - "integrity" "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==" - "resolved" "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" - "version" "4.1.0" +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== dependencies: - "argparse" "^2.0.1" + argparse "^2.0.1" -"json-parse-even-better-errors@^3.0.0": - "version" "3.0.0" +json-parse-even-better-errors@^3.0.0: + version "3.0.0" -"json-schema-traverse@^0.4.1": - "integrity" "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - "resolved" "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" - "version" "0.4.1" +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== -"json-stable-stringify-without-jsonify@^1.0.1": - "integrity" "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" - "resolved" "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" - "version" "1.0.1" +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== -"json-stringify-nice@^1.1.4": - "version" "1.1.4" +json-stringify-nice@^1.1.4: + version "1.1.4" -"json5@^1.0.1": - "integrity" "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==" - "resolved" "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz" - "version" "1.0.2" +json5@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: - "minimist" "^1.2.0" + minimist "^1.2.0" -"json5@^2.2.0": - "integrity" "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" - "resolved" "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" - "version" "2.2.3" +json5@^2.2.0: + version "2.2.3" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -"jsonparse@^1.3.1": - "version" "1.3.1" +jsonparse@^1.3.1: + version "1.3.1" -"jsx-ast-utils@^2.4.1 || ^3.0.0", "jsx-ast-utils@^3.3.2": - "integrity" "sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==" - "resolved" "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz" - "version" "3.3.3" +"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.2: + version "3.3.3" + resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz" + integrity sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw== dependencies: - "array-includes" "^3.1.5" - "object.assign" "^4.1.3" + array-includes "^3.1.5" + object.assign "^4.1.3" -"just-diff-apply@^5.2.0": - "version" "5.4.1" +just-diff-apply@^5.2.0: + version "5.4.1" -"just-diff@^5.0.1": - "version" "5.1.1" +just-diff@^5.0.1: + version "5.1.1" -"language-subtag-registry@^0.3.20": - "integrity" "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==" - "resolved" "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz" - "version" "0.3.22" +language-subtag-registry@^0.3.20: + version "0.3.22" + resolved "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz" + integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w== -"language-tags@^1.0.5": - "integrity" "sha512-HNkaCgM8wZgE/BZACeotAAgpL9FUjEnhgF0FVQMIgH//zqTPreLYMb3rWYkYAqPoF75Jwuycp1da7uz66cfFQg==" - "resolved" "https://registry.npmjs.org/language-tags/-/language-tags-1.0.6.tgz" - "version" "1.0.6" +language-tags@^1.0.5: + version "1.0.6" + resolved "https://registry.npmjs.org/language-tags/-/language-tags-1.0.6.tgz" + integrity sha512-HNkaCgM8wZgE/BZACeotAAgpL9FUjEnhgF0FVQMIgH//zqTPreLYMb3rWYkYAqPoF75Jwuycp1da7uz66cfFQg== dependencies: - "language-subtag-registry" "^0.3.20" + language-subtag-registry "^0.3.20" -"levn@^0.4.1": - "integrity" "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==" - "resolved" "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" - "version" "0.4.1" +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== dependencies: - "prelude-ls" "^1.2.1" - "type-check" "~0.4.0" + prelude-ls "^1.2.1" + type-check "~0.4.0" -"libnpmaccess@^7.0.1": - "version" "7.0.1" +libnpmaccess@^7.0.1: + version "7.0.1" dependencies: - "npm-package-arg" "^10.1.0" - "npm-registry-fetch" "^14.0.3" + npm-package-arg "^10.1.0" + npm-registry-fetch "^14.0.3" -"libnpmdiff@^5.0.6": - "version" "5.0.6" +libnpmdiff@^5.0.6: + version "5.0.6" dependencies: "@npmcli/arborist" "^6.1.5" "@npmcli/disparity-colors" "^3.0.0" "@npmcli/installed-package-contents" "^2.0.0" - "binary-extensions" "^2.2.0" - "diff" "^5.1.0" - "minimatch" "^5.1.1" - "npm-package-arg" "^10.1.0" - "pacote" "^15.0.7" - "tar" "^6.1.13" + binary-extensions "^2.2.0" + diff "^5.1.0" + minimatch "^5.1.1" + npm-package-arg "^10.1.0" + pacote "^15.0.7" + tar "^6.1.13" -"libnpmexec@^5.0.6": - "version" "5.0.6" +libnpmexec@^5.0.6: + version "5.0.6" dependencies: "@npmcli/arborist" "^6.1.5" "@npmcli/run-script" "^6.0.0" - "chalk" "^4.1.0" - "ci-info" "^3.7.0" - "npm-package-arg" "^10.1.0" - "npmlog" "^7.0.1" - "pacote" "^15.0.7" - "proc-log" "^3.0.0" - "read" "^1.0.7" - "read-package-json-fast" "^3.0.1" - "semver" "^7.3.7" - "walk-up-path" "^1.0.0" - -"libnpmfund@^4.0.6": - "version" "4.0.6" + chalk "^4.1.0" + ci-info "^3.7.0" + npm-package-arg "^10.1.0" + npmlog "^7.0.1" + pacote "^15.0.7" + proc-log "^3.0.0" + read "^1.0.7" + read-package-json-fast "^3.0.1" + semver "^7.3.7" + walk-up-path "^1.0.0" + +libnpmfund@^4.0.6: + version "4.0.6" dependencies: "@npmcli/arborist" "^6.1.5" -"libnpmhook@^9.0.1": - "version" "9.0.1" +libnpmhook@^9.0.1: + version "9.0.1" dependencies: - "aproba" "^2.0.0" - "npm-registry-fetch" "^14.0.3" + aproba "^2.0.0" + npm-registry-fetch "^14.0.3" -"libnpmorg@^5.0.1": - "version" "5.0.1" +libnpmorg@^5.0.1: + version "5.0.1" dependencies: - "aproba" "^2.0.0" - "npm-registry-fetch" "^14.0.3" + aproba "^2.0.0" + npm-registry-fetch "^14.0.3" -"libnpmpack@^5.0.6": - "version" "5.0.6" +libnpmpack@^5.0.6: + version "5.0.6" dependencies: "@npmcli/arborist" "^6.1.5" "@npmcli/run-script" "^6.0.0" - "npm-package-arg" "^10.1.0" - "pacote" "^15.0.7" + npm-package-arg "^10.1.0" + pacote "^15.0.7" -"libnpmpublish@^7.0.6": - "version" "7.0.6" +libnpmpublish@^7.0.6: + version "7.0.6" dependencies: - "normalize-package-data" "^5.0.0" - "npm-package-arg" "^10.1.0" - "npm-registry-fetch" "^14.0.3" - "semver" "^7.3.7" - "ssri" "^10.0.1" + normalize-package-data "^5.0.0" + npm-package-arg "^10.1.0" + npm-registry-fetch "^14.0.3" + semver "^7.3.7" + ssri "^10.0.1" -"libnpmsearch@^6.0.1": - "version" "6.0.1" +libnpmsearch@^6.0.1: + version "6.0.1" dependencies: - "npm-registry-fetch" "^14.0.3" + npm-registry-fetch "^14.0.3" -"libnpmteam@^5.0.1": - "version" "5.0.1" +libnpmteam@^5.0.1: + version "5.0.1" dependencies: - "aproba" "^2.0.0" - "npm-registry-fetch" "^14.0.3" + aproba "^2.0.0" + npm-registry-fetch "^14.0.3" -"libnpmversion@^4.0.1": - "version" "4.0.1" +libnpmversion@^4.0.1: + version "4.0.1" dependencies: "@npmcli/git" "^4.0.1" "@npmcli/run-script" "^6.0.0" - "json-parse-even-better-errors" "^3.0.0" - "proc-log" "^3.0.0" - "semver" "^7.3.7" - -"lilconfig@^2.0.5", "lilconfig@^2.0.6": - "integrity" "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==" - "resolved" "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz" - "version" "2.0.6" - -"locate-path@^6.0.0": - "integrity" "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==" - "resolved" "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" - "version" "6.0.0" - dependencies: - "p-locate" "^5.0.0" - -"lodash.merge@^4.6.2": - "integrity" "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" - "resolved" "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" - "version" "4.6.2" - -"lodash.throttle@^4.1.1": - "integrity" "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" - "resolved" "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz" - "version" "4.1.1" - -"lodash@^4.17.15": - "integrity" "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - "resolved" "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" - "version" "4.17.21" - -"loose-envify@^1.1.0", "loose-envify@^1.2.0", "loose-envify@^1.3.1", "loose-envify@^1.4.0": - "integrity" "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==" - "resolved" "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" - "version" "1.4.0" - dependencies: - "js-tokens" "^3.0.0 || ^4.0.0" - -"lru-cache@^6.0.0": - "integrity" "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==" - "resolved" "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" - "version" "6.0.0" - dependencies: - "yallist" "^4.0.0" - -"lru-cache@^7.4.4", "lru-cache@^7.5.1", "lru-cache@^7.7.1": - "version" "7.13.2" - -"make-fetch-happen@^10.0.3": - "version" "10.2.1" - dependencies: - "agentkeepalive" "^4.2.1" - "cacache" "^16.1.0" - "http-cache-semantics" "^4.1.0" - "http-proxy-agent" "^5.0.0" - "https-proxy-agent" "^5.0.0" - "is-lambda" "^1.0.1" - "lru-cache" "^7.7.1" - "minipass" "^3.1.6" - "minipass-collect" "^1.0.2" - "minipass-fetch" "^2.0.3" - "minipass-flush" "^1.0.5" - "minipass-pipeline" "^1.2.4" - "negotiator" "^0.6.3" - "promise-retry" "^2.0.1" - "socks-proxy-agent" "^7.0.0" - "ssri" "^9.0.0" - -"make-fetch-happen@^11.0.0", "make-fetch-happen@^11.0.2": - "version" "11.0.2" - dependencies: - "agentkeepalive" "^4.2.1" - "cacache" "^17.0.0" - "http-cache-semantics" "^4.1.0" - "http-proxy-agent" "^5.0.0" - "https-proxy-agent" "^5.0.0" - "is-lambda" "^1.0.1" - "lru-cache" "^7.7.1" - "minipass" "^4.0.0" - "minipass-collect" "^1.0.2" - "minipass-fetch" "^3.0.0" - "minipass-flush" "^1.0.5" - "minipass-pipeline" "^1.2.4" - "negotiator" "^0.6.3" - "promise-retry" "^2.0.1" - "socks-proxy-agent" "^7.0.0" - "ssri" "^10.0.0" - -"merge2@^1.3.0", "merge2@^1.4.1": - "integrity" "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" - "resolved" "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" - "version" "1.4.1" - -"micromatch@^4.0.4", "micromatch@^4.0.5": - "integrity" "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==" - "resolved" "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz" - "version" "4.0.5" - dependencies: - "braces" "^3.0.2" - "picomatch" "^2.3.1" - -"minimatch@^3.0.5", "minimatch@^3.1.1", "minimatch@^3.1.2": - "integrity" "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==" - "resolved" "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" - "version" "3.1.2" - dependencies: - "brace-expansion" "^1.1.7" - -"minimatch@^5.0.1", "minimatch@^5.1.1": - "version" "5.1.1" - dependencies: - "brace-expansion" "^2.0.1" - -"minimist@^1.2.0", "minimist@^1.2.6": - "integrity" "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" - "resolved" "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz" - "version" "1.2.7" - -"minipass-collect@^1.0.2": - "version" "1.0.2" - dependencies: - "minipass" "^3.0.0" - -"minipass-fetch@^2.0.3": - "version" "2.1.2" - dependencies: - "minipass" "^3.1.6" - "minipass-sized" "^1.0.3" - "minizlib" "^2.1.2" + json-parse-even-better-errors "^3.0.0" + proc-log "^3.0.0" + semver "^7.3.7" + +lilconfig@^2.0.5, lilconfig@^2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz" + integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg== + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz" + integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ== + +lodash@^4.17.15: + version "4.17.21" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: + version "7.13.2" + +make-fetch-happen@^10.0.3: + version "10.2.1" + dependencies: + agentkeepalive "^4.2.1" + cacache "^16.1.0" + http-cache-semantics "^4.1.0" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^7.7.1" + minipass "^3.1.6" + minipass-collect "^1.0.2" + minipass-fetch "^2.0.3" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^0.6.3" + promise-retry "^2.0.1" + socks-proxy-agent "^7.0.0" + ssri "^9.0.0" + +make-fetch-happen@^11.0.0, make-fetch-happen@^11.0.2: + version "11.0.2" + dependencies: + agentkeepalive "^4.2.1" + cacache "^17.0.0" + http-cache-semantics "^4.1.0" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^7.7.1" + minipass "^4.0.0" + minipass-collect "^1.0.2" + minipass-fetch "^3.0.0" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^0.6.3" + promise-retry "^2.0.1" + socks-proxy-agent "^7.0.0" + ssri "^10.0.0" + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4, micromatch@^4.0.5: + version "4.0.5" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^5.0.1, minimatch@^5.1.1: + version "5.1.1" + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.7" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz" + integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== + +minipass-collect@^1.0.2: + version "1.0.2" + dependencies: + minipass "^3.0.0" + +minipass-fetch@^2.0.3: + version "2.1.2" + dependencies: + minipass "^3.1.6" + minipass-sized "^1.0.3" + minizlib "^2.1.2" optionalDependencies: - "encoding" "^0.1.13" + encoding "^0.1.13" -"minipass-fetch@^3.0.0": - "version" "3.0.0" +minipass-fetch@^3.0.0: + version "3.0.0" dependencies: - "minipass" "^3.1.6" - "minipass-sized" "^1.0.3" - "minizlib" "^2.1.2" + minipass "^3.1.6" + minipass-sized "^1.0.3" + minizlib "^2.1.2" optionalDependencies: - "encoding" "^0.1.13" + encoding "^0.1.13" -"minipass-flush@^1.0.5": - "version" "1.0.5" +minipass-flush@^1.0.5: + version "1.0.5" dependencies: - "minipass" "^3.0.0" + minipass "^3.0.0" -"minipass-json-stream@^1.0.1": - "version" "1.0.1" +minipass-json-stream@^1.0.1: + version "1.0.1" dependencies: - "jsonparse" "^1.3.1" - "minipass" "^3.0.0" + jsonparse "^1.3.1" + minipass "^3.0.0" -"minipass-pipeline@^1.2.4": - "version" "1.2.4" +minipass-pipeline@^1.2.4: + version "1.2.4" dependencies: - "minipass" "^3.0.0" + minipass "^3.0.0" -"minipass-sized@^1.0.3": - "version" "1.0.3" +minipass-sized@^1.0.3: + version "1.0.3" dependencies: - "minipass" "^3.0.0" + minipass "^3.0.0" -"minipass@^3.0.0": - "version" "3.3.6" +minipass@^3.0.0: + version "3.3.6" dependencies: - "yallist" "^4.0.0" + yallist "^4.0.0" -"minipass@^3.1.1", "minipass@^3.1.6": - "version" "3.3.6" +minipass@^3.1.1, minipass@^3.1.6: + version "3.3.6" dependencies: - "yallist" "^4.0.0" + yallist "^4.0.0" -"minipass@^4.0.0": - "version" "4.0.0" +minipass@^4.0.0: + version "4.0.0" dependencies: - "yallist" "^4.0.0" + yallist "^4.0.0" -"minizlib@^2.1.1", "minizlib@^2.1.2": - "version" "2.1.2" +minizlib@^2.1.1, minizlib@^2.1.2: + version "2.1.2" dependencies: - "minipass" "^3.0.0" - "yallist" "^4.0.0" + minipass "^3.0.0" + yallist "^4.0.0" -"mkdirp@^1.0.3", "mkdirp@^1.0.4": - "version" "1.0.4" +mkdirp@^1.0.3, mkdirp@^1.0.4: + version "1.0.4" -"ms@^2.0.0", "ms@^2.1.2": - "version" "2.1.3" +ms@^2.0.0, ms@^2.1.2: + version "2.1.3" -"ms@^2.1.1", "ms@2.1.2": - "integrity" "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - "resolved" "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" - "version" "2.1.2" +ms@^2.1.1, ms@2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -"ms@2.0.0": - "integrity" "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - "resolved" "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" - "version" "2.0.0" +ms@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== -"mute-stream@~0.0.4": - "version" "0.0.8" +mute-stream@~0.0.4: + version "0.0.8" -"nanoid@^3.3.4": - "integrity" "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" - "resolved" "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz" - "version" "3.3.4" +nanoid@^3.3.4: + version "3.3.4" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz" + integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== -"nanoid@^4.0.0": - "integrity" "sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg==" - "resolved" "https://registry.npmjs.org/nanoid/-/nanoid-4.0.0.tgz" - "version" "4.0.0" +nanoid@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-4.0.0.tgz" + integrity sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg== -"natural-compare-lite@^1.4.0": - "integrity" "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==" - "resolved" "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz" - "version" "1.4.0" +natural-compare-lite@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz" + integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== -"natural-compare@^1.4.0": - "integrity" "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" - "resolved" "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" - "version" "1.4.0" +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -"negotiator@^0.6.3": - "version" "0.6.3" +negotiator@^0.6.3: + version "0.6.3" -"node-fetch-native@^1.0.1": - "integrity" "sha512-VzW+TAk2wE4X9maiKMlT+GsPU4OMmR1U9CrHSmd3DFLn2IcZ9VJ6M6BBugGfYUnPCLSYxXdZy17M0BEJyhUTwg==" - "resolved" "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.0.1.tgz" - "version" "1.0.1" +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-gyp@^9.0.0", "node-gyp@^9.3.0": - "version" "9.3.0" +node-gyp@^9.0.0, node-gyp@^9.3.0: + version "9.3.0" dependencies: - "env-paths" "^2.2.0" - "glob" "^7.1.4" - "graceful-fs" "^4.2.6" - "make-fetch-happen" "^10.0.3" - "nopt" "^6.0.0" - "npmlog" "^6.0.0" - "rimraf" "^3.0.2" - "semver" "^7.3.5" - "tar" "^6.1.2" - "which" "^2.0.2" + env-paths "^2.2.0" + glob "^7.1.4" + graceful-fs "^4.2.6" + make-fetch-happen "^10.0.3" + nopt "^6.0.0" + npmlog "^6.0.0" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.2" + which "^2.0.2" -"node-releases@^2.0.6": - "integrity" "sha512-EJ3rzxL9pTWPjk5arA0s0dgXpnyiAbJDE6wHT62g7VsgrgQgmmZ+Ru++M1BFofncWja+Pnn3rEr3fieRySAdKQ==" - "resolved" "https://registry.npmjs.org/node-releases/-/node-releases-2.0.7.tgz" - "version" "2.0.7" +node-releases@^2.0.6: + version "2.0.7" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.7.tgz" + integrity sha512-EJ3rzxL9pTWPjk5arA0s0dgXpnyiAbJDE6wHT62g7VsgrgQgmmZ+Ru++M1BFofncWja+Pnn3rEr3fieRySAdKQ== -"nopt@^6.0.0": - "version" "6.0.0" +nopt@^6.0.0: + version "6.0.0" dependencies: - "abbrev" "^1.0.0" + abbrev "^1.0.0" -"nopt@^7.0.0": - "version" "7.0.0" +nopt@^7.0.0: + version "7.0.0" dependencies: - "abbrev" "^2.0.0" + abbrev "^2.0.0" -"normalize-package-data@^5.0.0": - "version" "5.0.0" +normalize-package-data@^5.0.0: + version "5.0.0" dependencies: - "hosted-git-info" "^6.0.0" - "is-core-module" "^2.8.1" - "semver" "^7.3.5" - "validate-npm-package-license" "^3.0.4" + hosted-git-info "^6.0.0" + is-core-module "^2.8.1" + semver "^7.3.5" + validate-npm-package-license "^3.0.4" -"normalize-path@^3.0.0", "normalize-path@~3.0.0": - "integrity" "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - "resolved" "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" - "version" "3.0.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" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -"normalize-range@^0.1.2": - "integrity" "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==" - "resolved" "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" - "version" "0.1.2" +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" + integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== -"npm-audit-report@^4.0.0": - "version" "4.0.0" +npm-audit-report@^4.0.0: + version "4.0.0" dependencies: - "chalk" "^4.0.0" + chalk "^4.0.0" -"npm-bundled@^3.0.0": - "version" "3.0.0" +npm-bundled@^3.0.0: + version "3.0.0" dependencies: - "npm-normalize-package-bin" "^3.0.0" + npm-normalize-package-bin "^3.0.0" -"npm-install-checks@^6.0.0": - "version" "6.0.0" +npm-install-checks@^6.0.0: + version "6.0.0" dependencies: - "semver" "^7.1.1" + semver "^7.1.1" -"npm-normalize-package-bin@^3.0.0": - "version" "3.0.0" +npm-normalize-package-bin@^3.0.0: + version "3.0.0" -"npm-package-arg@^10.0.0", "npm-package-arg@^10.1.0": - "version" "10.1.0" +npm-package-arg@^10.0.0, npm-package-arg@^10.1.0: + version "10.1.0" dependencies: - "hosted-git-info" "^6.0.0" - "proc-log" "^3.0.0" - "semver" "^7.3.5" - "validate-npm-package-name" "^5.0.0" + hosted-git-info "^6.0.0" + proc-log "^3.0.0" + semver "^7.3.5" + validate-npm-package-name "^5.0.0" -"npm-packlist@^7.0.0": - "version" "7.0.4" +npm-packlist@^7.0.0: + version "7.0.4" dependencies: - "ignore-walk" "^6.0.0" + ignore-walk "^6.0.0" -"npm-pick-manifest@^8.0.0", "npm-pick-manifest@^8.0.1": - "version" "8.0.1" +npm-pick-manifest@^8.0.0, npm-pick-manifest@^8.0.1: + version "8.0.1" dependencies: - "npm-install-checks" "^6.0.0" - "npm-normalize-package-bin" "^3.0.0" - "npm-package-arg" "^10.0.0" - "semver" "^7.3.5" + npm-install-checks "^6.0.0" + npm-normalize-package-bin "^3.0.0" + npm-package-arg "^10.0.0" + semver "^7.3.5" -"npm-profile@^7.0.1": - "version" "7.0.1" +npm-profile@^7.0.1: + version "7.0.1" dependencies: - "npm-registry-fetch" "^14.0.0" - "proc-log" "^3.0.0" + npm-registry-fetch "^14.0.0" + proc-log "^3.0.0" -"npm-registry-fetch@^14.0.0", "npm-registry-fetch@^14.0.3": - "version" "14.0.3" +npm-registry-fetch@^14.0.0, npm-registry-fetch@^14.0.3: + version "14.0.3" dependencies: - "make-fetch-happen" "^11.0.0" - "minipass" "^4.0.0" - "minipass-fetch" "^3.0.0" - "minipass-json-stream" "^1.0.1" - "minizlib" "^2.1.2" - "npm-package-arg" "^10.0.0" - "proc-log" "^3.0.0" + make-fetch-happen "^11.0.0" + minipass "^4.0.0" + minipass-fetch "^3.0.0" + minipass-json-stream "^1.0.1" + minizlib "^2.1.2" + npm-package-arg "^10.0.0" + proc-log "^3.0.0" -"npm-user-validate@^1.0.1": - "version" "1.0.1" +npm-user-validate@^1.0.1: + version "1.0.1" -"npm@^9.2.0": - "integrity" "sha512-oypVdaWGHDuV79RXLvp+B9gh6gDyAmoHKrQ0/JBYTWWx5D8/+AAxFdZC84fSIiyDdyW4qfrSyYGKhekxDOaMXQ==" - "resolved" "https://registry.npmjs.org/npm/-/npm-9.2.0.tgz" - "version" "9.2.0" +npm@^9.2.0: + version "9.2.0" + resolved "https://registry.npmjs.org/npm/-/npm-9.2.0.tgz" + integrity sha512-oypVdaWGHDuV79RXLvp+B9gh6gDyAmoHKrQ0/JBYTWWx5D8/+AAxFdZC84fSIiyDdyW4qfrSyYGKhekxDOaMXQ== dependencies: "@isaacs/string-locale-compare" "^1.1.0" "@npmcli/arborist" "^6.1.5" @@ -2513,1144 +2513,1144 @@ "@npmcli/map-workspaces" "^3.0.0" "@npmcli/package-json" "^3.0.0" "@npmcli/run-script" "^6.0.0" - "abbrev" "^2.0.0" - "archy" "~1.0.0" - "cacache" "^17.0.3" - "chalk" "^4.1.2" - "ci-info" "^3.7.0" - "cli-columns" "^4.0.0" - "cli-table3" "^0.6.3" - "columnify" "^1.6.0" - "fastest-levenshtein" "^1.0.16" - "fs-minipass" "^2.1.0" - "glob" "^8.0.1" - "graceful-fs" "^4.2.10" - "hosted-git-info" "^6.1.1" - "ini" "^3.0.1" - "init-package-json" "^4.0.1" - "is-cidr" "^4.0.2" - "json-parse-even-better-errors" "^3.0.0" - "libnpmaccess" "^7.0.1" - "libnpmdiff" "^5.0.6" - "libnpmexec" "^5.0.6" - "libnpmfund" "^4.0.6" - "libnpmhook" "^9.0.1" - "libnpmorg" "^5.0.1" - "libnpmpack" "^5.0.6" - "libnpmpublish" "^7.0.6" - "libnpmsearch" "^6.0.1" - "libnpmteam" "^5.0.1" - "libnpmversion" "^4.0.1" - "make-fetch-happen" "^11.0.2" - "minimatch" "^5.1.1" - "minipass" "^4.0.0" - "minipass-pipeline" "^1.2.4" - "mkdirp" "^1.0.4" - "ms" "^2.1.2" - "node-gyp" "^9.3.0" - "nopt" "^7.0.0" - "npm-audit-report" "^4.0.0" - "npm-install-checks" "^6.0.0" - "npm-package-arg" "^10.1.0" - "npm-pick-manifest" "^8.0.1" - "npm-profile" "^7.0.1" - "npm-registry-fetch" "^14.0.3" - "npm-user-validate" "^1.0.1" - "npmlog" "^7.0.1" - "p-map" "^4.0.0" - "pacote" "^15.0.7" - "parse-conflict-json" "^3.0.0" - "proc-log" "^3.0.0" - "qrcode-terminal" "^0.12.0" - "read" "~1.0.7" - "read-package-json" "^6.0.0" - "read-package-json-fast" "^3.0.1" - "rimraf" "^3.0.2" - "semver" "^7.3.8" - "ssri" "^10.0.1" - "tar" "^6.1.13" - "text-table" "~0.2.0" - "tiny-relative-date" "^1.3.0" - "treeverse" "^3.0.0" - "validate-npm-package-name" "^5.0.0" - "which" "^3.0.0" - "write-file-atomic" "^5.0.0" - -"npmlog@^6.0.0": - "version" "6.0.2" - dependencies: - "are-we-there-yet" "^3.0.0" - "console-control-strings" "^1.1.0" - "gauge" "^4.0.3" - "set-blocking" "^2.0.0" - -"npmlog@^7.0.1": - "version" "7.0.1" - dependencies: - "are-we-there-yet" "^4.0.0" - "console-control-strings" "^1.1.0" - "gauge" "^5.0.0" - "set-blocking" "^2.0.0" - -"object-assign@^4.1.1": - "integrity" "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" - "resolved" "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" - "version" "4.1.1" - -"object-hash@^3.0.0": - "integrity" "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==" - "resolved" "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz" - "version" "3.0.0" - -"object-inspect@^1.12.2", "object-inspect@^1.9.0": - "integrity" "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" - "resolved" "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz" - "version" "1.12.2" - -"object-keys@^1.1.1": - "integrity" "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" - "resolved" "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" - "version" "1.1.1" - -"object.assign@^4.1.2", "object.assign@^4.1.3", "object.assign@^4.1.4": - "integrity" "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==" - "resolved" "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz" - "version" "4.1.4" - dependencies: - "call-bind" "^1.0.2" - "define-properties" "^1.1.4" - "has-symbols" "^1.0.3" - "object-keys" "^1.1.1" - -"object.entries@^1.1.5": - "integrity" "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==" - "resolved" "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz" - "version" "1.1.6" - dependencies: - "call-bind" "^1.0.2" - "define-properties" "^1.1.4" - "es-abstract" "^1.20.4" - -"object.fromentries@^2.0.5": - "integrity" "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==" - "resolved" "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz" - "version" "2.0.6" - dependencies: - "call-bind" "^1.0.2" - "define-properties" "^1.1.4" - "es-abstract" "^1.20.4" - -"object.hasown@^1.1.0": - "integrity" "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==" - "resolved" "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz" - "version" "1.1.2" - dependencies: - "define-properties" "^1.1.4" - "es-abstract" "^1.20.4" - -"object.values@^1.1.5": - "integrity" "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==" - "resolved" "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz" - "version" "1.1.6" - dependencies: - "call-bind" "^1.0.2" - "define-properties" "^1.1.4" - "es-abstract" "^1.20.4" - -"ofetch@^1.0.0": - "integrity" "sha512-d40aof8czZFSQKJa4+F7Ch3UC5D631cK1TTUoK+iNEut9NoiCL+u0vykl/puYVUS2df4tIQl5upQcolIcEzQjQ==" - "resolved" "https://registry.npmjs.org/ofetch/-/ofetch-1.0.0.tgz" - "version" "1.0.0" - dependencies: - "destr" "^1.2.1" - "node-fetch-native" "^1.0.1" - "ufo" "^1.0.0" - -"once@^1.3.0": - "integrity" "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==" - "resolved" "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - "version" "1.4.0" - dependencies: - "wrappy" "1" - -"optionator@^0.9.1": - "integrity" "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==" - "resolved" "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz" - "version" "0.9.1" - dependencies: - "deep-is" "^0.1.3" - "fast-levenshtein" "^2.0.6" - "levn" "^0.4.1" - "prelude-ls" "^1.2.1" - "type-check" "^0.4.0" - "word-wrap" "^1.2.3" - -"p-limit@^3.0.2": - "integrity" "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==" - "resolved" "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" - "version" "3.1.0" - dependencies: - "yocto-queue" "^0.1.0" - -"p-locate@^5.0.0": - "integrity" "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==" - "resolved" "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" - "version" "5.0.0" + abbrev "^2.0.0" + archy "~1.0.0" + cacache "^17.0.3" + chalk "^4.1.2" + ci-info "^3.7.0" + cli-columns "^4.0.0" + cli-table3 "^0.6.3" + columnify "^1.6.0" + fastest-levenshtein "^1.0.16" + fs-minipass "^2.1.0" + glob "^8.0.1" + graceful-fs "^4.2.10" + hosted-git-info "^6.1.1" + ini "^3.0.1" + init-package-json "^4.0.1" + is-cidr "^4.0.2" + json-parse-even-better-errors "^3.0.0" + libnpmaccess "^7.0.1" + libnpmdiff "^5.0.6" + libnpmexec "^5.0.6" + libnpmfund "^4.0.6" + libnpmhook "^9.0.1" + libnpmorg "^5.0.1" + libnpmpack "^5.0.6" + libnpmpublish "^7.0.6" + libnpmsearch "^6.0.1" + libnpmteam "^5.0.1" + libnpmversion "^4.0.1" + make-fetch-happen "^11.0.2" + minimatch "^5.1.1" + minipass "^4.0.0" + minipass-pipeline "^1.2.4" + mkdirp "^1.0.4" + ms "^2.1.2" + node-gyp "^9.3.0" + nopt "^7.0.0" + npm-audit-report "^4.0.0" + npm-install-checks "^6.0.0" + npm-package-arg "^10.1.0" + npm-pick-manifest "^8.0.1" + npm-profile "^7.0.1" + npm-registry-fetch "^14.0.3" + npm-user-validate "^1.0.1" + npmlog "^7.0.1" + p-map "^4.0.0" + pacote "^15.0.7" + parse-conflict-json "^3.0.0" + proc-log "^3.0.0" + qrcode-terminal "^0.12.0" + read "~1.0.7" + read-package-json "^6.0.0" + read-package-json-fast "^3.0.1" + rimraf "^3.0.2" + semver "^7.3.8" + ssri "^10.0.1" + tar "^6.1.13" + text-table "~0.2.0" + tiny-relative-date "^1.3.0" + treeverse "^3.0.0" + validate-npm-package-name "^5.0.0" + which "^3.0.0" + write-file-atomic "^5.0.0" + +npmlog@^6.0.0: + version "6.0.2" + dependencies: + are-we-there-yet "^3.0.0" + console-control-strings "^1.1.0" + gauge "^4.0.3" + set-blocking "^2.0.0" + +npmlog@^7.0.1: + version "7.0.1" + dependencies: + are-we-there-yet "^4.0.0" + console-control-strings "^1.1.0" + gauge "^5.0.0" + set-blocking "^2.0.0" + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + +object-inspect@^1.12.2, object-inspect@^1.9.0: + version "1.12.2" + resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz" + integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.2, object.assign@^4.1.3, object.assign@^4.1.4: + version "4.1.4" + resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz" + integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +object.entries@^1.1.5: + version "1.1.6" + resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz" + integrity sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +object.fromentries@^2.0.5: + version "2.0.6" + resolved "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz" + integrity sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +object.hasown@^1.1.0: + version "1.1.2" + resolved "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz" + integrity sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw== + dependencies: + define-properties "^1.1.4" + es-abstract "^1.20.4" + +object.values@^1.1.5: + version "1.1.6" + resolved "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz" + integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +ofetch@^1.0.0: + 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.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" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== dependencies: - "p-limit" "^3.0.2" - -"p-map@^4.0.0": - "version" "4.0.0" + p-limit "^3.0.2" + +p-map@^4.0.0: + version "4.0.0" dependencies: - "aggregate-error" "^3.0.0" - -"pacote@^15.0.0", "pacote@^15.0.7": - "version" "15.0.7" + aggregate-error "^3.0.0" + +pacote@^15.0.0, pacote@^15.0.7: + version "15.0.7" dependencies: "@npmcli/git" "^4.0.0" "@npmcli/installed-package-contents" "^2.0.1" "@npmcli/promise-spawn" "^6.0.1" "@npmcli/run-script" "^6.0.0" - "cacache" "^17.0.0" - "fs-minipass" "^2.1.0" - "minipass" "^4.0.0" - "npm-package-arg" "^10.0.0" - "npm-packlist" "^7.0.0" - "npm-pick-manifest" "^8.0.0" - "npm-registry-fetch" "^14.0.0" - "proc-log" "^3.0.0" - "promise-retry" "^2.0.1" - "read-package-json" "^6.0.0" - "read-package-json-fast" "^3.0.0" - "ssri" "^10.0.0" - "tar" "^6.1.11" - -"parent-module@^1.0.0": - "integrity" "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==" - "resolved" "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" - "version" "1.0.1" - dependencies: - "callsites" "^3.0.0" - -"parse-conflict-json@^3.0.0": - "version" "3.0.0" - dependencies: - "json-parse-even-better-errors" "^3.0.0" - "just-diff" "^5.0.1" - "just-diff-apply" "^5.2.0" - -"path-exists@^4.0.0": - "integrity" "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" - "resolved" "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" - "version" "4.0.0" - -"path-is-absolute@^1.0.0": - "integrity" "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" - "resolved" "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" - "version" "1.0.1" - -"path-key@^3.1.0": - "integrity" "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - "resolved" "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" - "version" "3.1.1" - -"path-parse@^1.0.7": - "integrity" "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - "resolved" "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" - "version" "1.0.7" - -"path-to-regexp@^1.7.0": - "integrity" "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==" - "resolved" "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz" - "version" "1.8.0" - dependencies: - "isarray" "0.0.1" - -"path-type@^4.0.0": - "integrity" "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" - "resolved" "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" - "version" "4.0.0" - -"performance-now@^2.1.0": - "integrity" "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" - "resolved" "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" - "version" "2.1.0" - -"picocolors@^1.0.0": - "integrity" "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - "resolved" "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" - "version" "1.0.0" - -"picomatch@^2.0.4", "picomatch@^2.2.1", "picomatch@^2.3.1": - "integrity" "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" - "resolved" "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" - "version" "2.3.1" - -"pify@^2.3.0": - "integrity" "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==" - "resolved" "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" - "version" "2.3.0" - -"postcss-import@^14.1.0": - "integrity" "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==" - "resolved" "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz" - "version" "14.1.0" - dependencies: - "postcss-value-parser" "^4.0.0" - "read-cache" "^1.0.0" - "resolve" "^1.1.7" - -"postcss-js@^4.0.0": - "integrity" "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==" - "resolved" "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz" - "version" "4.0.0" - dependencies: - "camelcase-css" "^2.0.1" - -"postcss-load-config@^3.1.4": - "integrity" "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==" - "resolved" "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz" - "version" "3.1.4" - dependencies: - "lilconfig" "^2.0.5" - "yaml" "^1.10.2" - -"postcss-nested@6.0.0": - "integrity" "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==" - "resolved" "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz" - "version" "6.0.0" - dependencies: - "postcss-selector-parser" "^6.0.10" - -"postcss-selector-parser@^6.0.10": - "integrity" "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==" - "resolved" "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz" - "version" "6.0.11" + cacache "^17.0.0" + fs-minipass "^2.1.0" + minipass "^4.0.0" + npm-package-arg "^10.0.0" + npm-packlist "^7.0.0" + npm-pick-manifest "^8.0.0" + npm-registry-fetch "^14.0.0" + proc-log "^3.0.0" + promise-retry "^2.0.1" + read-package-json "^6.0.0" + read-package-json-fast "^3.0.0" + ssri "^10.0.0" + tar "^6.1.11" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-conflict-json@^3.0.0: + version "3.0.0" + dependencies: + json-parse-even-better-errors "^3.0.0" + just-diff "^5.0.1" + just-diff-apply "^5.2.0" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +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" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pify@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + +postcss-import@^14.1.0: + version "14.1.0" + resolved "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz" + integrity sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw== + dependencies: + postcss-value-parser "^4.0.0" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-js@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz" + integrity sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ== + dependencies: + camelcase-css "^2.0.1" + +postcss-load-config@^3.1.4: + version "3.1.4" + resolved "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz" + integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg== + dependencies: + lilconfig "^2.0.5" + yaml "^1.10.2" + +postcss-nested@6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz" + integrity sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w== + dependencies: + postcss-selector-parser "^6.0.10" + +postcss-selector-parser@^6.0.10: + version "6.0.11" + resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz" + integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== dependencies: - "cssesc" "^3.0.0" - "util-deprecate" "^1.0.2" + cssesc "^3.0.0" + util-deprecate "^1.0.2" -"postcss-value-parser@^4.0.0", "postcss-value-parser@^4.2.0": - "integrity" "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - "resolved" "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" - "version" "4.2.0" +postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -"postcss@^8.0.0", "postcss@^8.1.0", "postcss@^8.2.14", "postcss@^8.3.3", "postcss@^8.4.18", "postcss@^8.4.20", "postcss@>=8.0.9": - "integrity" "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==" - "resolved" "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz" - "version" "8.4.20" - dependencies: - "nanoid" "^3.3.4" - "picocolors" "^1.0.0" - "source-map-js" "^1.0.2" - -"prelude-ls@^1.2.1": - "integrity" "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" - "resolved" "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" - "version" "1.2.1" - -"prettier-linter-helpers@^1.0.0": - "integrity" "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==" - "resolved" "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz" - "version" "1.0.0" - dependencies: - "fast-diff" "^1.1.2" - -"prettier-plugin-tailwindcss@^0.1.7": - "integrity" "sha512-/EKQURUrxLu66CMUg4+1LwGdxnz8of7IDvrSLqEtDqhLH61SAlNNUSr90UTvZaemujgl3OH/VHg+fyGltrNixw==" - "resolved" "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.1.13.tgz" - "version" "0.1.13" +postcss@^8.0.0, postcss@^8.1.0, postcss@^8.2.14, postcss@^8.3.3, postcss@^8.4.18, postcss@^8.4.20, postcss@>=8.0.9: + version "8.4.20" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz" + integrity sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g== + dependencies: + nanoid "^3.3.4" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" + +prettier-plugin-tailwindcss@^0.1.7: + version "0.1.13" + resolved "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.1.13.tgz" + integrity sha512-/EKQURUrxLu66CMUg4+1LwGdxnz8of7IDvrSLqEtDqhLH61SAlNNUSr90UTvZaemujgl3OH/VHg+fyGltrNixw== -"prettier@^2.5.1", "prettier@>=2.0.0", "prettier@>=2.2.0": - "integrity" "sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==" - "resolved" "https://registry.npmjs.org/prettier/-/prettier-2.8.1.tgz" - "version" "2.8.1" +prettier@^2.5.1, prettier@>=2.0.0, prettier@>=2.2.0: + version "2.8.1" + resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.1.tgz" + integrity sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg== -"proc-log@^3.0.0": - "version" "3.0.0" +proc-log@^3.0.0: + version "3.0.0" -"process@^0.11.10": - "version" "0.11.10" - -"promise-all-reject-late@^1.0.0": - "version" "1.0.1" +process@^0.11.10: + version "0.11.10" + +promise-all-reject-late@^1.0.0: + version "1.0.1" -"promise-call-limit@^1.0.1": - "version" "1.0.1" +promise-call-limit@^1.0.1: + version "1.0.1" -"promise-inflight@^1.0.1": - "version" "1.0.1" +promise-inflight@^1.0.1: + version "1.0.1" -"promise-retry@^2.0.1": - "version" "2.0.1" +promise-retry@^2.0.1: + version "2.0.1" dependencies: - "err-code" "^2.0.2" - "retry" "^0.12.0" + err-code "^2.0.2" + retry "^0.12.0" -"promzard@^0.3.0": - "version" "0.3.0" +promzard@^0.3.0: + version "0.3.0" dependencies: - "read" "1" + read "1" -"prop-types@^15.6.0", "prop-types@^15.6.2", "prop-types@^15.7.2", "prop-types@^15.8.1": - "integrity" "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==" - "resolved" "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" - "version" "15.8.1" +prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== dependencies: - "loose-envify" "^1.4.0" - "object-assign" "^4.1.1" - "react-is" "^16.13.1" + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" -"punycode@^2.1.0": - "integrity" "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - "resolved" "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" - "version" "2.1.1" +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -"qrcode-terminal@^0.12.0": - "version" "0.12.0" +qrcode-terminal@^0.12.0: + version "0.12.0" -"queue-microtask@^1.2.2": - "integrity" "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" - "resolved" "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" - "version" "1.2.3" +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -"quick-lru@^5.1.1": - "integrity" "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" - "resolved" "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz" - "version" "5.1.1" +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== -"raf@^3.0.0": - "integrity" "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==" - "resolved" "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz" - "version" "3.4.1" +raf@^3.0.0: + version "3.4.1" + resolved "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz" + integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== dependencies: - "performance-now" "^2.1.0" + performance-now "^2.1.0" -"react-dom@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^16 || ^17 || ^18", "react-dom@^17.0.2", "react-dom@>=16.6.0": - "integrity" "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==" - "resolved" "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz" - "version" "17.0.2" +"react-dom@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^16 || ^17 || ^18", react-dom@^17.0.2, react-dom@>=16.6.0: + version "17.0.2" + resolved "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz" + integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== dependencies: - "loose-envify" "^1.1.0" - "object-assign" "^4.1.1" - "scheduler" "^0.20.2" + loose-envify "^1.1.0" + object-assign "^4.1.1" + scheduler "^0.20.2" -"react-fast-compare@^3.1.1": - "integrity" "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" - "resolved" "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz" - "version" "3.2.0" +react-fast-compare@^3.1.1: + version "3.2.0" + resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz" + integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== -"react-helmet@^6.1.0": - "integrity" "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==" - "resolved" "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz" - "version" "6.1.0" +react-helmet@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz" + integrity sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw== dependencies: - "object-assign" "^4.1.1" - "prop-types" "^15.7.2" - "react-fast-compare" "^3.1.1" - "react-side-effect" "^2.1.0" + object-assign "^4.1.1" + prop-types "^15.7.2" + react-fast-compare "^3.1.1" + react-side-effect "^2.1.0" -"react-i18next@^12.1.1": - "integrity" "sha512-mFdieOI0LDy84q3JuZU6Aou1DoWW2fhapcTGeBS8+vWSJuViuoCLQAMYSb0QoHhXS8B0WKUOPpx4cffAP7r/aA==" - "resolved" "https://registry.npmjs.org/react-i18next/-/react-i18next-12.1.1.tgz" - "version" "12.1.1" +react-i18next@^12.1.1: + version "12.1.1" + resolved "https://registry.npmjs.org/react-i18next/-/react-i18next-12.1.1.tgz" + integrity sha512-mFdieOI0LDy84q3JuZU6Aou1DoWW2fhapcTGeBS8+vWSJuViuoCLQAMYSb0QoHhXS8B0WKUOPpx4cffAP7r/aA== dependencies: "@babel/runtime" "^7.14.5" - "html-parse-stringify" "^3.0.1" + html-parse-stringify "^3.0.1" -"react-is@^16.13.1", "react-is@^16.6.0", "react-is@^16.7.0": - "integrity" "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - "resolved" "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" - "version" "16.13.1" +react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -"react-router-dom@^5.2.0": - "integrity" "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==" - "resolved" "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz" - "version" "5.3.4" +react-router-dom@^5.2.0: + version "5.3.4" + resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz" + integrity sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ== dependencies: "@babel/runtime" "^7.12.13" - "history" "^4.9.0" - "loose-envify" "^1.3.1" - "prop-types" "^15.6.2" - "react-router" "5.3.4" - "tiny-invariant" "^1.0.2" - "tiny-warning" "^1.0.0" - -"react-router@5.3.4": - "integrity" "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==" - "resolved" "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz" - "version" "5.3.4" + history "^4.9.0" + loose-envify "^1.3.1" + prop-types "^15.6.2" + react-router "5.3.4" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + +react-router@5.3.4: + version "5.3.4" + resolved "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz" + integrity sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA== dependencies: "@babel/runtime" "^7.12.13" - "history" "^4.9.0" - "hoist-non-react-statics" "^3.1.0" - "loose-envify" "^1.3.1" - "path-to-regexp" "^1.7.0" - "prop-types" "^15.6.2" - "react-is" "^16.6.0" - "tiny-invariant" "^1.0.2" - "tiny-warning" "^1.0.0" - -"react-side-effect@^2.1.0": - "integrity" "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==" - "resolved" "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz" - "version" "2.1.2" - -"react-stickynode@^4.1.0": - "integrity" "sha512-zylWgfad75jLfh/gYIayDcDWIDwO4weZrsZqDpjZ/axhF06zRjdCWFBgUr33Pvv2+htKWqPSFksWTyB6aMQ1ZQ==" - "resolved" "https://registry.npmjs.org/react-stickynode/-/react-stickynode-4.1.0.tgz" - "version" "4.1.0" - dependencies: - "classnames" "^2.0.0" - "core-js" "^3.6.5" - "prop-types" "^15.6.0" - "shallowequal" "^1.0.0" - "subscribe-ui-event" "^2.0.6" - -"react-transition-group@^4.4.5": - "integrity" "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==" - "resolved" "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz" - "version" "4.4.5" + history "^4.9.0" + hoist-non-react-statics "^3.1.0" + loose-envify "^1.3.1" + path-to-regexp "^1.7.0" + prop-types "^15.6.2" + react-is "^16.6.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + +react-side-effect@^2.1.0: + version "2.1.2" + resolved "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz" + integrity sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw== + +react-stickynode@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/react-stickynode/-/react-stickynode-4.1.0.tgz" + integrity sha512-zylWgfad75jLfh/gYIayDcDWIDwO4weZrsZqDpjZ/axhF06zRjdCWFBgUr33Pvv2+htKWqPSFksWTyB6aMQ1ZQ== + dependencies: + classnames "^2.0.0" + core-js "^3.6.5" + prop-types "^15.6.0" + shallowequal "^1.0.0" + subscribe-ui-event "^2.0.6" + +react-transition-group@^4.4.5: + version "4.4.5" + resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== dependencies: "@babel/runtime" "^7.5.5" - "dom-helpers" "^5.0.1" - "loose-envify" "^1.4.0" - "prop-types" "^15.6.2" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" -"react@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16 || ^17 || ^18", "react@^16.3.0 || ^17.0.0 || ^18.0.0", "react@^17.0.2", "react@>= 16.8.0", "react@>=15", "react@>=16.3.0", "react@>=16.6.0", "react@17.0.2": - "integrity" "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==" - "resolved" "https://registry.npmjs.org/react/-/react-17.0.2.tgz" - "version" "17.0.2" +"react@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16 || ^17 || ^18", "react@^16.3.0 || ^17.0.0 || ^18.0.0", react@^17.0.2, "react@>= 16.8.0", react@>=15, react@>=16.3.0, react@>=16.6.0, react@17.0.2: + version "17.0.2" + resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz" + integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== dependencies: - "loose-envify" "^1.1.0" - "object-assign" "^4.1.1" + loose-envify "^1.1.0" + object-assign "^4.1.1" -"read-cache@^1.0.0": - "integrity" "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==" - "resolved" "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz" - "version" "1.0.0" +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz" + integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== dependencies: - "pify" "^2.3.0" + pify "^2.3.0" -"read-cmd-shim@^4.0.0": - "version" "4.0.0" +read-cmd-shim@^4.0.0: + version "4.0.0" -"read-package-json-fast@^3.0.0", "read-package-json-fast@^3.0.1": - "version" "3.0.1" +read-package-json-fast@^3.0.0, read-package-json-fast@^3.0.1: + version "3.0.1" dependencies: - "json-parse-even-better-errors" "^3.0.0" - "npm-normalize-package-bin" "^3.0.0" + json-parse-even-better-errors "^3.0.0" + npm-normalize-package-bin "^3.0.0" -"read-package-json@^6.0.0": - "version" "6.0.0" +read-package-json@^6.0.0: + version "6.0.0" dependencies: - "glob" "^8.0.1" - "json-parse-even-better-errors" "^3.0.0" - "normalize-package-data" "^5.0.0" - "npm-normalize-package-bin" "^3.0.0" + glob "^8.0.1" + json-parse-even-better-errors "^3.0.0" + normalize-package-data "^5.0.0" + npm-normalize-package-bin "^3.0.0" -"read@^1.0.7", "read@~1.0.7", "read@1": - "version" "1.0.7" +read@^1.0.7, read@~1.0.7, read@1: + version "1.0.7" dependencies: - "mute-stream" "~0.0.4" + mute-stream "~0.0.4" -"readable-stream@^3.6.0": - "version" "3.6.0" +readable-stream@^3.6.0: + version "3.6.0" dependencies: - "inherits" "^2.0.3" - "string_decoder" "^1.1.1" - "util-deprecate" "^1.0.1" + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" -"readable-stream@^4.1.0": - "version" "4.2.0" +readable-stream@^4.1.0: + version "4.2.0" dependencies: - "abort-controller" "^3.0.0" - "buffer" "^6.0.3" - "events" "^3.3.0" - "process" "^0.11.10" + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" -"readdirp@~3.6.0": - "integrity" "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==" - "resolved" "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" - "version" "3.6.0" +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== dependencies: - "picomatch" "^2.2.1" + picomatch "^2.2.1" -"regenerator-runtime@^0.13.11": - "integrity" "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" - "resolved" "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz" - "version" "0.13.11" +regenerator-runtime@^0.13.11: + version "0.13.11" + resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== -"regexp.prototype.flags@^1.4.3": - "integrity" "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==" - "resolved" "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz" - "version" "1.4.3" +regexp.prototype.flags@^1.4.3: + version "1.4.3" + resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz" + integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== dependencies: - "call-bind" "^1.0.2" - "define-properties" "^1.1.3" - "functions-have-names" "^1.2.2" + call-bind "^1.0.2" + define-properties "^1.1.3" + functions-have-names "^1.2.2" -"regexpp@^3.2.0": - "integrity" "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==" - "resolved" "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz" - "version" "3.2.0" +regexpp@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== -"resolve-from@^4.0.0": - "integrity" "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" - "resolved" "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" - "version" "4.0.0" +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -"resolve-pathname@^3.0.0": - "integrity" "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" - "resolved" "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz" - "version" "3.0.0" +resolve-pathname@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz" + integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== -"resolve@^1.1.7", "resolve@^1.20.0", "resolve@^1.22.0", "resolve@^1.22.1": - "integrity" "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==" - "resolved" "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz" - "version" "1.22.1" +resolve@^1.1.7, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.22.1: + version "1.22.1" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== dependencies: - "is-core-module" "^2.9.0" - "path-parse" "^1.0.7" - "supports-preserve-symlinks-flag" "^1.0.0" + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" -"resolve@^2.0.0-next.3": - "integrity" "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==" - "resolved" "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz" - "version" "2.0.0-next.4" +resolve@^2.0.0-next.3: + version "2.0.0-next.4" + resolved "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz" + integrity sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ== dependencies: - "is-core-module" "^2.9.0" - "path-parse" "^1.0.7" - "supports-preserve-symlinks-flag" "^1.0.0" + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" -"retry@^0.12.0": - "version" "0.12.0" +retry@^0.12.0: + version "0.12.0" -"reusify@^1.0.4": - "integrity" "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" - "resolved" "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" - "version" "1.0.4" +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -"rimraf@^3.0.2": - "integrity" "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==" - "resolved" "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" - "version" "3.0.2" +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: - "glob" "^7.1.3" + glob "^7.1.3" -"rollup@^3.7.0": - "integrity" "sha512-jN9rx3k5pfg9H9al0r0y1EYKSeiRANZRYX32SuNXAnKzh6cVyf4LZVto1KAuDnbHT03E1CpsgqDKaqQ8FZtgxw==" - "resolved" "https://registry.npmjs.org/rollup/-/rollup-3.7.4.tgz" - "version" "3.7.4" +rollup@^3.7.0: + version "3.7.4" + resolved "https://registry.npmjs.org/rollup/-/rollup-3.7.4.tgz" + integrity sha512-jN9rx3k5pfg9H9al0r0y1EYKSeiRANZRYX32SuNXAnKzh6cVyf4LZVto1KAuDnbHT03E1CpsgqDKaqQ8FZtgxw== optionalDependencies: - "fsevents" "~2.3.2" + fsevents "~2.3.2" -"run-parallel@^1.1.9": - "integrity" "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==" - "resolved" "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" - "version" "1.2.0" +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== dependencies: - "queue-microtask" "^1.2.2" + queue-microtask "^1.2.2" -"safe-buffer@~5.2.0": - "integrity" "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - "resolved" "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" - "version" "5.2.1" +safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -"safe-regex-test@^1.0.0": - "integrity" "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==" - "resolved" "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz" - "version" "1.0.0" +safe-regex-test@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz" + integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== dependencies: - "call-bind" "^1.0.2" - "get-intrinsic" "^1.1.3" - "is-regex" "^1.1.4" + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-regex "^1.1.4" "safer-buffer@>= 2.1.2 < 3.0.0": - "integrity" "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - "resolved" "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" - "version" "2.1.2" + version "2.1.2" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -"scheduler@^0.20.2": - "integrity" "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==" - "resolved" "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz" - "version" "0.20.2" +scheduler@^0.20.2: + version "0.20.2" + resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz" + integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== dependencies: - "loose-envify" "^1.1.0" - "object-assign" "^4.1.1" + loose-envify "^1.1.0" + object-assign "^4.1.1" -"semver@^6.3.0": - "integrity" "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - "resolved" "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" - "version" "6.3.0" +semver@^6.3.0: + version "6.3.0" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -"semver@^7.0.0", "semver@^7.1.1", "semver@^7.3.5", "semver@^7.3.7", "semver@^7.3.8": - "integrity" "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==" - "resolved" "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz" - "version" "7.3.8" +semver@^7.0.0, semver@^7.1.1, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: + version "7.3.8" + resolved "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz" + integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== dependencies: - "lru-cache" "^6.0.0" + lru-cache "^6.0.0" -"set-blocking@^2.0.0": - "version" "2.0.0" +set-blocking@^2.0.0: + version "2.0.0" -"shallowequal@^1.0.0": - "integrity" "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" - "resolved" "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz" - "version" "1.1.0" +shallowequal@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz" + integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== -"shebang-command@^2.0.0": - "integrity" "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==" - "resolved" "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" - "version" "2.0.0" +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== dependencies: - "shebang-regex" "^3.0.0" + shebang-regex "^3.0.0" -"shebang-regex@^3.0.0": - "integrity" "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - "resolved" "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" - "version" "3.0.0" +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -"side-channel@^1.0.4": - "integrity" "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==" - "resolved" "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz" - "version" "1.0.4" +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== dependencies: - "call-bind" "^1.0.0" - "get-intrinsic" "^1.0.2" - "object-inspect" "^1.9.0" + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" -"signal-exit@^3.0.7": - "version" "3.0.7" +signal-exit@^3.0.7: + version "3.0.7" -"slash@^3.0.0": - "integrity" "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" - "resolved" "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" - "version" "3.0.0" +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -"smart-buffer@^4.2.0": - "version" "4.2.0" +smart-buffer@^4.2.0: + version "4.2.0" -"socks-proxy-agent@^7.0.0": - "version" "7.0.0" +socks-proxy-agent@^7.0.0: + version "7.0.0" dependencies: - "agent-base" "^6.0.2" - "debug" "^4.3.3" - "socks" "^2.6.2" + agent-base "^6.0.2" + debug "^4.3.3" + socks "^2.6.2" -"socks@^2.6.2": - "version" "2.7.0" +socks@^2.6.2: + version "2.7.0" dependencies: - "ip" "^2.0.0" - "smart-buffer" "^4.2.0" + ip "^2.0.0" + smart-buffer "^4.2.0" -"source-map-js@^1.0.2": - "integrity" "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" - "resolved" "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz" - "version" "1.0.2" +source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== -"spdx-correct@^3.0.0": - "version" "3.1.1" +spdx-correct@^3.0.0: + version "3.1.1" dependencies: - "spdx-expression-parse" "^3.0.0" - "spdx-license-ids" "^3.0.0" + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" -"spdx-exceptions@^2.1.0": - "version" "2.3.0" +spdx-exceptions@^2.1.0: + version "2.3.0" -"spdx-expression-parse@^3.0.0": - "version" "3.0.1" +spdx-expression-parse@^3.0.0: + version "3.0.1" dependencies: - "spdx-exceptions" "^2.1.0" - "spdx-license-ids" "^3.0.0" + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" -"spdx-license-ids@^3.0.0": - "version" "3.0.11" +spdx-license-ids@^3.0.0: + version "3.0.11" -"srt-webvtt@^2.0.0": - "integrity" "sha512-G2Z7/Jf2NRKrmLYNSIhSYZZYE6OFlKXFp9Au2/zJBKgrioUzmrAys1x7GT01dwl6d2sEnqr5uahEIOd0JW/Rbw==" - "resolved" "https://registry.npmjs.org/srt-webvtt/-/srt-webvtt-2.0.0.tgz" - "version" "2.0.0" +srt-webvtt@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/srt-webvtt/-/srt-webvtt-2.0.0.tgz" + integrity sha512-G2Z7/Jf2NRKrmLYNSIhSYZZYE6OFlKXFp9Au2/zJBKgrioUzmrAys1x7GT01dwl6d2sEnqr5uahEIOd0JW/Rbw== -"ssri@^10.0.0", "ssri@^10.0.1": - "version" "10.0.1" +ssri@^10.0.0, ssri@^10.0.1: + version "10.0.1" dependencies: - "minipass" "^4.0.0" + minipass "^4.0.0" -"ssri@^9.0.0": - "version" "9.0.1" +ssri@^9.0.0: + version "9.0.1" dependencies: - "minipass" "^3.1.1" + minipass "^3.1.1" -"string_decoder@^1.1.1": - "integrity" "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==" - "resolved" "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" - "version" "1.3.0" - dependencies: - "safe-buffer" "~5.2.0" - -"string-width@^1.0.2 || 2 || 3 || 4", "string-width@^4.2.0", "string-width@^4.2.3": - "version" "4.2.3" - dependencies: - "emoji-regex" "^8.0.0" - "is-fullwidth-code-point" "^3.0.0" - "strip-ansi" "^6.0.1" - -"string.prototype.matchall@^4.0.6": - "integrity" "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==" - "resolved" "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz" - "version" "4.0.8" - dependencies: - "call-bind" "^1.0.2" - "define-properties" "^1.1.4" - "es-abstract" "^1.20.4" - "get-intrinsic" "^1.1.3" - "has-symbols" "^1.0.3" - "internal-slot" "^1.0.3" - "regexp.prototype.flags" "^1.4.3" - "side-channel" "^1.0.4" - -"string.prototype.trimend@^1.0.6": - "integrity" "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==" - "resolved" "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz" - "version" "1.0.6" - dependencies: - "call-bind" "^1.0.2" - "define-properties" "^1.1.4" - "es-abstract" "^1.20.4" - -"string.prototype.trimstart@^1.0.6": - "integrity" "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==" - "resolved" "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz" - "version" "1.0.6" - dependencies: - "call-bind" "^1.0.2" - "define-properties" "^1.1.4" - "es-abstract" "^1.20.4" - -"strip-ansi@^6.0.1": - "integrity" "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==" - "resolved" "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - "version" "6.0.1" - dependencies: - "ansi-regex" "^5.0.1" - -"strip-bom@^3.0.0": - "integrity" "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==" - "resolved" "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" - "version" "3.0.0" - -"strip-json-comments@^3.1.0", "strip-json-comments@^3.1.1": - "integrity" "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" - "resolved" "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" - "version" "3.1.1" - -"subscribe-ui-event@^2.0.6": - "integrity" "sha512-Acrtf9XXl6lpyHAWYeRD1xTPUQHDERfL4GHeNuYAtZMc4Z8Us2iDBP0Fn3xiRvkQ1FO+hx+qRLmPEwiZxp7FDQ==" - "resolved" "https://registry.npmjs.org/subscribe-ui-event/-/subscribe-ui-event-2.0.7.tgz" - "version" "2.0.7" - dependencies: - "eventemitter3" "^3.0.0" - "lodash" "^4.17.15" - "raf" "^3.0.0" - -"supports-color@^7.1.0": - "integrity" "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==" - "resolved" "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" - "version" "7.2.0" - dependencies: - "has-flag" "^4.0.0" - -"supports-preserve-symlinks-flag@^1.0.0": - "integrity" "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" - "resolved" "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" - "version" "1.0.0" - -"tailwind-scrollbar@^2.0.1": - "integrity" "sha512-OcR7qHBbux4k+k6bWqnEQFYFooLK/F4dhkBz6nvswIoaA9ancZ5h20e0tyV7ifSWLDCUBtpG+1NHRA8HMRH/wg==" - "resolved" "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-2.0.1.tgz" - "version" "2.0.1" - -"tailwindcss@^3.2.4", "tailwindcss@>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1", "tailwindcss@3.x": - "integrity" "sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==" - "resolved" "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz" - "version" "3.2.4" - dependencies: - "arg" "^5.0.2" - "chokidar" "^3.5.3" - "color-name" "^1.1.4" - "detective" "^5.2.1" - "didyoumean" "^1.2.2" - "dlv" "^1.1.3" - "fast-glob" "^3.2.12" - "glob-parent" "^6.0.2" - "is-glob" "^4.0.3" - "lilconfig" "^2.0.6" - "micromatch" "^4.0.5" - "normalize-path" "^3.0.0" - "object-hash" "^3.0.0" - "picocolors" "^1.0.0" - "postcss" "^8.4.18" - "postcss-import" "^14.1.0" - "postcss-js" "^4.0.0" - "postcss-load-config" "^3.1.4" - "postcss-nested" "6.0.0" - "postcss-selector-parser" "^6.0.10" - "postcss-value-parser" "^4.2.0" - "quick-lru" "^5.1.1" - "resolve" "^1.22.1" - -"tar@^6.1.11", "tar@^6.1.13", "tar@^6.1.2": - "version" "6.1.13" - dependencies: - "chownr" "^2.0.0" - "fs-minipass" "^2.0.0" - "minipass" "^4.0.0" - "minizlib" "^2.1.1" - "mkdirp" "^1.0.3" - "yallist" "^4.0.0" - -"text-table@^0.2.0": - "integrity" "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" - "resolved" "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" - "version" "0.2.0" - -"text-table@~0.2.0": - "version" "0.2.0" - -"tiny-invariant@^1.0.2": - "integrity" "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==" - "resolved" "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz" - "version" "1.3.1" - -"tiny-relative-date@^1.3.0": - "version" "1.3.0" - -"tiny-warning@^1.0.0": - "integrity" "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" - "resolved" "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz" - "version" "1.0.3" - -"to-regex-range@^5.0.1": - "integrity" "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==" - "resolved" "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" - "version" "5.0.1" - dependencies: - "is-number" "^7.0.0" - -"treeverse@^3.0.0": - "version" "3.0.0" - -"tsconfig-paths@^3.14.1": - "integrity" "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==" - "resolved" "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz" - "version" "3.14.1" +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string.prototype.matchall@^4.0.6: + version "4.0.8" + resolved "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz" + integrity sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + get-intrinsic "^1.1.3" + has-symbols "^1.0.3" + internal-slot "^1.0.3" + regexp.prototype.flags "^1.4.3" + side-channel "^1.0.4" + +string.prototype.trimend@^1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz" + integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +string.prototype.trimstart@^1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz" + integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +subscribe-ui-event@^2.0.6: + version "2.0.7" + resolved "https://registry.npmjs.org/subscribe-ui-event/-/subscribe-ui-event-2.0.7.tgz" + integrity sha512-Acrtf9XXl6lpyHAWYeRD1xTPUQHDERfL4GHeNuYAtZMc4Z8Us2iDBP0Fn3xiRvkQ1FO+hx+qRLmPEwiZxp7FDQ== + dependencies: + eventemitter3 "^3.0.0" + lodash "^4.17.15" + raf "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tailwind-scrollbar@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-2.0.1.tgz" + integrity sha512-OcR7qHBbux4k+k6bWqnEQFYFooLK/F4dhkBz6nvswIoaA9ancZ5h20e0tyV7ifSWLDCUBtpG+1NHRA8HMRH/wg== + +tailwindcss@^3.2.4, "tailwindcss@>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1", tailwindcss@3.x: + version "3.2.4" + resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz" + integrity sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ== + dependencies: + arg "^5.0.2" + chokidar "^3.5.3" + color-name "^1.1.4" + detective "^5.2.1" + didyoumean "^1.2.2" + dlv "^1.1.3" + fast-glob "^3.2.12" + glob-parent "^6.0.2" + is-glob "^4.0.3" + lilconfig "^2.0.6" + micromatch "^4.0.5" + normalize-path "^3.0.0" + object-hash "^3.0.0" + picocolors "^1.0.0" + postcss "^8.4.18" + postcss-import "^14.1.0" + postcss-js "^4.0.0" + postcss-load-config "^3.1.4" + postcss-nested "6.0.0" + postcss-selector-parser "^6.0.10" + postcss-value-parser "^4.2.0" + quick-lru "^5.1.1" + resolve "^1.22.1" + +tar@^6.1.11, tar@^6.1.13, tar@^6.1.2: + version "6.1.13" + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^4.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +text-table@~0.2.0: + version "0.2.0" + +tiny-invariant@^1.0.2: + version "1.3.1" + resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz" + integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== + +tiny-relative-date@^1.3.0: + version "1.3.0" + +tiny-warning@^1.0.0: + version "1.0.3" + resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +treeverse@^3.0.0: + version "3.0.0" + +tsconfig-paths@^3.14.1: + version "3.14.1" + resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz" + integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== dependencies: "@types/json5" "^0.0.29" - "json5" "^1.0.1" - "minimist" "^1.2.6" - "strip-bom" "^3.0.0" + json5 "^1.0.1" + minimist "^1.2.6" + strip-bom "^3.0.0" -"tslib@^1.8.1": - "integrity" "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - "resolved" "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" - "version" "1.14.1" +tslib@^1.8.1: + version "1.14.1" + resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -"tsutils@^3.21.0": - "integrity" "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==" - "resolved" "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" - "version" "3.21.0" +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== dependencies: - "tslib" "^1.8.1" + tslib "^1.8.1" -"type-check@^0.4.0", "type-check@~0.4.0": - "integrity" "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==" - "resolved" "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" - "version" "0.4.0" +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== dependencies: - "prelude-ls" "^1.2.1" + prelude-ls "^1.2.1" -"type-fest@^0.20.2": - "integrity" "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" - "resolved" "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" - "version" "0.20.2" +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -"typescript@^4.6.4", "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta": - "integrity" "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==" - "resolved" "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz" - "version" "4.9.4" +typescript@^4.6.4, "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta": + version "4.9.4" + resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz" + integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== -"ufo@^1.0.0": - "integrity" "sha512-boAm74ubXHY7KJQZLlXrtMz52qFvpsbOxDcZOnw/Wf+LS4Mmyu7JxmzD4tDLtUQtmZECypJ0FrCz4QIe6dvKRA==" - "resolved" "https://registry.npmjs.org/ufo/-/ufo-1.0.1.tgz" - "version" "1.0.1" +ufo@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/ufo/-/ufo-1.0.1.tgz" + integrity sha512-boAm74ubXHY7KJQZLlXrtMz52qFvpsbOxDcZOnw/Wf+LS4Mmyu7JxmzD4tDLtUQtmZECypJ0FrCz4QIe6dvKRA== -"unbox-primitive@^1.0.2": - "integrity" "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==" - "resolved" "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz" - "version" "1.0.2" +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== dependencies: - "call-bind" "^1.0.2" - "has-bigints" "^1.0.2" - "has-symbols" "^1.0.3" - "which-boxed-primitive" "^1.0.2" + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" -"unique-filename@^2.0.0": - "version" "2.0.1" +unique-filename@^2.0.0: + version "2.0.1" dependencies: - "unique-slug" "^3.0.0" + unique-slug "^3.0.0" -"unique-filename@^3.0.0": - "version" "3.0.0" +unique-filename@^3.0.0: + version "3.0.0" dependencies: - "unique-slug" "^4.0.0" + unique-slug "^4.0.0" -"unique-slug@^3.0.0": - "version" "3.0.0" +unique-slug@^3.0.0: + version "3.0.0" dependencies: - "imurmurhash" "^0.1.4" + imurmurhash "^0.1.4" -"unique-slug@^4.0.0": - "version" "4.0.0" +unique-slug@^4.0.0: + version "4.0.0" dependencies: - "imurmurhash" "^0.1.4" + imurmurhash "^0.1.4" -"unpacker@^1.0.1": - "integrity" "sha512-0HTljwp8+JBdITpoHcK1LWi7X9U2BspUmWv78UWZh7NshYhbh1nec8baY/iSbe2OQTZ2bhAtVdnr6/BTD0DKVg==" - "resolved" "https://registry.npmjs.org/unpacker/-/unpacker-1.0.1.tgz" - "version" "1.0.1" +unpacker@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/unpacker/-/unpacker-1.0.1.tgz" + integrity sha512-0HTljwp8+JBdITpoHcK1LWi7X9U2BspUmWv78UWZh7NshYhbh1nec8baY/iSbe2OQTZ2bhAtVdnr6/BTD0DKVg== -"update-browserslist-db@^1.0.9": - "integrity" "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==" - "resolved" "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz" - "version" "1.0.10" +update-browserslist-db@^1.0.9: + version "1.0.10" + resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz" + integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== dependencies: - "escalade" "^3.1.1" - "picocolors" "^1.0.0" + escalade "^3.1.1" + picocolors "^1.0.0" -"uri-js@^4.2.2": - "integrity" "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==" - "resolved" "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" - "version" "4.4.1" +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== dependencies: - "punycode" "^2.1.0" + punycode "^2.1.0" -"util-deprecate@^1.0.1", "util-deprecate@^1.0.2": - "integrity" "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - "resolved" "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - "version" "1.0.2" +util-deprecate@^1.0.1, util-deprecate@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -"validate-npm-package-license@^3.0.4": - "version" "3.0.4" +validate-npm-package-license@^3.0.4: + version "3.0.4" dependencies: - "spdx-correct" "^3.0.0" - "spdx-expression-parse" "^3.0.0" + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" -"validate-npm-package-name@^5.0.0": - "version" "5.0.0" +validate-npm-package-name@^5.0.0: + version "5.0.0" dependencies: - "builtins" "^5.0.0" + builtins "^5.0.0" -"value-equal@^1.0.1": - "integrity" "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" - "resolved" "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz" - "version" "1.0.1" +value-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz" + integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== -"vite-plugin-package-version@^1.0.2": - "integrity" "sha512-xCJMR0KD4rqSUwINyHJlLizio2VzYzaMrRkqC9xWaVGXgw1lIrzdD+wBUf1XDM8EhL1JoQ7aykLOfKrlZd1SoQ==" - "resolved" "https://registry.npmjs.org/vite-plugin-package-version/-/vite-plugin-package-version-1.0.2.tgz" - "version" "1.0.2" +vite-plugin-package-version@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/vite-plugin-package-version/-/vite-plugin-package-version-1.0.2.tgz" + integrity sha512-xCJMR0KD4rqSUwINyHJlLizio2VzYzaMrRkqC9xWaVGXgw1lIrzdD+wBUf1XDM8EhL1JoQ7aykLOfKrlZd1SoQ== -"vite@^4.0.0", "vite@^4.0.1", "vite@>=2.0.0-beta.69": - "integrity" "sha512-kZQPzbDau35iWOhy3CpkrRC7It+HIHtulAzBhMqzGHKRf/4+vmh8rPDDdv98SWQrFWo6//3ozwsRmwQIPZsK9g==" - "resolved" "https://registry.npmjs.org/vite/-/vite-4.0.1.tgz" - "version" "4.0.1" +vite@^4.0.0, vite@^4.0.1, vite@>=2.0.0-beta.69: + version "4.0.1" + resolved "https://registry.npmjs.org/vite/-/vite-4.0.1.tgz" + integrity sha512-kZQPzbDau35iWOhy3CpkrRC7It+HIHtulAzBhMqzGHKRf/4+vmh8rPDDdv98SWQrFWo6//3ozwsRmwQIPZsK9g== dependencies: - "esbuild" "^0.16.3" - "postcss" "^8.4.20" - "resolve" "^1.22.1" - "rollup" "^3.7.0" + esbuild "^0.16.3" + postcss "^8.4.20" + resolve "^1.22.1" + rollup "^3.7.0" optionalDependencies: - "fsevents" "~2.3.2" + fsevents "~2.3.2" -"void-elements@3.1.0": - "integrity" "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==" - "resolved" "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz" - "version" "3.1.0" +void-elements@3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz" + integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== -"walk-up-path@^1.0.0": - "version" "1.0.0" +walk-up-path@^1.0.0: + version "1.0.0" -"wcwidth@^1.0.0": - "version" "1.0.1" +wcwidth@^1.0.0: + version "1.0.1" dependencies: - "defaults" "^1.0.3" + defaults "^1.0.3" -"which-boxed-primitive@^1.0.2": - "integrity" "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==" - "resolved" "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz" - "version" "1.0.2" +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== dependencies: - "is-bigint" "^1.0.1" - "is-boolean-object" "^1.1.0" - "is-number-object" "^1.0.4" - "is-string" "^1.0.5" - "is-symbol" "^1.0.3" + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" -"which@^2.0.1": - "integrity" "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==" - "resolved" "https://registry.npmjs.org/which/-/which-2.0.2.tgz" - "version" "2.0.2" +which@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: - "isexe" "^2.0.0" + isexe "^2.0.0" -"which@^2.0.2": - "version" "2.0.2" +which@^2.0.2: + version "2.0.2" dependencies: - "isexe" "^2.0.0" + isexe "^2.0.0" -"which@^3.0.0": - "version" "3.0.0" +which@^3.0.0: + version "3.0.0" dependencies: - "isexe" "^2.0.0" + isexe "^2.0.0" -"wide-align@^1.1.5": - "version" "1.1.5" +wide-align@^1.1.5: + version "1.1.5" dependencies: - "string-width" "^1.0.2 || 2 || 3 || 4" + string-width "^1.0.2 || 2 || 3 || 4" -"word-wrap@^1.2.3": - "integrity" "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" - "resolved" "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" - "version" "1.2.3" +word-wrap@^1.2.3: + version "1.2.3" + resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -"wrappy@1": - "integrity" "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - "resolved" "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - "version" "1.0.2" +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -"write-file-atomic@^5.0.0": - "version" "5.0.0" +write-file-atomic@^5.0.0: + version "5.0.0" dependencies: - "imurmurhash" "^0.1.4" - "signal-exit" "^3.0.7" + imurmurhash "^0.1.4" + signal-exit "^3.0.7" -"xtend@^4.0.2": - "integrity" "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" - "resolved" "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz" - "version" "4.0.2" +xtend@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== -"yallist@^4.0.0": - "integrity" "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - "resolved" "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" - "version" "4.0.0" +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -"yaml@^1.10.2": - "integrity" "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" - "resolved" "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" - "version" "1.10.2" +yaml@^1.10.2: + version "1.10.2" + resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== -"yocto-queue@^0.1.0": - "integrity" "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" - "resolved" "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" - "version" "0.1.0" +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== From b43f39b0076896153035729f931d9771b05f0964 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 18 Feb 2023 22:41:50 +0100 Subject: [PATCH 121/135] domain migrations Co-authored-by: Jip Frijlink --- package.json | 2 + src/index.tsx | 5 - src/setup/App.tsx | 2 + src/state/watched/migrations/v2.ts | 6 +- src/views/other/v2Migration.tsx | 61 ++ yarn.lock | 1173 +--------------------------- 6 files changed, 83 insertions(+), 1166 deletions(-) create mode 100644 src/views/other/v2Migration.tsx diff --git a/package.json b/package.json index 2b912e81..6663ba27 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "lodash.throttle": "^4.1.1", "nanoid": "^4.0.0", "ofetch": "^1.0.0", + "pako": "^2.1.0", "react": "^17.0.2", "react-dom": "^17.0.2", "react-helmet": "^6.1.0", @@ -54,6 +55,7 @@ "@types/fscreen": "^1.0.1", "@types/lodash.throttle": "^4.1.7", "@types/node": "^17.0.15", + "@types/pako": "^2.0.0", "@types/react": "^17.0.39", "@types/react-dom": "^17.0.11", "@types/react-router": "^5.1.18", diff --git a/src/index.tsx b/src/index.tsx index 97f8db56..a6bbd66c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -22,8 +22,6 @@ initializeChromecast(); // TODO video todos: // - chrome cast support // - bug: safari fullscreen will make video overlap player controls -// - improvement: make scrapers use fuzzy matching on normalized titles -// - bug: .ass subtitle files are fucked // TODO stuff to test: // - browser: firefox, chrome, edge, safari desktop @@ -37,9 +35,6 @@ initializeChromecast(); // - implement jons providers/embedscrapers // - AFTER all that: rank providers/embedscrapers -// TODO general todos: -// - localize everything (fix loading screen text (series vs movies)) - const LazyLoadedApp = React.lazy(async () => { await initializeStores(); return { diff --git a/src/setup/App.tsx b/src/setup/App.tsx index 6ab1374c..50c89f97 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -6,12 +6,14 @@ import { NotFoundPage } from "@/views/notfound/NotFoundView"; import { MediaView } from "@/views/media/MediaView"; import { SearchView } from "@/views/search/SearchView"; import { MWMediaType } from "@/backend/metadata/types"; +import { V2MigrationView } from "@/views/other/v2Migration"; function App() { return ( + diff --git a/src/state/watched/migrations/v2.ts b/src/state/watched/migrations/v2.ts index 5b2ee2f5..1d145968 100644 --- a/src/state/watched/migrations/v2.ts +++ b/src/state/watched/migrations/v2.ts @@ -148,6 +148,8 @@ export async function migrateV2Videos(old: OldData) { items: [], }; + const now = Date.now(); + for (const oldWatched of oldData.items) { if (oldWatched.mediaType === "movie") { if (!mediaMetas[oldWatched.mediaId]["0"]?.meta) continue; @@ -186,8 +188,8 @@ export async function migrateV2Videos(old: OldData) { }, progress: oldWatched.progress, percentage: oldWatched.percentage, - watchedAt: Date.now(), // There was no watchedAt in V2 - // Put watchedAt in the future to show last episode as most recently + watchedAt: now + Number(oldWatched.seasonId) * 1000 + Number(oldWatched.episodeId), // There was no watchedAt in V2 + // JANK ALERT: Put watchedAt in the future to show last episode as most recently }; if ( diff --git a/src/views/other/v2Migration.tsx b/src/views/other/v2Migration.tsx new file mode 100644 index 00000000..f97c27f7 --- /dev/null +++ b/src/views/other/v2Migration.tsx @@ -0,0 +1,61 @@ +import { useEffect, useState } from "react"; +import pako from "pako"; + +function fromBinary(str: string): Uint8Array { + let result = new Uint8Array(str.length); + [...str].forEach((char, i) => { + result[i] = char.charCodeAt(0); + }); + return result; +} + + +export function V2MigrationView() { + const [done, setDone] = useState(false); + useEffect(() => { + const params = new URLSearchParams(window.location.search ?? ""); + if (!params.has("m-time") || !params.has("m-data")) { + // migration params missing, just redirect + setDone(true); + return; + } + + const data = JSON.parse(pako.inflate(fromBinary(atob(params.get("m-data") as string)), { to: "string" })); + const timeOfMigration = new Date(params.get("m-time") as string); + + const savedTime = localStorage.getItem("mw-migration-date"); + if (savedTime) { + if (new Date(savedTime) >= timeOfMigration) { + // has already migrated this or something newer, skip + setDone(true); + return; + } + } + + // restore migration data + if (data.bookmarks) + localStorage.setItem("mw-bookmarks", JSON.stringify(data.bookmarks)) + if (data.videoProgress) + localStorage.setItem("video-progress", JSON.stringify(data.videoProgress)) + localStorage.setItem("mw-migration-date", timeOfMigration.toISOString()) + + // finished + setDone(true); + }, []) + + // redirect when done + useEffect(() => { + if (!done) return; + const newUrl = new URL(window.location.href); + + const newParams = [] as string[]; + newUrl.searchParams.forEach((_, key)=>newParams.push(key)); + newParams.forEach(v => newUrl.searchParams.delete(v)) + + newUrl.hash = ""; + + window.location.href = newUrl.toString(); + }, [done]) + + return null; +} diff --git a/yarn.lock b/yarn.lock index a4ef64b4..976dbed1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17,9 +17,6 @@ dependencies: regenerator-runtime "^0.13.11" -"@colors/colors@1.5.0": - version "1.5.0" - "@esbuild/linux-x64@0.16.5": version "0.16.5" resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.5.tgz" @@ -45,9 +42,6 @@ resolved "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-1.0.0-beta.5.tgz" integrity sha512-WoSwyhAZPOe6RB/IgicOtCHtrWwEpfKIZ/H/nxpKfnZL9CB6hhhBGU5bCdMRw7YpAUF2CDlQa+WWh+gCqz5lDg== -"@gar/promisify@^1.1.3": - version "1.1.3" - "@headlessui/react@^1.5.0": version "1.7.5" resolved "https://registry.npmjs.org/@headlessui/react/-/react-1.7.5.tgz" @@ -74,9 +68,6 @@ resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== -"@isaacs/string-locale-compare@^1.1.0": - version "1.1.0" - "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" @@ -98,141 +89,6 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@npmcli/arborist@^6.1.5": - version "6.1.5" - dependencies: - "@isaacs/string-locale-compare" "^1.1.0" - "@npmcli/fs" "^3.1.0" - "@npmcli/installed-package-contents" "^2.0.0" - "@npmcli/map-workspaces" "^3.0.0" - "@npmcli/metavuln-calculator" "^5.0.0" - "@npmcli/name-from-folder" "^1.0.1" - "@npmcli/node-gyp" "^3.0.0" - "@npmcli/package-json" "^3.0.0" - "@npmcli/query" "^3.0.0" - "@npmcli/run-script" "^6.0.0" - bin-links "^4.0.1" - cacache "^17.0.3" - common-ancestor-path "^1.0.1" - hosted-git-info "^6.1.1" - json-parse-even-better-errors "^3.0.0" - json-stringify-nice "^1.1.4" - minimatch "^5.1.1" - nopt "^7.0.0" - npm-install-checks "^6.0.0" - npm-package-arg "^10.1.0" - npm-pick-manifest "^8.0.1" - npm-registry-fetch "^14.0.3" - npmlog "^7.0.1" - pacote "^15.0.7" - parse-conflict-json "^3.0.0" - proc-log "^3.0.0" - promise-all-reject-late "^1.0.0" - promise-call-limit "^1.0.1" - read-package-json-fast "^3.0.1" - semver "^7.3.7" - ssri "^10.0.1" - treeverse "^3.0.0" - walk-up-path "^1.0.0" - -"@npmcli/config@^6.1.0": - version "6.1.0" - dependencies: - "@npmcli/map-workspaces" "^3.0.0" - ini "^3.0.0" - nopt "^7.0.0" - proc-log "^3.0.0" - read-package-json-fast "^3.0.0" - semver "^7.3.5" - walk-up-path "^1.0.0" - -"@npmcli/disparity-colors@^3.0.0": - version "3.0.0" - dependencies: - ansi-styles "^4.3.0" - -"@npmcli/fs@^2.1.0": - version "2.1.2" - dependencies: - "@gar/promisify" "^1.1.3" - semver "^7.3.5" - -"@npmcli/fs@^3.1.0": - version "3.1.0" - dependencies: - semver "^7.3.5" - -"@npmcli/git@^4.0.0", "@npmcli/git@^4.0.1": - version "4.0.3" - dependencies: - "@npmcli/promise-spawn" "^6.0.0" - lru-cache "^7.4.4" - mkdirp "^1.0.4" - npm-pick-manifest "^8.0.0" - proc-log "^3.0.0" - promise-inflight "^1.0.1" - promise-retry "^2.0.1" - semver "^7.3.5" - which "^3.0.0" - -"@npmcli/installed-package-contents@^2.0.0", "@npmcli/installed-package-contents@^2.0.1": - version "2.0.1" - dependencies: - npm-bundled "^3.0.0" - npm-normalize-package-bin "^3.0.0" - -"@npmcli/map-workspaces@^3.0.0": - version "3.0.0" - dependencies: - "@npmcli/name-from-folder" "^1.0.1" - glob "^8.0.1" - minimatch "^5.0.1" - read-package-json-fast "^3.0.0" - -"@npmcli/metavuln-calculator@^5.0.0": - version "5.0.0" - dependencies: - cacache "^17.0.0" - json-parse-even-better-errors "^3.0.0" - pacote "^15.0.0" - semver "^7.3.5" - -"@npmcli/move-file@^2.0.0": - version "2.0.1" - dependencies: - mkdirp "^1.0.4" - rimraf "^3.0.2" - -"@npmcli/name-from-folder@^1.0.1": - version "1.0.1" - -"@npmcli/node-gyp@^3.0.0": - version "3.0.0" - -"@npmcli/package-json@^3.0.0": - version "3.0.0" - dependencies: - json-parse-even-better-errors "^3.0.0" - -"@npmcli/promise-spawn@^6.0.0", "@npmcli/promise-spawn@^6.0.1": - version "6.0.1" - dependencies: - which "^3.0.0" - -"@npmcli/query@^3.0.0": - version "3.0.0" - dependencies: - postcss-selector-parser "^6.0.10" - -"@npmcli/run-script@^6.0.0": - version "6.0.0" - dependencies: - "@npmcli/node-gyp" "^3.0.0" - "@npmcli/promise-spawn" "^6.0.0" - node-gyp "^9.0.0" - read-package-json-fast "^3.0.0" - which "^3.0.0" - "@swc/core-linux-x64-gnu@1.3.22": version "1.3.22" resolved "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.22.tgz" @@ -264,9 +120,6 @@ resolved "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.2.tgz" integrity sha512-HFzAQuqYCjyy/SX9sLGB1lroPzmcnWv1FHkIpmypte10hptf4oPUfucryMKovZh2u0uiS9U5Ty3GghWfEJGwVw== -"@tootallnate/once@2": - version "2.0.0" - "@types/chrome@*": version "0.0.210" resolved "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.210.tgz" @@ -341,6 +194,11 @@ resolved "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz" integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== +"@types/pako@^2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@types/pako/-/pako-2.0.0.tgz" + integrity sha512-10+iaz93qR5WYxTo+PMifD5TSxiOtdRaxBf7INGGXMQgTCu8Z/7GYWYFUOS3q/G0nE5boj1r4FEB+WSy7s5gbA== + "@types/prop-types@*": version "15.7.5" resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz" @@ -500,17 +358,6 @@ dependencies: "@swc/core" "^1.3.21" -abbrev@^1.0.0: - version "1.1.1" - -abbrev@^2.0.0: - version "2.0.0" - -abort-controller@^3.0.0: - version "3.0.0" - dependencies: - event-target-shim "^5.0.0" - acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" @@ -540,24 +387,6 @@ acorn@^7.0.0: resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -agent-base@^6.0.2, agent-base@6: - version "6.0.2" - dependencies: - debug "4" - -agentkeepalive@^4.2.1: - version "4.2.1" - dependencies: - debug "^4.1.0" - depd "^1.1.2" - humanize-ms "^1.2.1" - -aggregate-error@^3.0.0: - version "3.1.0" - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - ajv@^6.10.0, ajv@^6.12.4: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" @@ -573,7 +402,7 @@ ansi-regex@^5.0.1: resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-styles@^4.1.0, ansi-styles@^4.3.0: +ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== @@ -588,24 +417,6 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" -"aproba@^1.0.3 || ^2.0.0", aproba@^2.0.0: - version "2.0.0" - -archy@~1.0.0: - version "1.0.0" - -are-we-there-yet@^3.0.0: - version "3.0.1" - dependencies: - delegates "^1.0.0" - readable-stream "^3.6.0" - -are-we-there-yet@^4.0.0: - version "4.0.0" - dependencies: - delegates "^1.0.0" - readable-stream "^4.1.0" - arg@^5.0.2: version "5.0.2" resolved "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz" @@ -692,25 +503,11 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.3.1: - version "1.5.1" - -bin-links@^4.0.1: - version "4.0.1" - dependencies: - cmd-shim "^6.0.0" - npm-normalize-package-bin "^3.0.0" - read-cmd-shim "^4.0.0" - write-file-atomic "^5.0.0" - binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -binary-extensions@^2.2.0: - version "2.2.0" - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" @@ -719,11 +516,6 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -brace-expansion@^2.0.1: - version "2.0.1" - dependencies: - balanced-match "^1.0.0" - braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" @@ -741,56 +533,6 @@ browserslist@^4.21.4, "browserslist@>= 4.21.0": node-releases "^2.0.6" update-browserslist-db "^1.0.9" -buffer@^6.0.3: - version "6.0.3" - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - -builtins@^5.0.0: - version "5.0.1" - dependencies: - semver "^7.0.0" - -cacache@^16.1.0: - version "16.1.3" - dependencies: - "@npmcli/fs" "^2.1.0" - "@npmcli/move-file" "^2.0.0" - chownr "^2.0.0" - fs-minipass "^2.1.0" - glob "^8.0.1" - infer-owner "^1.0.4" - lru-cache "^7.7.1" - minipass "^3.1.6" - minipass-collect "^1.0.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - mkdirp "^1.0.4" - p-map "^4.0.0" - promise-inflight "^1.0.1" - rimraf "^3.0.2" - ssri "^9.0.0" - tar "^6.1.11" - unique-filename "^2.0.0" - -cacache@^17.0.0, cacache@^17.0.3: - version "17.0.3" - dependencies: - "@npmcli/fs" "^3.1.0" - fs-minipass "^2.1.0" - glob "^8.0.1" - lru-cache "^7.7.1" - minipass "^4.0.0" - minipass-collect "^1.0.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - p-map "^4.0.0" - promise-inflight "^1.0.1" - ssri "^10.0.0" - tar "^6.1.11" - unique-filename "^3.0.0" - call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" @@ -814,7 +556,7 @@ caniuse-lite@^1.0.30001400, caniuse-lite@^1.0.30001426: resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001439.tgz" integrity sha512-1MgUzEkoMO6gKfXflStpYgZDlFM7M/ck/bgfVCACO5vnAf0fXoNVHdWtqGU+MYca+4bL9Z5bpOVmR33cWW9G2A== -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: +chalk@^4.0.0: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -837,49 +579,16 @@ chokidar@^3.5.3: optionalDependencies: fsevents "~2.3.2" -chownr@^2.0.0: - version "2.0.0" - -ci-info@^3.7.0: - version "3.7.0" - -cidr-regex@^3.1.1: - version "3.1.1" - dependencies: - ip-regex "^4.1.0" - classnames@^2.0.0: version "2.3.2" resolved "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz" integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== -clean-stack@^2.0.0: - version "2.2.0" - -cli-columns@^4.0.0: - version "4.0.0" - dependencies: - string-width "^4.2.3" - strip-ansi "^6.0.1" - -cli-table3@^0.6.3: - version "0.6.3" - dependencies: - string-width "^4.2.0" - optionalDependencies: - "@colors/colors" "1.5.0" - client-only@^0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== -clone@^1.0.2: - version "1.0.4" - -cmd-shim@^6.0.0: - version "6.0.0" - color-convert@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" @@ -892,18 +601,6 @@ color-name@^1.1.4, color-name@~1.1.4: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-support@^1.1.3: - version "1.1.3" - -columnify@^1.6.0: - version "1.6.0" - dependencies: - strip-ansi "^6.0.1" - wcwidth "^1.0.0" - -common-ancestor-path@^1.0.1: - version "1.0.1" - concat-map@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" @@ -914,9 +611,6 @@ confusing-browser-globals@^1.0.10: resolved "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz" integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA== -console-control-strings@^1.1.0: - version "1.1.0" - core-js-pure@^3.25.1: version "3.26.1" resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.26.1.tgz" @@ -970,11 +664,6 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.1.0, debug@^4.3.3, debug@4: - version "4.3.4" - dependencies: - ms "2.1.2" - debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" @@ -987,11 +676,6 @@ deep-is@^0.1.3: resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== -defaults@^1.0.3: - version "1.0.3" - dependencies: - clone "^1.0.2" - define-properties@^1.1.3, define-properties@^1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz" @@ -1005,12 +689,6 @@ defined@^1.0.0: resolved "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz" integrity sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q== -delegates@^1.0.0: - version "1.0.0" - -depd@^1.1.2: - version "1.1.2" - destr@^1.2.1: version "1.2.2" resolved "https://registry.npmjs.org/destr/-/destr-1.2.2.tgz" @@ -1030,9 +708,6 @@ didyoumean@^1.2.2: resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz" integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== -diff@^5.1.0: - version "5.1.0" - dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" @@ -1072,25 +747,11 @@ electron-to-chromium@^1.4.251: resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz" integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA== -emoji-regex@^8.0.0: - version "8.0.0" - emoji-regex@^9.2.2: version "9.2.2" resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== -encoding@^0.1.13: - version "0.1.13" - dependencies: - iconv-lite "^0.6.2" - -env-paths@^2.2.0: - version "2.2.1" - -err-code@^2.0.2: - version "2.0.3" - es-abstract@^1.19.0, es-abstract@^1.20.4: version "1.20.5" resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.5.tgz" @@ -1412,17 +1073,11 @@ esutils@^2.0.2: resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -event-target-shim@^5.0.0: - version "5.0.1" - eventemitter3@^3.0.0: version "3.1.2" resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz" integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== -events@^3.3.0: - version "3.3.0" - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" @@ -1454,9 +1109,6 @@ fast-levenshtein@^2.0.6: resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -fastest-levenshtein@^1.0.16: - version "1.0.16" - fastq@^1.6.0: version "1.14.0" resolved "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz" @@ -1504,11 +1156,6 @@ fraction.js@^4.2.0: resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz" integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== -fs-minipass@^2.0.0, fs-minipass@^2.1.0: - version "2.1.0" - dependencies: - minipass "^3.0.0" - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" @@ -1544,30 +1191,6 @@ fuse.js@^6.4.6: resolved "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz" integrity sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA== -gauge@^4.0.3: - version "4.0.4" - dependencies: - aproba "^1.0.3 || ^2.0.0" - color-support "^1.1.3" - console-control-strings "^1.1.0" - has-unicode "^2.0.1" - signal-exit "^3.0.7" - string-width "^4.2.3" - strip-ansi "^6.0.1" - wide-align "^1.1.5" - -gauge@^5.0.0: - version "5.0.0" - dependencies: - aproba "^1.0.3 || ^2.0.0" - color-support "^1.1.3" - console-control-strings "^1.1.0" - has-unicode "^2.0.1" - signal-exit "^3.0.7" - string-width "^4.2.3" - strip-ansi "^6.0.1" - wide-align "^1.1.5" - get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz" @@ -1618,25 +1241,6 @@ glob@^7.1.3, glob@^7.2.0: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.4: - version "7.2.3" - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^8.0.1: - version "8.0.3" - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - globals@^13.15.0: version "13.19.0" resolved "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz" @@ -1663,9 +1267,6 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.2.10, graceful-fs@^4.2.6: - version "4.2.10" - grapheme-splitter@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz" @@ -1700,9 +1301,6 @@ has-tostringtag@^1.0.0: dependencies: has-symbols "^1.0.2" -has-unicode@^2.0.1: - version "2.0.1" - has@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" @@ -1734,11 +1332,6 @@ hoist-non-react-statics@^3.1.0: dependencies: react-is "^16.7.0" -hosted-git-info@^6.0.0, hosted-git-info@^6.1.1: - version "6.1.1" - dependencies: - lru-cache "^7.5.1" - html-parse-stringify@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz" @@ -1746,32 +1339,6 @@ html-parse-stringify@^3.0.1: dependencies: void-elements "3.1.0" -http-cache-semantics@^4.1.0: - version "4.1.0" - -http-proxy-agent@^5.0.0: - version "5.0.0" - dependencies: - "@tootallnate/once" "2" - agent-base "6" - debug "4" - -https-proxy-agent@^5.0.0: - version "5.0.1" - dependencies: - agent-base "6" - debug "4" - -humanize-ms@^1.2.1: - version "1.2.1" - dependencies: - ms "^2.0.0" - -i@^0.3.7: - version "0.3.7" - resolved "https://registry.npmjs.org/i/-/i-0.3.7.tgz" - integrity sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q== - i18next-browser-languagedetector@^7.0.1: version "7.0.1" resolved "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.0.1.tgz" @@ -1786,21 +1353,6 @@ i18next@^22.4.5, "i18next@>= 19.0.0": dependencies: "@babel/runtime" "^7.20.6" -iconv-lite@^0.6.2: - version "0.6.3" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -ieee754@^1.2.1: - version "1.2.1" - -ignore-walk@^6.0.0: - version "6.0.0" - dependencies: - minimatch "^5.0.1" - ignore@^5.2.0: version "5.2.1" resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz" @@ -1819,12 +1371,6 @@ imurmurhash@^0.1.4: resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== -indent-string@^4.0.0: - version "4.0.0" - -infer-owner@^1.0.4: - version "1.0.4" - inflight@^1.0.4: version "1.0.6" resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" @@ -1833,25 +1379,11 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@^2.0.3, inherits@2: +inherits@2: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ini@^3.0.0, ini@^3.0.1: - version "3.0.1" - -init-package-json@^4.0.1: - version "4.0.1" - dependencies: - npm-package-arg "^10.0.0" - promzard "^0.3.0" - read "^1.0.7" - read-package-json "^6.0.0" - semver "^7.3.5" - validate-npm-package-license "^3.0.4" - validate-npm-package-name "^5.0.0" - internal-slot@^1.0.3: version "1.0.4" resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.4.tgz" @@ -1861,12 +1393,6 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" -ip-regex@^4.1.0: - version "4.3.0" - -ip@^2.0.0: - version "2.0.0" - is-bigint@^1.0.1: version "1.0.4" resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz" @@ -1894,11 +1420,6 @@ is-callable@^1.1.4, is-callable@^1.2.7: resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-cidr@^4.0.2: - version "4.0.2" - dependencies: - cidr-regex "^3.1.1" - is-core-module@^2.8.1, is-core-module@^2.9.0: version "2.11.0" resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz" @@ -1918,9 +1439,6 @@ is-extglob@^2.1.1: resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" @@ -1928,9 +1446,6 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" -is-lambda@^1.0.1: - version "1.0.1" - is-negative-zero@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz" @@ -2016,9 +1531,6 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -json-parse-even-better-errors@^3.0.0: - version "3.0.0" - json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" @@ -2029,9 +1541,6 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== -json-stringify-nice@^1.1.4: - version "1.1.4" - json5@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz" @@ -2044,9 +1553,6 @@ json5@^2.2.0: resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jsonparse@^1.3.1: - version "1.3.1" - "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.2: version "3.3.3" resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz" @@ -2055,12 +1561,6 @@ jsonparse@^1.3.1: array-includes "^3.1.5" object.assign "^4.1.3" -just-diff-apply@^5.2.0: - version "5.4.1" - -just-diff@^5.0.1: - version "5.1.1" - language-subtag-registry@^0.3.20: version "0.3.22" resolved "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz" @@ -2081,95 +1581,6 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -libnpmaccess@^7.0.1: - version "7.0.1" - dependencies: - npm-package-arg "^10.1.0" - npm-registry-fetch "^14.0.3" - -libnpmdiff@^5.0.6: - version "5.0.6" - dependencies: - "@npmcli/arborist" "^6.1.5" - "@npmcli/disparity-colors" "^3.0.0" - "@npmcli/installed-package-contents" "^2.0.0" - binary-extensions "^2.2.0" - diff "^5.1.0" - minimatch "^5.1.1" - npm-package-arg "^10.1.0" - pacote "^15.0.7" - tar "^6.1.13" - -libnpmexec@^5.0.6: - version "5.0.6" - dependencies: - "@npmcli/arborist" "^6.1.5" - "@npmcli/run-script" "^6.0.0" - chalk "^4.1.0" - ci-info "^3.7.0" - npm-package-arg "^10.1.0" - npmlog "^7.0.1" - pacote "^15.0.7" - proc-log "^3.0.0" - read "^1.0.7" - read-package-json-fast "^3.0.1" - semver "^7.3.7" - walk-up-path "^1.0.0" - -libnpmfund@^4.0.6: - version "4.0.6" - dependencies: - "@npmcli/arborist" "^6.1.5" - -libnpmhook@^9.0.1: - version "9.0.1" - dependencies: - aproba "^2.0.0" - npm-registry-fetch "^14.0.3" - -libnpmorg@^5.0.1: - version "5.0.1" - dependencies: - aproba "^2.0.0" - npm-registry-fetch "^14.0.3" - -libnpmpack@^5.0.6: - version "5.0.6" - dependencies: - "@npmcli/arborist" "^6.1.5" - "@npmcli/run-script" "^6.0.0" - npm-package-arg "^10.1.0" - pacote "^15.0.7" - -libnpmpublish@^7.0.6: - version "7.0.6" - dependencies: - normalize-package-data "^5.0.0" - npm-package-arg "^10.1.0" - npm-registry-fetch "^14.0.3" - semver "^7.3.7" - ssri "^10.0.1" - -libnpmsearch@^6.0.1: - version "6.0.1" - dependencies: - npm-registry-fetch "^14.0.3" - -libnpmteam@^5.0.1: - version "5.0.1" - dependencies: - aproba "^2.0.0" - npm-registry-fetch "^14.0.3" - -libnpmversion@^4.0.1: - version "4.0.1" - dependencies: - "@npmcli/git" "^4.0.1" - "@npmcli/run-script" "^6.0.0" - json-parse-even-better-errors "^3.0.0" - proc-log "^3.0.0" - semver "^7.3.7" - lilconfig@^2.0.5, lilconfig@^2.0.6: version "2.0.6" resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz" @@ -2211,49 +1622,6 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: - version "7.13.2" - -make-fetch-happen@^10.0.3: - version "10.2.1" - dependencies: - agentkeepalive "^4.2.1" - cacache "^16.1.0" - http-cache-semantics "^4.1.0" - http-proxy-agent "^5.0.0" - https-proxy-agent "^5.0.0" - is-lambda "^1.0.1" - lru-cache "^7.7.1" - minipass "^3.1.6" - minipass-collect "^1.0.2" - minipass-fetch "^2.0.3" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - negotiator "^0.6.3" - promise-retry "^2.0.1" - socks-proxy-agent "^7.0.0" - ssri "^9.0.0" - -make-fetch-happen@^11.0.0, make-fetch-happen@^11.0.2: - version "11.0.2" - dependencies: - agentkeepalive "^4.2.1" - cacache "^17.0.0" - http-cache-semantics "^4.1.0" - http-proxy-agent "^5.0.0" - https-proxy-agent "^5.0.0" - is-lambda "^1.0.1" - lru-cache "^7.7.1" - minipass "^4.0.0" - minipass-collect "^1.0.2" - minipass-fetch "^3.0.0" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - negotiator "^0.6.3" - promise-retry "^2.0.1" - socks-proxy-agent "^7.0.0" - ssri "^10.0.0" - merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" @@ -2274,87 +1642,11 @@ minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1, minimatch@^5.1.1: - version "5.1.1" - dependencies: - brace-expansion "^2.0.1" - minimist@^1.2.0, minimist@^1.2.6: version "1.2.7" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== -minipass-collect@^1.0.2: - version "1.0.2" - dependencies: - minipass "^3.0.0" - -minipass-fetch@^2.0.3: - version "2.1.2" - dependencies: - minipass "^3.1.6" - minipass-sized "^1.0.3" - minizlib "^2.1.2" - optionalDependencies: - encoding "^0.1.13" - -minipass-fetch@^3.0.0: - version "3.0.0" - dependencies: - minipass "^3.1.6" - minipass-sized "^1.0.3" - minizlib "^2.1.2" - optionalDependencies: - encoding "^0.1.13" - -minipass-flush@^1.0.5: - version "1.0.5" - dependencies: - minipass "^3.0.0" - -minipass-json-stream@^1.0.1: - version "1.0.1" - dependencies: - jsonparse "^1.3.1" - minipass "^3.0.0" - -minipass-pipeline@^1.2.4: - version "1.2.4" - dependencies: - minipass "^3.0.0" - -minipass-sized@^1.0.3: - version "1.0.3" - dependencies: - minipass "^3.0.0" - -minipass@^3.0.0: - version "3.3.6" - dependencies: - yallist "^4.0.0" - -minipass@^3.1.1, minipass@^3.1.6: - version "3.3.6" - dependencies: - yallist "^4.0.0" - -minipass@^4.0.0: - version "4.0.0" - dependencies: - yallist "^4.0.0" - -minizlib@^2.1.1, minizlib@^2.1.2: - version "2.1.2" - dependencies: - minipass "^3.0.0" - yallist "^4.0.0" - -mkdirp@^1.0.3, mkdirp@^1.0.4: - version "1.0.4" - -ms@^2.0.0, ms@^2.1.2: - version "2.1.3" - ms@^2.1.1, ms@2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" @@ -2365,9 +1657,6 @@ ms@2.0.0: resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== -mute-stream@~0.0.4: - version "0.0.8" - nanoid@^3.3.4: version "3.3.4" resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz" @@ -2388,51 +1677,16 @@ natural-compare@^1.4.0: resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -negotiator@^0.6.3: - version "0.6.3" - 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-gyp@^9.0.0, node-gyp@^9.3.0: - version "9.3.0" - dependencies: - env-paths "^2.2.0" - glob "^7.1.4" - graceful-fs "^4.2.6" - make-fetch-happen "^10.0.3" - nopt "^6.0.0" - npmlog "^6.0.0" - rimraf "^3.0.2" - semver "^7.3.5" - tar "^6.1.2" - which "^2.0.2" - node-releases@^2.0.6: version "2.0.7" resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.7.tgz" integrity sha512-EJ3rzxL9pTWPjk5arA0s0dgXpnyiAbJDE6wHT62g7VsgrgQgmmZ+Ru++M1BFofncWja+Pnn3rEr3fieRySAdKQ== -nopt@^6.0.0: - version "6.0.0" - dependencies: - abbrev "^1.0.0" - -nopt@^7.0.0: - version "7.0.0" - dependencies: - abbrev "^2.0.0" - -normalize-package-data@^5.0.0: - version "5.0.0" - dependencies: - hosted-git-info "^6.0.0" - is-core-module "^2.8.1" - semver "^7.3.5" - validate-npm-package-license "^3.0.4" - 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" @@ -2443,155 +1697,6 @@ normalize-range@^0.1.2: resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== -npm-audit-report@^4.0.0: - version "4.0.0" - dependencies: - chalk "^4.0.0" - -npm-bundled@^3.0.0: - version "3.0.0" - dependencies: - npm-normalize-package-bin "^3.0.0" - -npm-install-checks@^6.0.0: - version "6.0.0" - dependencies: - semver "^7.1.1" - -npm-normalize-package-bin@^3.0.0: - version "3.0.0" - -npm-package-arg@^10.0.0, npm-package-arg@^10.1.0: - version "10.1.0" - dependencies: - hosted-git-info "^6.0.0" - proc-log "^3.0.0" - semver "^7.3.5" - validate-npm-package-name "^5.0.0" - -npm-packlist@^7.0.0: - version "7.0.4" - dependencies: - ignore-walk "^6.0.0" - -npm-pick-manifest@^8.0.0, npm-pick-manifest@^8.0.1: - version "8.0.1" - dependencies: - npm-install-checks "^6.0.0" - npm-normalize-package-bin "^3.0.0" - npm-package-arg "^10.0.0" - semver "^7.3.5" - -npm-profile@^7.0.1: - version "7.0.1" - dependencies: - npm-registry-fetch "^14.0.0" - proc-log "^3.0.0" - -npm-registry-fetch@^14.0.0, npm-registry-fetch@^14.0.3: - version "14.0.3" - dependencies: - make-fetch-happen "^11.0.0" - minipass "^4.0.0" - minipass-fetch "^3.0.0" - minipass-json-stream "^1.0.1" - minizlib "^2.1.2" - npm-package-arg "^10.0.0" - proc-log "^3.0.0" - -npm-user-validate@^1.0.1: - version "1.0.1" - -npm@^9.2.0: - version "9.2.0" - resolved "https://registry.npmjs.org/npm/-/npm-9.2.0.tgz" - integrity sha512-oypVdaWGHDuV79RXLvp+B9gh6gDyAmoHKrQ0/JBYTWWx5D8/+AAxFdZC84fSIiyDdyW4qfrSyYGKhekxDOaMXQ== - dependencies: - "@isaacs/string-locale-compare" "^1.1.0" - "@npmcli/arborist" "^6.1.5" - "@npmcli/config" "^6.1.0" - "@npmcli/map-workspaces" "^3.0.0" - "@npmcli/package-json" "^3.0.0" - "@npmcli/run-script" "^6.0.0" - abbrev "^2.0.0" - archy "~1.0.0" - cacache "^17.0.3" - chalk "^4.1.2" - ci-info "^3.7.0" - cli-columns "^4.0.0" - cli-table3 "^0.6.3" - columnify "^1.6.0" - fastest-levenshtein "^1.0.16" - fs-minipass "^2.1.0" - glob "^8.0.1" - graceful-fs "^4.2.10" - hosted-git-info "^6.1.1" - ini "^3.0.1" - init-package-json "^4.0.1" - is-cidr "^4.0.2" - json-parse-even-better-errors "^3.0.0" - libnpmaccess "^7.0.1" - libnpmdiff "^5.0.6" - libnpmexec "^5.0.6" - libnpmfund "^4.0.6" - libnpmhook "^9.0.1" - libnpmorg "^5.0.1" - libnpmpack "^5.0.6" - libnpmpublish "^7.0.6" - libnpmsearch "^6.0.1" - libnpmteam "^5.0.1" - libnpmversion "^4.0.1" - make-fetch-happen "^11.0.2" - minimatch "^5.1.1" - minipass "^4.0.0" - minipass-pipeline "^1.2.4" - mkdirp "^1.0.4" - ms "^2.1.2" - node-gyp "^9.3.0" - nopt "^7.0.0" - npm-audit-report "^4.0.0" - npm-install-checks "^6.0.0" - npm-package-arg "^10.1.0" - npm-pick-manifest "^8.0.1" - npm-profile "^7.0.1" - npm-registry-fetch "^14.0.3" - npm-user-validate "^1.0.1" - npmlog "^7.0.1" - p-map "^4.0.0" - pacote "^15.0.7" - parse-conflict-json "^3.0.0" - proc-log "^3.0.0" - qrcode-terminal "^0.12.0" - read "~1.0.7" - read-package-json "^6.0.0" - read-package-json-fast "^3.0.1" - rimraf "^3.0.2" - semver "^7.3.8" - ssri "^10.0.1" - tar "^6.1.13" - text-table "~0.2.0" - tiny-relative-date "^1.3.0" - treeverse "^3.0.0" - validate-npm-package-name "^5.0.0" - which "^3.0.0" - write-file-atomic "^5.0.0" - -npmlog@^6.0.0: - version "6.0.2" - dependencies: - are-we-there-yet "^3.0.0" - console-control-strings "^1.1.0" - gauge "^4.0.3" - set-blocking "^2.0.0" - -npmlog@^7.0.1: - version "7.0.1" - dependencies: - are-we-there-yet "^4.0.0" - console-control-strings "^1.1.0" - gauge "^5.0.0" - set-blocking "^2.0.0" - object-assign@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" @@ -2699,31 +1804,10 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" -p-map@^4.0.0: - version "4.0.0" - dependencies: - aggregate-error "^3.0.0" - -pacote@^15.0.0, pacote@^15.0.7: - version "15.0.7" - dependencies: - "@npmcli/git" "^4.0.0" - "@npmcli/installed-package-contents" "^2.0.1" - "@npmcli/promise-spawn" "^6.0.1" - "@npmcli/run-script" "^6.0.0" - cacache "^17.0.0" - fs-minipass "^2.1.0" - minipass "^4.0.0" - npm-package-arg "^10.0.0" - npm-packlist "^7.0.0" - npm-pick-manifest "^8.0.0" - npm-registry-fetch "^14.0.0" - proc-log "^3.0.0" - promise-retry "^2.0.1" - read-package-json "^6.0.0" - read-package-json-fast "^3.0.0" - ssri "^10.0.0" - tar "^6.1.11" +pako@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz" + integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== parent-module@^1.0.0: version "1.0.1" @@ -2732,13 +1816,6 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-conflict-json@^3.0.0: - version "3.0.0" - dependencies: - json-parse-even-better-errors "^3.0.0" - just-diff "^5.0.1" - just-diff-apply "^5.2.0" - path-exists@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" @@ -2866,32 +1943,6 @@ prettier@^2.5.1, prettier@>=2.0.0, prettier@>=2.2.0: resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.1.tgz" integrity sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg== -proc-log@^3.0.0: - version "3.0.0" - -process@^0.11.10: - version "0.11.10" - -promise-all-reject-late@^1.0.0: - version "1.0.1" - -promise-call-limit@^1.0.1: - version "1.0.1" - -promise-inflight@^1.0.1: - version "1.0.1" - -promise-retry@^2.0.1: - version "2.0.1" - dependencies: - err-code "^2.0.2" - retry "^0.12.0" - -promzard@^0.3.0: - version "0.3.0" - dependencies: - read "1" - prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" @@ -2906,9 +1957,6 @@ punycode@^2.1.0: resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -qrcode-terminal@^0.12.0: - version "0.12.0" - queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" @@ -3032,43 +2080,6 @@ read-cache@^1.0.0: dependencies: pify "^2.3.0" -read-cmd-shim@^4.0.0: - version "4.0.0" - -read-package-json-fast@^3.0.0, read-package-json-fast@^3.0.1: - version "3.0.1" - dependencies: - json-parse-even-better-errors "^3.0.0" - npm-normalize-package-bin "^3.0.0" - -read-package-json@^6.0.0: - version "6.0.0" - dependencies: - glob "^8.0.1" - json-parse-even-better-errors "^3.0.0" - normalize-package-data "^5.0.0" - npm-normalize-package-bin "^3.0.0" - -read@^1.0.7, read@~1.0.7, read@1: - version "1.0.7" - dependencies: - mute-stream "~0.0.4" - -readable-stream@^3.6.0: - version "3.6.0" - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readable-stream@^4.1.0: - version "4.2.0" - dependencies: - abort-controller "^3.0.0" - buffer "^6.0.3" - events "^3.3.0" - process "^0.11.10" - readdirp@~3.6.0: version "3.6.0" resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" @@ -3123,9 +2134,6 @@ resolve@^2.0.0-next.3: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -retry@^0.12.0: - version "0.12.0" - reusify@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" @@ -3152,11 +2160,6 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - safe-regex-test@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz" @@ -3166,11 +2169,6 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" -"safer-buffer@>= 2.1.2 < 3.0.0": - version "2.1.2" - resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - scheduler@^0.20.2: version "0.20.2" resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz" @@ -3184,16 +2182,13 @@ semver@^6.3.0: resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.0.0, semver@^7.1.1, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: +semver@^7.3.7: version "7.3.8" resolved "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz" integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== dependencies: lru-cache "^6.0.0" -set-blocking@^2.0.0: - version "2.0.0" - shallowequal@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz" @@ -3220,82 +2215,21 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" -signal-exit@^3.0.7: - version "3.0.7" - slash@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -smart-buffer@^4.2.0: - version "4.2.0" - -socks-proxy-agent@^7.0.0: - version "7.0.0" - dependencies: - agent-base "^6.0.2" - debug "^4.3.3" - socks "^2.6.2" - -socks@^2.6.2: - version "2.7.0" - dependencies: - ip "^2.0.0" - smart-buffer "^4.2.0" - source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== -spdx-correct@^3.0.0: - version "3.1.1" - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.3.0" - -spdx-expression-parse@^3.0.0: - version "3.0.1" - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.11" - srt-webvtt@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/srt-webvtt/-/srt-webvtt-2.0.0.tgz" integrity sha512-G2Z7/Jf2NRKrmLYNSIhSYZZYE6OFlKXFp9Au2/zJBKgrioUzmrAys1x7GT01dwl6d2sEnqr5uahEIOd0JW/Rbw== -ssri@^10.0.0, ssri@^10.0.1: - version "10.0.1" - dependencies: - minipass "^4.0.0" - -ssri@^9.0.0: - version "9.0.1" - dependencies: - minipass "^3.1.1" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string.prototype.matchall@^4.0.6: version "4.0.8" resolved "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz" @@ -3400,32 +2334,16 @@ tailwindcss@^3.2.4, "tailwindcss@>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1", tailwin quick-lru "^5.1.1" resolve "^1.22.1" -tar@^6.1.11, tar@^6.1.13, tar@^6.1.2: - version "6.1.13" - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^4.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" - text-table@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== -text-table@~0.2.0: - version "0.2.0" - tiny-invariant@^1.0.2: version "1.3.1" resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz" integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== -tiny-relative-date@^1.3.0: - version "1.3.0" - tiny-warning@^1.0.0: version "1.0.3" resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz" @@ -3438,9 +2356,6 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -treeverse@^3.0.0: - version "3.0.0" - tsconfig-paths@^3.14.1: version "3.14.1" resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz" @@ -3495,26 +2410,6 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" -unique-filename@^2.0.0: - version "2.0.1" - dependencies: - unique-slug "^3.0.0" - -unique-filename@^3.0.0: - version "3.0.0" - dependencies: - unique-slug "^4.0.0" - -unique-slug@^3.0.0: - version "3.0.0" - dependencies: - imurmurhash "^0.1.4" - -unique-slug@^4.0.0: - version "4.0.0" - dependencies: - imurmurhash "^0.1.4" - unpacker@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/unpacker/-/unpacker-1.0.1.tgz" @@ -3535,22 +2430,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -util-deprecate@^1.0.1, util-deprecate@^1.0.2: +util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -validate-npm-package-license@^3.0.4: - version "3.0.4" - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - -validate-npm-package-name@^5.0.0: - version "5.0.0" - dependencies: - builtins "^5.0.0" - value-equal@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz" @@ -3578,14 +2462,6 @@ void-elements@3.1.0: resolved "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz" integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== -walk-up-path@^1.0.0: - version "1.0.0" - -wcwidth@^1.0.0: - version "1.0.1" - dependencies: - defaults "^1.0.3" - which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz" @@ -3604,21 +2480,6 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -which@^2.0.2: - version "2.0.2" - dependencies: - isexe "^2.0.0" - -which@^3.0.0: - version "3.0.0" - dependencies: - isexe "^2.0.0" - -wide-align@^1.1.5: - version "1.1.5" - dependencies: - string-width "^1.0.2 || 2 || 3 || 4" - word-wrap@^1.2.3: version "1.2.3" resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" @@ -3629,12 +2490,6 @@ wrappy@1: resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -write-file-atomic@^5.0.0: - version "5.0.0" - dependencies: - imurmurhash "^0.1.4" - signal-exit "^3.0.7" - xtend@^4.0.2: version "4.0.2" resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz" From 4a352879759e70f6548e8e4734d53c50d2b23df9 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 18 Feb 2023 22:42:24 +0100 Subject: [PATCH 122/135] start on jons providers Co-authored-by: Jip Frijlink Co-authored-by: Jonathan Barrow --- src/backend/embeds/playm4u.ts | 20 +++ src/backend/embeds/streamm4u.ts | 71 +++++++++++ src/backend/helpers/embed.ts | 4 +- src/backend/helpers/scrape.ts | 18 +-- src/backend/index.ts | 4 +- src/backend/providers/m4ufree.ts | 207 +++++++++++++++++++++++++++++++ 6 files changed, 313 insertions(+), 11 deletions(-) create mode 100644 src/backend/embeds/playm4u.ts create mode 100644 src/backend/embeds/streamm4u.ts create mode 100644 src/backend/providers/m4ufree.ts diff --git a/src/backend/embeds/playm4u.ts b/src/backend/embeds/playm4u.ts new file mode 100644 index 00000000..4be4d455 --- /dev/null +++ b/src/backend/embeds/playm4u.ts @@ -0,0 +1,20 @@ +import { MWEmbedType } from "@/backend/helpers/embed"; +import { MWMediaType } from "../metadata/types"; +import { registerEmbedScraper } from "@/backend/helpers/register"; +import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams"; + +registerEmbedScraper({ + id: "playm4u", + displayName: "playm4u", + for: MWEmbedType.PLAYM4U, + rank: 0, + async getStream(ctx) { + throw new Error("Oh well 2") + return { + streamUrl: '', + quality: MWStreamQuality.Q1080P, + captions: [], + type: MWStreamType.MP4, + }; + }, +}) \ No newline at end of file diff --git a/src/backend/embeds/streamm4u.ts b/src/backend/embeds/streamm4u.ts new file mode 100644 index 00000000..ccbc7d47 --- /dev/null +++ b/src/backend/embeds/streamm4u.ts @@ -0,0 +1,71 @@ +import { MWEmbedType } from "@/backend/helpers/embed"; +import { MWMediaType } from "../metadata/types"; +import { registerEmbedScraper } from "@/backend/helpers/register"; +import { MWStreamQuality, MWStreamType, MWStream } from "@/backend/helpers/streams"; +import { proxiedFetch } from "@/backend/helpers/fetch"; + +const HOST = 'streamm4u.club'; +const URL_BASE = `https://${HOST}`; +const URL_API = `${URL_BASE}/api`; +const URL_API_SOURCE = `${URL_API}/source`; + +// TODO check out 403 / 404 on successfully returned video stream URLs +registerEmbedScraper({ + id: "streamm4u", + displayName: "streamm4u", + for: MWEmbedType.STREAMM4U, + rank: 100, + async getStream({ progress, url }) { + + const scrapingThreads = []; + let streams = []; + + const sources = (await scrape(url)).sort((a, b) => Number(b.quality.replace("p", "")) - Number(a.quality.replace("p", ""))); + let preferredSourceIndex = 0; + let preferredSource; + + while (!preferredSource && sources[preferredSourceIndex]) { + console.log('Testing', preferredSourceIndex) + console.log(sources[preferredSourceIndex]?.streamUrl) + // try { + // await proxiedFetch(sources[preferredSourceIndex]?.streamUrl) + // } catch (err) { } + preferredSource = sources[0] + preferredSourceIndex++ + } + console.log(preferredSource) + + if (!preferredSource) throw new Error("No source found") + + progress(100) + + return preferredSource + }, +}) + +async function scrape(embed: string) { + const sources: MWStream[] = []; + + const embedID = embed.split('/').pop(); + + console.log(`${URL_API_SOURCE}/${embedID}`) + const json = await proxiedFetch(`${URL_API_SOURCE}/${embedID}`, { + method: 'POST', + body: `r=&d=${HOST}` + }); + + if (json.success) { + const streams = json.data; + + for (const stream of streams) { + sources.push({ + streamUrl: stream.file as string, + quality: stream.label as MWStreamQuality, + type: stream.type as MWStreamType, + captions: [] + }); + } + } + + return sources; +} \ No newline at end of file diff --git a/src/backend/helpers/embed.ts b/src/backend/helpers/embed.ts index 9f99b28a..0ccf1419 100644 --- a/src/backend/helpers/embed.ts +++ b/src/backend/helpers/embed.ts @@ -1,7 +1,9 @@ import { MWStream } from "./streams"; export enum MWEmbedType { - OPENLOAD = "openload", + M4UFREE = "m4ufree", + STREAMM4U = "streamm4u", + PLAYM4U = "playm4u" } export type MWEmbed = { diff --git a/src/backend/helpers/scrape.ts b/src/backend/helpers/scrape.ts index 2e9e5e65..7805fa4c 100644 --- a/src/backend/helpers/scrape.ts +++ b/src/backend/helpers/scrape.ts @@ -25,15 +25,15 @@ type MWProviderRunContextBase = { }; type MWProviderRunContextTypeSpecific = | { - type: MWMediaType.MOVIE | MWMediaType.ANIME; - episode: undefined; - season: undefined; - } + type: MWMediaType.MOVIE | MWMediaType.ANIME; + episode: undefined; + season: undefined; + } | { - type: MWMediaType.SERIES; - episode: string; - season: string; - }; + type: MWMediaType.SERIES; + episode: string; + season: string; + }; export type MWProviderRunContext = MWProviderRunContextBase & MWProviderRunContextTypeSpecific; @@ -50,7 +50,7 @@ async function findBestEmbedStream( embedNum += 1; if (!embed.type) continue; const scraper = getEmbedScraperByType(embed.type); - if (!scraper) throw new Error("Type for embed not found"); + if (!scraper) throw new Error("Type for embed not found: " + embed.type); const eventId = [providerId, scraper.id, embedNum].join("|"); diff --git a/src/backend/index.ts b/src/backend/index.ts index bb752003..7261a45d 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -5,8 +5,10 @@ import "./providers/gdriveplayer"; import "./providers/flixhq"; import "./providers/superstream"; import "./providers/netfilm"; +import "./providers/m4ufree"; // embeds -// -- nothing here yet +import "./embeds/streamm4u" +import "./embeds/playm4u" initializeScraperStore(); diff --git a/src/backend/providers/m4ufree.ts b/src/backend/providers/m4ufree.ts new file mode 100644 index 00000000..78cc2d15 --- /dev/null +++ b/src/backend/providers/m4ufree.ts @@ -0,0 +1,207 @@ +import { compareTitle } from "@/utils/titleMatch"; +import { MWEmbedType } from "../helpers/embed"; +import { proxiedFetch } from "../helpers/fetch"; +import { registerProvider } from "../helpers/register"; +import { MWMediaType } from "../metadata/types"; +import { MWEmbed } from "@/backend/helpers/embed"; + +const HOST = 'm4ufree.com'; +const URL_BASE = `https://${HOST}`; +const URL_SEARCH = `${URL_BASE}/search`; +const URL_AJAX = `${URL_BASE}/ajax`; +const URL_AJAX_TV = `${URL_BASE}/ajaxtv`; + +// * Years can be in one of 4 formats: +// * - "startyear" (for movies, EX: 2022) +// * - "startyear-" (for TV series which has not ended, EX: 2022-) +// * - "startyear-endyear" (for TV series which has ended, EX: 2022-2023) +// * - "startyearendyear" (for TV series which has ended, EX: 20222023) +const REGEX_TITLE_AND_YEAR = /(.*) \(?(\d*|\d*-|\d*-\d*)\)?$/; +const REGEX_TYPE = /.*-(movie|tvshow)-online-free-m4ufree\.html/; +const REGEX_COOKIES = /XSRF-TOKEN=(.*?);.*laravel_session=(.*?);/; +const REGEX_SEASON_EPISODE = /S(\d*)-E(\d*)/; + +function toDom(html: string) { + return new DOMParser().parseFromString(html, "text/html") +} + +registerProvider({ + id: "m4ufree", + displayName: "m4ufree", + rank: -1, + type: [MWMediaType.MOVIE, MWMediaType.SERIES], + + async scrape({ media, progress, type, episode: episodeId, season: seasonId }) { + const season = media.meta.seasons?.find(s => s.id === seasonId)?.number || 1 + const episode = media.meta.type === MWMediaType.SERIES ? media.meta.seasonData.episodes.find(ep => ep.id === episodeId)?.number || 1 : undefined + + const embeds: MWEmbed[] = []; + + /* +, { + responseType: "text" as any, + } + */ + let responseText = await proxiedFetch(`${URL_SEARCH}/${encodeURIComponent(media.meta.title)}.html`); + let dom = toDom(responseText); + + const searchResults = [...dom.querySelectorAll('.item')].map(element => { + const tooltipText = element.querySelector('.tiptitle p')?.innerHTML; + if (!tooltipText) return; + + let regexResult = REGEX_TITLE_AND_YEAR.exec(tooltipText); + + if (!regexResult || !regexResult[1] || !regexResult[2]) { + return; + } + + const title = regexResult[1]; + const year = Number(regexResult[2].slice(0, 4)); // * Some media stores the start AND end year. Only need start year + const a = element.querySelector('a'); + if (!a) return; + const href = a.href; + + regexResult = REGEX_TYPE.exec(href); + + if (!regexResult || !regexResult[1]) { + return; + } + + let scraperDeterminedType = regexResult[1]; + + scraperDeterminedType = scraperDeterminedType === 'tvshow' ? 'show' : 'movie'; // * Map to Trakt type + + return { type: scraperDeterminedType, title, year, href }; + }).filter(item => item); + + const mediaInResults = searchResults.find(item => item && item.title === media.meta.title && item.year.toString() === media.meta.year); + + if (!mediaInResults) { + // * Nothing found + return { + embeds, + }; + } + + let cookies: string | null = ''; + const responseTextFromMedia = await proxiedFetch(mediaInResults.href, { + onResponse(context) { + cookies = context.response.headers.get('X-Set-Cookie') + }, + }); + dom = toDom(responseTextFromMedia); + + let regexResult = REGEX_COOKIES.exec(cookies); + + if (!regexResult || !regexResult[1] || !regexResult[2]) { + // * DO SOMETHING? + throw new Error("No regexResults, yikesssssss kinda gross idk") + } + + const cookieHeader = `XSRF-TOKEN=${regexResult[1]}; laravel_session=${regexResult[2]}`; + + const token = dom.querySelector('meta[name="csrf-token"]')?.getAttribute("content"); + if (!token) return { embeds }; + + if (type === MWMediaType.SERIES) { + // * Get the season/episode data + const episodes = [...dom.querySelectorAll('.episode')].map(element => { + regexResult = REGEX_SEASON_EPISODE.exec(element.innerHTML); + + if (!regexResult || !regexResult[1] || !regexResult[2]) { + return; + } + + const episode = Number(regexResult[1]); + const season = Number(regexResult[2]); + + return { + id: element.getAttribute('idepisode'), + episode: episode, + season: season + }; + }).filter(item => item); + + const ep = episodes.find(ep => ep && ep.episode === episode && ep.season === season); + if (!ep) return { embeds } + + const form = `idepisode=${ep.id}&_token=${token}`; + + let response = await proxiedFetch(URL_AJAX_TV, { + method: 'POST', + headers: { + 'Accept': '*/*', + 'Accept-Encoding': 'gzip, deflate, br', + 'Accept-Language': "en-US,en;q=0.9", + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + 'X-Requested-With': 'XMLHttpRequest', + 'Sec-CH-UA': '"Not?A_Brand";v="8", "Chromium";v="108", "Microsoft Edge";v="108"', + 'Sec-CH-UA-Mobile': '?0', + 'Sec-CH-UA-Platform': '"Linux"', + 'Sec-Fetch-Site': 'same-origin', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Dest': 'empty', + 'X-Cookie': cookieHeader, + 'X-Origin': URL_BASE, + 'X-Referer': mediaInResults.href + }, + body: form + }); + + dom = toDom(response); + } + + const servers = [...dom.querySelectorAll('.singlemv')].map(element => element.getAttribute('data')); + + for (const server of servers) { + const form = `m4u=${server}&_token=${token}`; + + const response = await proxiedFetch(URL_AJAX, { + method: 'POST', + headers: { + 'Accept': '*/*', + 'Accept-Encoding': 'gzip, deflate, br', + 'Accept-Language': "en-US,en;q=0.9", + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + 'X-Requested-With': 'XMLHttpRequest', + 'Sec-CH-UA': '"Not?A_Brand";v="8", "Chromium";v="108", "Microsoft Edge";v="108"', + 'Sec-CH-UA-Mobile': '?0', + 'Sec-CH-UA-Platform': '"Linux"', + 'Sec-Fetch-Site': 'same-origin', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Dest': 'empty', + 'X-Cookie': cookieHeader, + 'X-Origin': URL_BASE, + 'X-Referer': mediaInResults.href + }, + body: form + }); + + const dom = toDom(response); + + const link = dom.querySelector('iframe')?.src; + + const getEmbedType = (url: string) => { + if (url.startsWith("https://streamm4u.club")) return MWEmbedType.STREAMM4U + if (url.startsWith("https://play.playm4u.xyz")) return MWEmbedType.PLAYM4U + return null; + } + + if (!link) continue; + + const embedType = getEmbedType(link); + if (embedType) { + embeds.push({ + url: link, + type: embedType + }) + }; + } + + console.log(embeds); + return { + embeds, + } + + } +}); From 209fe4369c2430e7e417429e6533731e658411f2 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sun, 19 Feb 2023 15:25:58 +0100 Subject: [PATCH 123/135] fix source selector with ids and fixed navigation issue with episode selector Co-authored-by: Jip Frijlink --- src/video/components/VideoPlayerBase.tsx | 2 +- src/video/components/actions/HeaderAction.tsx | 2 +- src/video/components/actions/PageTitleAction.tsx | 2 +- src/video/components/actions/SeriesSelectionAction.tsx | 2 +- src/video/components/controllers/SeriesController.tsx | 4 +++- .../components/hooks/useCurrentSeriesEpisodeInfo.ts | 2 +- src/video/components/parts/VideoPlayerError.tsx | 2 +- .../components/popouts/EpisodeSelectionPopout.tsx | 6 +++++- src/video/components/popouts/SourceSelectionPopout.tsx | 10 +++------- src/video/state/types.ts | 3 ++- src/views/media/MediaView.tsx | 5 ++--- 11 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/video/components/VideoPlayerBase.tsx b/src/video/components/VideoPlayerBase.tsx index 1f1fbba7..954bf0d1 100644 --- a/src/video/components/VideoPlayerBase.tsx +++ b/src/video/components/VideoPlayerBase.tsx @@ -32,7 +32,7 @@ function VideoPlayerBaseWithState(props: VideoPlayerBaseProps) { // TODO move error boundary to only decorated, shouldn't have styling return ( - +
; + return ; } diff --git a/src/video/components/actions/PageTitleAction.tsx b/src/video/components/actions/PageTitleAction.tsx index ab0daf0a..171a06f7 100644 --- a/src/video/components/actions/PageTitleAction.tsx +++ b/src/video/components/actions/PageTitleAction.tsx @@ -9,7 +9,7 @@ export function PageTitleAction() { if (!meta) return null; - const title = isSeries ? `${meta.title} - ${humanizedEpisodeId}` : meta.title; + const title = isSeries ? `${meta.meta.title} - ${humanizedEpisodeId}` : meta.meta.title; return ( diff --git a/src/video/components/actions/SeriesSelectionAction.tsx b/src/video/components/actions/SeriesSelectionAction.tsx index f048db26..abf2082e 100644 --- a/src/video/components/actions/SeriesSelectionAction.tsx +++ b/src/video/components/actions/SeriesSelectionAction.tsx @@ -19,7 +19,7 @@ export function SeriesSelectionAction(props: Props) { const videoInterface = useInterface(descriptor); const controls = useControls(descriptor); - if (meta?.meta.type !== MWMediaType.SERIES) return null; + if (meta?.meta.meta.type !== MWMediaType.SERIES) return null; return (
diff --git a/src/video/components/controllers/SeriesController.tsx b/src/video/components/controllers/SeriesController.tsx index d676e15b..481d3ce2 100644 --- a/src/video/components/controllers/SeriesController.tsx +++ b/src/video/components/controllers/SeriesController.tsx @@ -1,6 +1,7 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useMeta } from "@/video/state/logic/meta"; import { useEffect, useRef } from "react"; +import { useHistory } from "react-router-dom"; interface SeriesControllerProps { onSelect?: (state: { episodeId?: string; seasonId?: string }) => void; @@ -9,6 +10,7 @@ interface SeriesControllerProps { export function SeriesController(props: SeriesControllerProps) { const descriptor = useVideoPlayerDescriptor(); const meta = useMeta(descriptor); + const history = useHistory(); const lastState = useRef<{ episodeId?: string; @@ -34,7 +36,7 @@ export function SeriesController(props: SeriesControllerProps) { lastState.current = currentState; props.onSelect?.(currentState); } - }, [meta, props]); + }, [meta, props, history]); return null; } diff --git a/src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts b/src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts index 7e62a0e4..7275cc07 100644 --- a/src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts +++ b/src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts @@ -20,7 +20,7 @@ export function useCurrentSeriesEpisodeInfo(descriptor: string) { }, [currentSeasonInfo, meta]); const isSeries = Boolean( - meta?.meta?.type === MWMediaType.SERIES && meta?.episode + meta?.meta.meta.type === MWMediaType.SERIES && meta?.episode ); if (!isSeries) return { isSeries: false }; diff --git a/src/video/components/parts/VideoPlayerError.tsx b/src/video/components/parts/VideoPlayerError.tsx index 77fb01d0..6ac7125c 100644 --- a/src/video/components/parts/VideoPlayerError.tsx +++ b/src/video/components/parts/VideoPlayerError.tsx @@ -31,7 +31,7 @@ export function VideoPlayerError(props: VideoPlayerErrorProps) {

- +
); diff --git a/src/video/components/popouts/EpisodeSelectionPopout.tsx b/src/video/components/popouts/EpisodeSelectionPopout.tsx index 7947d855..de54b457 100644 --- a/src/video/components/popouts/EpisodeSelectionPopout.tsx +++ b/src/video/components/popouts/EpisodeSelectionPopout.tsx @@ -57,7 +57,11 @@ export function EpisodeSelectionPopout() { const setCurrent = useCallback( (seasonId: string, episodeId: string) => { - controls.setCurrentEpisode(seasonId, episodeId); + controls.closePopout(); + // race condition, jank solution but it works. + setTimeout(() => { + controls.setCurrentEpisode(seasonId, episodeId); + }, 100) }, [controls] ); diff --git a/src/video/components/popouts/SourceSelectionPopout.tsx b/src/video/components/popouts/SourceSelectionPopout.tsx index 0f353094..270ccf81 100644 --- a/src/video/components/popouts/SourceSelectionPopout.tsx +++ b/src/video/components/popouts/SourceSelectionPopout.tsx @@ -21,7 +21,7 @@ export function SourceSelectionPopout() { const meta = useMeta(descriptor); const providers = useMemo( () => - meta ? getProviders().filter((v) => v.type.includes(meta.meta.type)) : [], + meta ? getProviders().filter((v) => v.type.includes(meta.meta.meta.type)) : [], [meta] ); @@ -39,13 +39,9 @@ export function SourceSelectionPopout() { if (!theProvider) throw new Error("Invalid provider"); if (!meta) throw new Error("need meta"); return runProvider(theProvider, { - media: { - imdbId: "", // TODO get actual ids - tmdbId: "", - meta: meta.meta, - }, + media: meta.meta, progress: () => { }, - type: meta.meta.type, + type: meta.meta.meta.type, episode: meta.episode?.episodeId as any, season: meta.episode?.seasonId as any, }); diff --git a/src/video/state/types.ts b/src/video/state/types.ts index ff378c52..5686af1d 100644 --- a/src/video/state/types.ts +++ b/src/video/state/types.ts @@ -3,12 +3,13 @@ import { MWStreamQuality, MWStreamType, } from "@/backend/helpers/streams"; +import { DetailedMeta } from "@/backend/metadata/getmeta"; import { MWMediaMeta } from "@/backend/metadata/types"; import Hls from "hls.js"; import { VideoPlayerStateProvider } from "./providers/providerTypes"; export type VideoPlayerMeta = { - meta: MWMediaMeta; + meta: DetailedMeta; captions: MWCaption[]; episode?: { episodeId: string; diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index f12869f4..242c73ea 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -37,7 +37,7 @@ function MediaViewLoading(props: { onGoBack(): void }) {
-

{t("videoPlaye.findingBestVideo")}

+

{t("videoPlayer.findingBestVideo")}

); @@ -114,7 +114,7 @@ export function MediaViewPlayer(props: MediaViewPlayerProps) { }, [props.stream]); const metaProps: VideoPlayerMeta = { - meta: props.meta.meta, + meta: props.meta, captions: [], }; let metaSeasonData: MWSeasonWithEpisodeMeta | undefined; @@ -254,7 +254,6 @@ export function MediaView() { stream={stream} selected={selected} onChangeStream={(sId, eId) => { - // TODO changing episode breaks useGoBack history.replace( `/media/${encodeURIComponent(params.media)}/${encodeURIComponent( sId From a0751380e5a0b59f20cfbc3181ada343e0b509b7 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sun, 19 Feb 2023 15:55:09 +0100 Subject: [PATCH 124/135] better source selection (empty states, error states, embed support Co-authored-by: Jip Frijlink --- package.json | 1 + src/backend/embeds/playm4u.ts | 2 +- src/backend/embeds/streamm4u.ts | 13 +- src/backend/helpers/embed.ts | 2 +- src/setup/locales/en/translation.json | 1 + .../popouts/SourceSelectionPopout.tsx | 104 +++++++-- .../state/providers/castingStateProvider.ts | 2 +- vite.config.ts | 9 +- yarn.lock | 218 +++++++++++++++++- 9 files changed, 310 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index 6663ba27..e7883b1d 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "tailwindcss": "^3.2.4", "typescript": "^4.6.4", "vite": "^4.0.1", + "vite-plugin-checker": "^0.5.6", "vite-plugin-package-version": "^1.0.2" } } diff --git a/src/backend/embeds/playm4u.ts b/src/backend/embeds/playm4u.ts index 4be4d455..45d5e2f3 100644 --- a/src/backend/embeds/playm4u.ts +++ b/src/backend/embeds/playm4u.ts @@ -9,7 +9,7 @@ registerEmbedScraper({ for: MWEmbedType.PLAYM4U, rank: 0, async getStream(ctx) { - throw new Error("Oh well 2") + // throw new Error("Oh well 2") return { streamUrl: '', quality: MWStreamQuality.Q1080P, diff --git a/src/backend/embeds/streamm4u.ts b/src/backend/embeds/streamm4u.ts index ccbc7d47..f9b34c4a 100644 --- a/src/backend/embeds/streamm4u.ts +++ b/src/backend/embeds/streamm4u.ts @@ -22,18 +22,7 @@ registerEmbedScraper({ const sources = (await scrape(url)).sort((a, b) => Number(b.quality.replace("p", "")) - Number(a.quality.replace("p", ""))); let preferredSourceIndex = 0; - let preferredSource; - - while (!preferredSource && sources[preferredSourceIndex]) { - console.log('Testing', preferredSourceIndex) - console.log(sources[preferredSourceIndex]?.streamUrl) - // try { - // await proxiedFetch(sources[preferredSourceIndex]?.streamUrl) - // } catch (err) { } - preferredSource = sources[0] - preferredSourceIndex++ - } - console.log(preferredSource) + let preferredSource = sources[0]; if (!preferredSource) throw new Error("No source found") diff --git a/src/backend/helpers/embed.ts b/src/backend/helpers/embed.ts index 0ccf1419..4dc6ee95 100644 --- a/src/backend/helpers/embed.ts +++ b/src/backend/helpers/embed.ts @@ -7,7 +7,7 @@ export enum MWEmbedType { } export type MWEmbed = { - type: MWEmbedType | null; + type: MWEmbedType; url: string; }; diff --git a/src/setup/locales/en/translation.json b/src/setup/locales/en/translation.json index 388c7838..d29abc38 100644 --- a/src/setup/locales/en/translation.json +++ b/src/setup/locales/en/translation.json @@ -68,6 +68,7 @@ "episode": "E{{index}} - {{title}}", "noCaptions": "No captions", "linkedCaptions": "Linked captions", + "noEmbeds": "No embeds were found for this source", "errors": { "loadingWentWong": "Something went wrong loading the episodes for {{seasonTitle}}", "embedsError": "Something went wrong loading the embeds for this thing that you like" diff --git a/src/video/components/popouts/SourceSelectionPopout.tsx b/src/video/components/popouts/SourceSelectionPopout.tsx index 270ccf81..81f783b5 100644 --- a/src/video/components/popouts/SourceSelectionPopout.tsx +++ b/src/video/components/popouts/SourceSelectionPopout.tsx @@ -7,11 +7,42 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useMeta } from "@/video/state/logic/meta"; import { useControls } from "@/video/state/logic/controls"; import { MWStream } from "@/backend/helpers/streams"; -import { getProviders } from "@/backend/helpers/register"; -import { runProvider } from "@/backend/helpers/run"; +import { getEmbedScraperByType, getProviders } from "@/backend/helpers/register"; +import { runEmbedScraper, runProvider } from "@/backend/helpers/run"; import { MWProviderScrapeResult } from "@/backend/helpers/provider"; import { PopoutListEntry, PopoutSection } from "./PopoutUtils"; import { useTranslation } from "react-i18next"; +import { MWEmbed, MWEmbedType } from "@/backend/helpers/embed"; + +interface EmbedEntryProps { + name: string; + type: MWEmbedType; + url: string; + onSelect: (stream: MWStream) => void; +} + +export function EmbedEntry(props: EmbedEntryProps) { + const [scrapeEmbed, loading, error] = useLoading(async () => { + const scraper = getEmbedScraperByType(props.type); + if (!scraper) throw new Error("Embed scraper not found") + const stream = await runEmbedScraper(scraper, { + progress: () => { }, // no progress tracking for inline scraping + url: props.url, + }) + props.onSelect(stream); + }); + + return ( { + scrapeEmbed(); + }} + > + {props.name} + ) +} export function SourceSelectionPopout() { const { t } = useTranslation() @@ -71,15 +102,23 @@ export function SourceSelectionPopout() { return; } - runScraper(providerId).then((v) => { + runScraper(providerId).then(async (v) => { if (!providerRef.current) return; if (v) { const len = v.embeds.length + (v.stream ? 1 : 0); if (len === 1) { const realStream = v.stream; if (!realStream) { - // TODO scrape embed - throw new Error("no embed scraper configured"); + const embed = v?.embeds[0]; + if (!embed) throw new Error("Embed scraper not found") + const scraper = getEmbedScraperByType(embed.type); + if (!scraper) throw new Error("Embed scraper not found") + const stream = await runEmbedScraper(scraper, { + progress: () => { }, // no progress tracking for inline scraping + url: embed.url, + }) + selectSource(stream); + return; } selectSource(realStream); return; @@ -99,6 +138,33 @@ export function SourceSelectionPopout() { ].join(" "); }, [showingProvider]); + const visibleEmbeds = useMemo(() => { + const embeds = scrapeResult?.embeds || []; + + // Count embed types to determine if it should show a number behind the name + const embedsPerType: Record = {} + for (const embed of embeds) { + if (!embed.type) continue; + if (!embedsPerType[embed.type]) embedsPerType[embed.type] = []; + embedsPerType[embed.type].push({ + ...embed, + displayName: embed.type + }) + } + + const embedsRes = Object.entries(embedsPerType).flatMap(([type, entries]) => { + if (entries.length > 1) return entries.map((embed, i) => ({ + ...embed, + displayName: `${embed.type} ${i + 1}` + })) + return entries; + }) + + console.log(embedsRes) + + return embedsRes; + }, [scrapeResult?.embeds]) + return ( <> @@ -168,17 +234,27 @@ export function SourceSelectionPopout() { Native source ) : null} - {scrapeResult?.embeds.map((v) => ( - 0 ? visibleEmbeds?.map((v) => ( + { - console.log("EMBED CHOSEN"); + url={v.url} + onSelect={(stream) => { + selectSource(stream); }} - > - {v.type} - - ))} + /> + )) : (
+
+ +

+ {t("videoPlayer.popouts.noEmbeds")} +

+
+
)} )}
diff --git a/src/video/state/providers/castingStateProvider.ts b/src/video/state/providers/castingStateProvider.ts index 69806a39..c2f2432a 100644 --- a/src/video/state/providers/castingStateProvider.ts +++ b/src/video/state/providers/castingStateProvider.ts @@ -110,7 +110,7 @@ export function createCastingStateProvider( } const movieMeta = new chrome.cast.media.MovieMediaMetadata(); - movieMeta.title = state.meta?.meta.title ?? ""; + movieMeta.title = state.meta?.meta.meta.title ?? ""; // TODO contentId? const mediaInfo = new chrome.cast.media.MediaInfo("hello", "video/mp4"); diff --git a/vite.config.ts b/vite.config.ts index c9788164..72f5bf13 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,10 +1,17 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react-swc"; import loadVersion from "vite-plugin-package-version"; +import checker from 'vite-plugin-checker' import path from "path"; export default defineConfig({ - plugins: [react(), loadVersion()], + plugins: [ + react(), + loadVersion(), + checker({ + typescript: true, // check typescript build errors in dev server + }) + ], resolve: { alias: { "@": path.resolve(__dirname, "./src"), diff --git a/yarn.lock b/yarn.lock index 976dbed1..7ec2f05b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,27 @@ # yarn lockfile v1 +"@babel/code-frame@^7.12.13": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + dependencies: + "@babel/highlight" "^7.18.6" + +"@babel/helper-validator-identifier@^7.18.6": + version "7.19.1" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz" + integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== + +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/runtime-corejs3@^7.10.2": version "7.20.6" resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.20.6.tgz" @@ -397,11 +418,25 @@ ajv@^6.10.0, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" @@ -556,7 +591,16 @@ caniuse-lite@^1.0.30001400, caniuse-lite@^1.0.30001426: resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001439.tgz" integrity sha512-1MgUzEkoMO6gKfXflStpYgZDlFM7M/ck/bgfVCACO5vnAf0fXoNVHdWtqGU+MYca+4bL9Z5bpOVmR33cWW9G2A== -chalk@^4.0.0: +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0, chalk@^4.1.1: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -564,7 +608,7 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chokidar@^3.5.3: +chokidar@^3.5.1, chokidar@^3.5.3: version "3.5.3" resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -589,6 +633,13 @@ client-only@^0.0.1: resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" @@ -601,6 +652,16 @@ color-name@^1.1.4, color-name@~1.1.4: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +commander@^8.0.0: + version "8.3.0" + resolved "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" @@ -832,6 +893,11 @@ escalade@^3.1.1: resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" @@ -990,7 +1056,7 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@*, "eslint@^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8", "eslint@^3 || ^4 || ^5 || ^6 || ^7 || ^8", "eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "eslint@^6.0.0 || ^7.0.0 || ^8.0.0", "eslint@^7.32.0 || ^8.2.0", eslint@^8.10.0, eslint@>=5, eslint@>=7.0.0, eslint@>=7.28.0: +eslint@*, "eslint@^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8", "eslint@^3 || ^4 || ^5 || ^6 || ^7 || ^8", "eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "eslint@^6.0.0 || ^7.0.0 || ^8.0.0", "eslint@^7.32.0 || ^8.2.0", eslint@^8.10.0, eslint@>=5, eslint@>=7, eslint@>=7.0.0, eslint@>=7.28.0: version "8.29.0" resolved "https://registry.npmjs.org/eslint/-/eslint-8.29.0.tgz" integrity sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg== @@ -1088,7 +1154,7 @@ fast-diff@^1.1.2: resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz" integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== -fast-glob@^3.2.12, fast-glob@^3.2.9: +fast-glob@^3.2.12, fast-glob@^3.2.7, fast-glob@^3.2.9: version "3.2.12" resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz" integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== @@ -1156,6 +1222,15 @@ fraction.js@^4.2.0: resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz" integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== +fs-extra@^11.1.0: + version "11.1.0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.0.tgz" + integrity sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" @@ -1267,6 +1342,11 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.10" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + grapheme-splitter@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz" @@ -1277,6 +1357,11 @@ has-bigints@^1.0.1, has-bigints@^1.0.2: resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz" integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + has-flag@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" @@ -1519,7 +1604,7 @@ js-sdsl@^4.1.4: resolved "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz" integrity sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ== -"js-tokens@^3.0.0 || ^4.0.0": +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== @@ -1553,6 +1638,15 @@ json5@^2.2.0: resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.2: version "3.3.3" resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz" @@ -1593,11 +1687,21 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.pick@^4.4.0: + version "4.4.0" + resolved "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz" + integrity sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q== + lodash.throttle@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz" @@ -1635,7 +1739,7 @@ micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" -minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -1697,6 +1801,13 @@ normalize-range@^0.1.2: resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + object-assign@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" @@ -1826,7 +1937,7 @@ path-is-absolute@^1.0.0: resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== -path-key@^3.1.0: +path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== @@ -2182,7 +2293,7 @@ semver@^6.3.0: resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.3.7: +semver@^7.3.4, semver@^7.3.7: version "7.3.8" resolved "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz" integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== @@ -2262,7 +2373,7 @@ string.prototype.trimstart@^1.0.6: define-properties "^1.1.4" es-abstract "^1.20.4" -strip-ansi@^6.0.1: +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -2288,6 +2399,13 @@ subscribe-ui-event@^2.0.6: lodash "^4.17.15" raf "^3.0.0" +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + supports-color@^7.1.0: version "7.2.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" @@ -2339,7 +2457,7 @@ text-table@^0.2.0: resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== -tiny-invariant@^1.0.2: +tiny-invariant@^1.0.2, tiny-invariant@^1.1.0: version "1.3.1" resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz" integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== @@ -2390,7 +2508,12 @@ type-fest@^0.20.2: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -typescript@^4.6.4, "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta": +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +typescript@*, typescript@^4.6.4, "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta": version "4.9.4" resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz" integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== @@ -2410,6 +2533,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + unpacker@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/unpacker/-/unpacker-1.0.1.tgz" @@ -2440,12 +2568,34 @@ value-equal@^1.0.1: resolved "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz" integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== +vite-plugin-checker@^0.5.6: + version "0.5.6" + resolved "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.5.6.tgz" + integrity sha512-ftRyON0gORUHDxcDt2BErmsikKSkfvl1i2DoP6Jt2zDO9InfvM6tqO1RkXhSjkaXEhKPea6YOnhFaZxW3BzudQ== + dependencies: + "@babel/code-frame" "^7.12.13" + ansi-escapes "^4.3.0" + chalk "^4.1.1" + chokidar "^3.5.1" + commander "^8.0.0" + fast-glob "^3.2.7" + fs-extra "^11.1.0" + lodash.debounce "^4.0.8" + lodash.pick "^4.4.0" + npm-run-path "^4.0.1" + strip-ansi "^6.0.0" + tiny-invariant "^1.1.0" + vscode-languageclient "^7.0.0" + vscode-languageserver "^7.0.0" + vscode-languageserver-textdocument "^1.0.1" + vscode-uri "^3.0.2" + vite-plugin-package-version@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/vite-plugin-package-version/-/vite-plugin-package-version-1.0.2.tgz" integrity sha512-xCJMR0KD4rqSUwINyHJlLizio2VzYzaMrRkqC9xWaVGXgw1lIrzdD+wBUf1XDM8EhL1JoQ7aykLOfKrlZd1SoQ== -vite@^4.0.0, vite@^4.0.1, vite@>=2.0.0-beta.69: +vite@^4.0.0, vite@^4.0.1, vite@>=2.0.0, vite@>=2.0.0-beta.69: version "4.0.1" resolved "https://registry.npmjs.org/vite/-/vite-4.0.1.tgz" integrity sha512-kZQPzbDau35iWOhy3CpkrRC7It+HIHtulAzBhMqzGHKRf/4+vmh8rPDDdv98SWQrFWo6//3ozwsRmwQIPZsK9g== @@ -2462,6 +2612,50 @@ void-elements@3.1.0: resolved "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz" integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== +vscode-jsonrpc@6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz" + integrity sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg== + +vscode-languageclient@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz" + integrity sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg== + dependencies: + minimatch "^3.0.4" + semver "^7.3.4" + vscode-languageserver-protocol "3.16.0" + +vscode-languageserver-protocol@3.16.0: + version "3.16.0" + resolved "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz" + integrity sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A== + dependencies: + vscode-jsonrpc "6.0.0" + vscode-languageserver-types "3.16.0" + +vscode-languageserver-textdocument@^1.0.1: + version "1.0.8" + resolved "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.8.tgz" + integrity sha512-1bonkGqQs5/fxGT5UchTgjGVnfysL0O8v1AYMBjqTbWQTFn721zaPGDYFkOKtfDgFiSgXM3KwaG3FMGfW4Ed9Q== + +vscode-languageserver-types@3.16.0: + version "3.16.0" + resolved "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz" + integrity sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA== + +vscode-languageserver@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz" + integrity sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw== + dependencies: + vscode-languageserver-protocol "3.16.0" + +vscode-uri@^3.0.2: + version "3.0.7" + resolved "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.7.tgz" + integrity sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA== + which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz" From c441d630740235a4851f63b376ef9cec4cab9b77 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sun, 19 Feb 2023 16:05:19 +0100 Subject: [PATCH 125/135] normal routing instead of hash Co-authored-by: Jip Frijlink --- src/backend/providers/m4ufree.ts | 1 + src/index.tsx | 16 ++++++++++++---- src/setup/config.ts | 3 +++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/backend/providers/m4ufree.ts b/src/backend/providers/m4ufree.ts index 78cc2d15..76e558db 100644 --- a/src/backend/providers/m4ufree.ts +++ b/src/backend/providers/m4ufree.ts @@ -29,6 +29,7 @@ registerProvider({ id: "m4ufree", displayName: "m4ufree", rank: -1, + disabled: true, // Disables because the redirector URLs it returns will throw 404 / 403 depending on if you view it in the browser or fetch it respectively. It just does not work. type: [MWMediaType.MOVIE, MWMediaType.SERIES], async scrape({ media, progress, type, episode: episodeId, season: seasonId }) { diff --git a/src/index.tsx b/src/index.tsx index a6bbd66c..3a8d7594 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,6 +1,6 @@ -import React, { Suspense } from "react"; +import React, { ReactNode, Suspense } from "react"; import ReactDOM from "react-dom"; -import { HashRouter } from "react-router-dom"; +import { BrowserRouter, HashRouter } from "react-router-dom"; import { ErrorBoundary } from "@/components/layout/ErrorBoundary"; import { conf } from "@/setup/config"; @@ -42,14 +42,22 @@ const LazyLoadedApp = React.lazy(async () => { }; }); +function TheRouter(props: { children: ReactNode }) { + const normalRouter = conf().NORMAL_ROUTER; + + if (normalRouter) + return + return +} + ReactDOM.render( - + - + , document.getElementById("root") diff --git a/src/setup/config.ts b/src/setup/config.ts index 951be497..72a762f5 100644 --- a/src/setup/config.ts +++ b/src/setup/config.ts @@ -7,6 +7,7 @@ interface Config { OMDB_API_KEY: string; TMDB_API_KEY: string; CORS_PROXY_URL: string; + NORMAL_ROUTER: boolean; } export interface RuntimeConfig extends Config { @@ -20,6 +21,7 @@ const env: Record = { GITHUB_LINK: undefined, DISCORD_LINK: undefined, CORS_PROXY_URL: import.meta.env.VITE_CORS_PROXY_URL, + NORMAL_ROUTER: import.meta.env.VITE_NORMAL_ROUTER, }; const alerts = [] as string[]; @@ -51,5 +53,6 @@ export function conf(): RuntimeConfig { TMDB_API_KEY: getKey("TMDB_API_KEY"), BASE_PROXY_URL: getKey("CORS_PROXY_URL"), CORS_PROXY_URL: `${getKey("CORS_PROXY_URL")}/?destination=`, + NORMAL_ROUTER: (getKey("NORMAL_ROUTER") ?? "false") === "true", }; } From c90d59ef93f927c6cb3d64d6370d51314cd823f8 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sun, 19 Feb 2023 16:42:46 +0100 Subject: [PATCH 126/135] update script Co-authored-by: Jip Frijlink --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 26bfc261..7f198e1d 100644 --- a/index.html +++ b/index.html @@ -40,7 +40,7 @@ /> - + movie-web From b3db58012f896996a2336cb85dedd53e8fef8902 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sun, 19 Feb 2023 18:03:54 +0100 Subject: [PATCH 127/135] linting Co-authored-by: Jip Frijlink --- .eslintrc.js | 1 + src/backend/embeds/playm4u.ts | 29 ++- src/backend/embeds/streamm4u.ts | 97 ++++---- src/backend/helpers/embed.ts | 2 +- src/backend/helpers/scrape.ts | 18 +- src/backend/index.ts | 4 +- src/backend/providers/flixhq.ts | 5 +- src/backend/providers/m4ufree.ts | 229 ++++++++++-------- src/backend/providers/superstream/index.ts | 3 +- src/components/Overlay.tsx | 14 ++ src/components/SearchBar.tsx | 2 +- src/components/buttons/EditButton.tsx | 6 +- src/components/layout/ErrorBoundary.tsx | 7 +- src/components/layout/Modal.tsx | 25 ++ src/components/media/MediaCard.tsx | 32 +-- src/index.tsx | 7 +- src/state/watched/migrations/v2.ts | 13 +- src/utils/titleMatch.ts | 8 +- .../actions/CaptionsSelectionAction.tsx | 4 +- .../components/actions/PageTitleAction.tsx | 4 +- .../actions/SeriesSelectionAction.tsx | 2 +- .../actions/SourceSelectionAction.tsx | 2 +- .../hooks/useCurrentSeriesEpisodeInfo.ts | 6 +- .../popouts/CaptionSelectionPopout.tsx | 2 +- .../popouts/EpisodeSelectionPopout.tsx | 72 +++--- .../popouts/SourceSelectionPopout.tsx | 117 +++++---- src/video/state/types.ts | 1 - src/views/media/MediaErrorView.tsx | 11 +- src/views/media/MediaView.tsx | 19 +- src/views/notfound/NotFoundView.tsx | 2 +- src/views/other/v2Migration.tsx | 102 ++++---- src/views/search/SearchLoadingView.tsx | 17 +- src/views/search/SearchView.tsx | 2 +- vite.config.ts | 11 +- 34 files changed, 494 insertions(+), 382 deletions(-) create mode 100644 src/components/Overlay.tsx create mode 100644 src/components/layout/Modal.tsx diff --git a/.eslintrc.js b/.eslintrc.js index 1556850a..40cda0da 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -50,6 +50,7 @@ module.exports = { "no-await-in-loop": "off", "no-nested-ternary": "off", "prefer-destructuring": "off", + "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], "react/jsx-filename-extension": [ "error", { extensions: [".js", ".tsx", ".jsx"] } diff --git a/src/backend/embeds/playm4u.ts b/src/backend/embeds/playm4u.ts index 45d5e2f3..8328d337 100644 --- a/src/backend/embeds/playm4u.ts +++ b/src/backend/embeds/playm4u.ts @@ -1,20 +1,19 @@ import { MWEmbedType } from "@/backend/helpers/embed"; -import { MWMediaType } from "../metadata/types"; import { registerEmbedScraper } from "@/backend/helpers/register"; import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams"; registerEmbedScraper({ - id: "playm4u", - displayName: "playm4u", - for: MWEmbedType.PLAYM4U, - rank: 0, - async getStream(ctx) { - // throw new Error("Oh well 2") - return { - streamUrl: '', - quality: MWStreamQuality.Q1080P, - captions: [], - type: MWStreamType.MP4, - }; - }, -}) \ No newline at end of file + id: "playm4u", + displayName: "playm4u", + for: MWEmbedType.PLAYM4U, + rank: 0, + async getStream() { + // throw new Error("Oh well 2") + return { + streamUrl: "", + quality: MWStreamQuality.Q1080P, + captions: [], + type: MWStreamType.MP4, + }; + }, +}); diff --git a/src/backend/embeds/streamm4u.ts b/src/backend/embeds/streamm4u.ts index f9b34c4a..d0eba66a 100644 --- a/src/backend/embeds/streamm4u.ts +++ b/src/backend/embeds/streamm4u.ts @@ -1,60 +1,65 @@ import { MWEmbedType } from "@/backend/helpers/embed"; -import { MWMediaType } from "../metadata/types"; import { registerEmbedScraper } from "@/backend/helpers/register"; -import { MWStreamQuality, MWStreamType, MWStream } from "@/backend/helpers/streams"; +import { + MWStreamQuality, + MWStreamType, + MWStream, +} from "@/backend/helpers/streams"; import { proxiedFetch } from "@/backend/helpers/fetch"; -const HOST = 'streamm4u.club'; +const HOST = "streamm4u.club"; const URL_BASE = `https://${HOST}`; const URL_API = `${URL_BASE}/api`; const URL_API_SOURCE = `${URL_API}/source`; -// TODO check out 403 / 404 on successfully returned video stream URLs -registerEmbedScraper({ - id: "streamm4u", - displayName: "streamm4u", - for: MWEmbedType.STREAMM4U, - rank: 100, - async getStream({ progress, url }) { +async function scrape(embed: string) { + const sources: MWStream[] = []; - const scrapingThreads = []; - let streams = []; + const embedID = embed.split("/").pop(); - const sources = (await scrape(url)).sort((a, b) => Number(b.quality.replace("p", "")) - Number(a.quality.replace("p", ""))); - let preferredSourceIndex = 0; - let preferredSource = sources[0]; + console.log(`${URL_API_SOURCE}/${embedID}`); + const json = await proxiedFetch(`${URL_API_SOURCE}/${embedID}`, { + method: "POST", + body: `r=&d=${HOST}`, + }); - if (!preferredSource) throw new Error("No source found") + if (json.success) { + const streams = json.data; - progress(100) + for (const stream of streams) { + sources.push({ + streamUrl: stream.file as string, + quality: stream.label as MWStreamQuality, + type: stream.type as MWStreamType, + captions: [], + }); + } + } - return preferredSource - }, -}) + return sources; +} -async function scrape(embed: string) { - const sources: MWStream[] = []; - - const embedID = embed.split('/').pop(); - - console.log(`${URL_API_SOURCE}/${embedID}`) - const json = await proxiedFetch(`${URL_API_SOURCE}/${embedID}`, { - method: 'POST', - body: `r=&d=${HOST}` - }); - - if (json.success) { - const streams = json.data; - - for (const stream of streams) { - sources.push({ - streamUrl: stream.file as string, - quality: stream.label as MWStreamQuality, - type: stream.type as MWStreamType, - captions: [] - }); - } - } - - return sources; -} \ No newline at end of file +// TODO check out 403 / 404 on successfully returned video stream URLs +registerEmbedScraper({ + id: "streamm4u", + displayName: "streamm4u", + for: MWEmbedType.STREAMM4U, + rank: 100, + async getStream({ progress, url }) { + // const scrapingThreads = []; + // const streams = []; + + const sources = (await scrape(url)).sort( + (a, b) => + Number(b.quality.replace("p", "")) - Number(a.quality.replace("p", "")) + ); + // const preferredSourceIndex = 0; + const preferredSource = sources[0]; + + if (!preferredSource) throw new Error("No source found"); + + progress(100); + + return preferredSource; + }, +}); diff --git a/src/backend/helpers/embed.ts b/src/backend/helpers/embed.ts index 4dc6ee95..64d039b7 100644 --- a/src/backend/helpers/embed.ts +++ b/src/backend/helpers/embed.ts @@ -3,7 +3,7 @@ import { MWStream } from "./streams"; export enum MWEmbedType { M4UFREE = "m4ufree", STREAMM4U = "streamm4u", - PLAYM4U = "playm4u" + PLAYM4U = "playm4u", } export type MWEmbed = { diff --git a/src/backend/helpers/scrape.ts b/src/backend/helpers/scrape.ts index 7805fa4c..cb160305 100644 --- a/src/backend/helpers/scrape.ts +++ b/src/backend/helpers/scrape.ts @@ -25,15 +25,15 @@ type MWProviderRunContextBase = { }; type MWProviderRunContextTypeSpecific = | { - type: MWMediaType.MOVIE | MWMediaType.ANIME; - episode: undefined; - season: undefined; - } + type: MWMediaType.MOVIE | MWMediaType.ANIME; + episode: undefined; + season: undefined; + } | { - type: MWMediaType.SERIES; - episode: string; - season: string; - }; + type: MWMediaType.SERIES; + episode: string; + season: string; + }; export type MWProviderRunContext = MWProviderRunContextBase & MWProviderRunContextTypeSpecific; @@ -50,7 +50,7 @@ async function findBestEmbedStream( embedNum += 1; if (!embed.type) continue; const scraper = getEmbedScraperByType(embed.type); - if (!scraper) throw new Error("Type for embed not found: " + embed.type); + if (!scraper) throw new Error(`Type for embed not found: ${embed.type}`); const eventId = [providerId, scraper.id, embedNum].join("|"); diff --git a/src/backend/index.ts b/src/backend/index.ts index 7261a45d..7a13a445 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -8,7 +8,7 @@ import "./providers/netfilm"; import "./providers/m4ufree"; // embeds -import "./embeds/streamm4u" -import "./embeds/playm4u" +import "./embeds/streamm4u"; +import "./embeds/playm4u"; initializeScraperStore(); diff --git a/src/backend/providers/flixhq.ts b/src/backend/providers/flixhq.ts index c8a89400..fdab1292 100644 --- a/src/backend/providers/flixhq.ts +++ b/src/backend/providers/flixhq.ts @@ -21,7 +21,10 @@ registerProvider({ } ); const foundItem = searchResults.results.find((v: any) => { - return compareTitle(v.title, media.meta.title) && v.releaseDate === media.meta.year; + return ( + compareTitle(v.title, media.meta.title) && + v.releaseDate === media.meta.year + ); }); if (!foundItem) throw new Error("No watchable item found"); const flixId = foundItem.id; diff --git a/src/backend/providers/m4ufree.ts b/src/backend/providers/m4ufree.ts index 76e558db..f4e79f5b 100644 --- a/src/backend/providers/m4ufree.ts +++ b/src/backend/providers/m4ufree.ts @@ -1,11 +1,9 @@ -import { compareTitle } from "@/utils/titleMatch"; -import { MWEmbedType } from "../helpers/embed"; +import { MWEmbed, MWEmbedType } from "@/backend/helpers/embed"; import { proxiedFetch } from "../helpers/fetch"; import { registerProvider } from "../helpers/register"; import { MWMediaType } from "../metadata/types"; -import { MWEmbed } from "@/backend/helpers/embed"; -const HOST = 'm4ufree.com'; +const HOST = "m4ufree.com"; const URL_BASE = `https://${HOST}`; const URL_SEARCH = `${URL_BASE}/search`; const URL_AJAX = `${URL_BASE}/ajax`; @@ -22,7 +20,7 @@ const REGEX_COOKIES = /XSRF-TOKEN=(.*?);.*laravel_session=(.*?);/; const REGEX_SEASON_EPISODE = /S(\d*)-E(\d*)/; function toDom(html: string) { - return new DOMParser().parseFromString(html, "text/html") + return new DOMParser().parseFromString(html, "text/html"); } registerProvider({ @@ -32,9 +30,14 @@ registerProvider({ disabled: true, // Disables because the redirector URLs it returns will throw 404 / 403 depending on if you view it in the browser or fetch it respectively. It just does not work. type: [MWMediaType.MOVIE, MWMediaType.SERIES], - async scrape({ media, progress, type, episode: episodeId, season: seasonId }) { - const season = media.meta.seasons?.find(s => s.id === seasonId)?.number || 1 - const episode = media.meta.type === MWMediaType.SERIES ? media.meta.seasonData.episodes.find(ep => ep.id === episodeId)?.number || 1 : undefined + async scrape({ media, type, episode: episodeId, season: seasonId }) { + const season = + media.meta.seasons?.find((s) => s.id === seasonId)?.number || 1; + const episode = + media.meta.type === MWMediaType.SERIES + ? media.meta.seasonData.episodes.find((ep) => ep.id === episodeId) + ?.number || 1 + : undefined; const embeds: MWEmbed[] = []; @@ -43,39 +46,49 @@ registerProvider({ responseType: "text" as any, } */ - let responseText = await proxiedFetch(`${URL_SEARCH}/${encodeURIComponent(media.meta.title)}.html`); + const responseText = await proxiedFetch( + `${URL_SEARCH}/${encodeURIComponent(media.meta.title)}.html` + ); let dom = toDom(responseText); - const searchResults = [...dom.querySelectorAll('.item')].map(element => { - const tooltipText = element.querySelector('.tiptitle p')?.innerHTML; - if (!tooltipText) return; + const searchResults = [...dom.querySelectorAll(".item")] + .map((element) => { + const tooltipText = element.querySelector(".tiptitle p")?.innerHTML; + if (!tooltipText) return; - let regexResult = REGEX_TITLE_AND_YEAR.exec(tooltipText); + let regexResult = REGEX_TITLE_AND_YEAR.exec(tooltipText); - if (!regexResult || !regexResult[1] || !regexResult[2]) { - return; - } + if (!regexResult || !regexResult[1] || !regexResult[2]) { + return; + } - const title = regexResult[1]; - const year = Number(regexResult[2].slice(0, 4)); // * Some media stores the start AND end year. Only need start year - const a = element.querySelector('a'); - if (!a) return; - const href = a.href; + const title = regexResult[1]; + const year = Number(regexResult[2].slice(0, 4)); // * Some media stores the start AND end year. Only need start year + const a = element.querySelector("a"); + if (!a) return; + const href = a.href; - regexResult = REGEX_TYPE.exec(href); + regexResult = REGEX_TYPE.exec(href); - if (!regexResult || !regexResult[1]) { - return; - } + if (!regexResult || !regexResult[1]) { + return; + } - let scraperDeterminedType = regexResult[1]; + let scraperDeterminedType = regexResult[1]; - scraperDeterminedType = scraperDeterminedType === 'tvshow' ? 'show' : 'movie'; // * Map to Trakt type + scraperDeterminedType = + scraperDeterminedType === "tvshow" ? "show" : "movie"; // * Map to Trakt type - return { type: scraperDeterminedType, title, year, href }; - }).filter(item => item); + return { type: scraperDeterminedType, title, year, href }; + }) + .filter((item) => item); - const mediaInResults = searchResults.find(item => item && item.title === media.meta.title && item.year.toString() === media.meta.year); + const mediaInResults = searchResults.find( + (item) => + item && + item.title === media.meta.title && + item.year.toString() === media.meta.year + ); if (!mediaInResults) { // * Nothing found @@ -84,109 +97,124 @@ registerProvider({ }; } - let cookies: string | null = ''; - const responseTextFromMedia = await proxiedFetch(mediaInResults.href, { - onResponse(context) { - cookies = context.response.headers.get('X-Set-Cookie') - }, - }); + let cookies: string | null = ""; + const responseTextFromMedia = await proxiedFetch( + mediaInResults.href, + { + onResponse(context) { + cookies = context.response.headers.get("X-Set-Cookie"); + }, + } + ); dom = toDom(responseTextFromMedia); let regexResult = REGEX_COOKIES.exec(cookies); if (!regexResult || !regexResult[1] || !regexResult[2]) { // * DO SOMETHING? - throw new Error("No regexResults, yikesssssss kinda gross idk") + throw new Error("No regexResults, yikesssssss kinda gross idk"); } const cookieHeader = `XSRF-TOKEN=${regexResult[1]}; laravel_session=${regexResult[2]}`; - const token = dom.querySelector('meta[name="csrf-token"]')?.getAttribute("content"); + const token = dom + .querySelector('meta[name="csrf-token"]') + ?.getAttribute("content"); if (!token) return { embeds }; if (type === MWMediaType.SERIES) { // * Get the season/episode data - const episodes = [...dom.querySelectorAll('.episode')].map(element => { - regexResult = REGEX_SEASON_EPISODE.exec(element.innerHTML); - - if (!regexResult || !regexResult[1] || !regexResult[2]) { - return; - } - - const episode = Number(regexResult[1]); - const season = Number(regexResult[2]); - - return { - id: element.getAttribute('idepisode'), - episode: episode, - season: season - }; - }).filter(item => item); + const episodes = [...dom.querySelectorAll(".episode")] + .map((element) => { + regexResult = REGEX_SEASON_EPISODE.exec(element.innerHTML); + + if (!regexResult || !regexResult[1] || !regexResult[2]) { + return; + } + + const newEpisode = Number(regexResult[1]); + const newSeason = Number(regexResult[2]); + + return { + id: element.getAttribute("idepisode"), + episode: newEpisode, + season: newSeason, + }; + }) + .filter((item) => item); - const ep = episodes.find(ep => ep && ep.episode === episode && ep.season === season); - if (!ep) return { embeds } + const ep = episodes.find( + (newEp) => newEp && newEp.episode === episode && newEp.season === season + ); + if (!ep) return { embeds }; const form = `idepisode=${ep.id}&_token=${token}`; - let response = await proxiedFetch(URL_AJAX_TV, { - method: 'POST', + const response = await proxiedFetch(URL_AJAX_TV, { + method: "POST", headers: { - 'Accept': '*/*', - 'Accept-Encoding': 'gzip, deflate, br', - 'Accept-Language': "en-US,en;q=0.9", - 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', - 'X-Requested-With': 'XMLHttpRequest', - 'Sec-CH-UA': '"Not?A_Brand";v="8", "Chromium";v="108", "Microsoft Edge";v="108"', - 'Sec-CH-UA-Mobile': '?0', - 'Sec-CH-UA-Platform': '"Linux"', - 'Sec-Fetch-Site': 'same-origin', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Dest': 'empty', - 'X-Cookie': cookieHeader, - 'X-Origin': URL_BASE, - 'X-Referer': mediaInResults.href + Accept: "*/*", + "Accept-Encoding": "gzip, deflate, br", + "Accept-Language": "en-US,en;q=0.9", + "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", + "X-Requested-With": "XMLHttpRequest", + "Sec-CH-UA": + '"Not?A_Brand";v="8", "Chromium";v="108", "Microsoft Edge";v="108"', + "Sec-CH-UA-Mobile": "?0", + "Sec-CH-UA-Platform": '"Linux"', + "Sec-Fetch-Site": "same-origin", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Dest": "empty", + "X-Cookie": cookieHeader, + "X-Origin": URL_BASE, + "X-Referer": mediaInResults.href, }, - body: form + body: form, }); dom = toDom(response); } - const servers = [...dom.querySelectorAll('.singlemv')].map(element => element.getAttribute('data')); + const servers = [...dom.querySelectorAll(".singlemv")].map((element) => + element.getAttribute("data") + ); for (const server of servers) { const form = `m4u=${server}&_token=${token}`; const response = await proxiedFetch(URL_AJAX, { - method: 'POST', + method: "POST", headers: { - 'Accept': '*/*', - 'Accept-Encoding': 'gzip, deflate, br', - 'Accept-Language': "en-US,en;q=0.9", - 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', - 'X-Requested-With': 'XMLHttpRequest', - 'Sec-CH-UA': '"Not?A_Brand";v="8", "Chromium";v="108", "Microsoft Edge";v="108"', - 'Sec-CH-UA-Mobile': '?0', - 'Sec-CH-UA-Platform': '"Linux"', - 'Sec-Fetch-Site': 'same-origin', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Dest': 'empty', - 'X-Cookie': cookieHeader, - 'X-Origin': URL_BASE, - 'X-Referer': mediaInResults.href + Accept: "*/*", + "Accept-Encoding": "gzip, deflate, br", + "Accept-Language": "en-US,en;q=0.9", + "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", + "X-Requested-With": "XMLHttpRequest", + "Sec-CH-UA": + '"Not?A_Brand";v="8", "Chromium";v="108", "Microsoft Edge";v="108"', + "Sec-CH-UA-Mobile": "?0", + "Sec-CH-UA-Platform": '"Linux"', + "Sec-Fetch-Site": "same-origin", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Dest": "empty", + "X-Cookie": cookieHeader, + "X-Origin": URL_BASE, + "X-Referer": mediaInResults.href, }, - body: form + body: form, }); - const dom = toDom(response); + const serverDom = toDom(response); - const link = dom.querySelector('iframe')?.src; + const link = serverDom.querySelector("iframe")?.src; const getEmbedType = (url: string) => { - if (url.startsWith("https://streamm4u.club")) return MWEmbedType.STREAMM4U - if (url.startsWith("https://play.playm4u.xyz")) return MWEmbedType.PLAYM4U + if (url.startsWith("https://streamm4u.club")) + return MWEmbedType.STREAMM4U; + if (url.startsWith("https://play.playm4u.xyz")) + return MWEmbedType.PLAYM4U; return null; - } + }; if (!link) continue; @@ -194,15 +222,14 @@ registerProvider({ if (embedType) { embeds.push({ url: link, - type: embedType - }) - }; + type: embedType, + }); + } } console.log(embeds); return { embeds, - } - - } + }; + }, }); diff --git a/src/backend/providers/superstream/index.ts b/src/backend/providers/superstream/index.ts index 0f20fb2d..8abed467 100644 --- a/src/backend/providers/superstream/index.ts +++ b/src/backend/providers/superstream/index.ts @@ -131,7 +131,8 @@ registerProvider({ const superstreamEntry = searchRes.find( (res: any) => - compareTitle(res.title, media.meta.title) && res.year === Number(media.meta.year) + compareTitle(res.title, media.meta.title) && + res.year === Number(media.meta.year) ); if (!superstreamEntry) throw new Error("No entry found on SuperStream"); diff --git a/src/components/Overlay.tsx b/src/components/Overlay.tsx new file mode 100644 index 00000000..243caedd --- /dev/null +++ b/src/components/Overlay.tsx @@ -0,0 +1,14 @@ +import { Helmet } from "react-helmet"; + +export function Overlay(props: { children: React.ReactNode }) { + return ( + <> + + + +
+ {props.children} +
+ + ); +} diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index ae332e8c..2b937549 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -67,7 +67,7 @@ export function SearchBarInput(props: SearchBarProps) { id: MWMediaType.SERIES, name: t("searchBar.series"), icon: Icons.CLAPPER_BOARD, - } + }, ]} onClick={() => setDropdownOpen((old) => !old)} > diff --git a/src/components/buttons/EditButton.tsx b/src/components/buttons/EditButton.tsx index 0b91e5ed..bcdd3cfd 100644 --- a/src/components/buttons/EditButton.tsx +++ b/src/components/buttons/EditButton.tsx @@ -10,7 +10,7 @@ export interface EditButtonProps { } export function EditButton(props: EditButtonProps) { - const { t } = useTranslation() + const { t } = useTranslation(); const [parent] = useAutoAnimate(); const onClick = useCallback(() => { @@ -24,7 +24,9 @@ export function EditButton(props: EditButtonProps) { > {props.editing ? ( - {t("media.stopEditing")} + + {t("media.stopEditing")} + ) : ( )} diff --git a/src/components/layout/ErrorBoundary.tsx b/src/components/layout/ErrorBoundary.tsx index a5bf4399..bde7c11d 100644 --- a/src/components/layout/ErrorBoundary.tsx +++ b/src/components/layout/ErrorBoundary.tsx @@ -36,12 +36,13 @@ interface ErrorMessageProps { } export function ErrorMessage(props: ErrorMessageProps) { - const { t } = useTranslation() + const { t } = useTranslation(); return (
diff --git a/src/components/layout/Modal.tsx b/src/components/layout/Modal.tsx new file mode 100644 index 00000000..fb787259 --- /dev/null +++ b/src/components/layout/Modal.tsx @@ -0,0 +1,25 @@ +import { Overlay } from "@/components/Overlay"; +import { ReactNode } from "react"; +import { createPortal } from "react-dom"; + +interface Props { + show: boolean; + children?: ReactNode; +} + +export function ModalFrame(props: { children?: ReactNode }) { + return {props.children}; +} + +export function Modal(props: Props) { + if (!props.show) return null; + return createPortal({props.children}, document.body); +} + +export function ModalCard(props: { children?: ReactNode }) { + return ( +
+ {props.children} +
+ ); +} diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index a67dba21..f4305eca 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -35,12 +35,14 @@ function MediaCardContent({ return (
{t("seasons.seasonAndEpisode", { season: series.season, - episode: series.episode + episode: series.episode, })}

@@ -62,12 +64,14 @@ function MediaCardContent({ {percentage !== undefined ? ( <>
@@ -83,8 +87,9 @@ function MediaCardContent({ ) : null}
diff --git a/src/index.tsx b/src/index.tsx index 3a8d7594..a6f88858 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -44,10 +44,9 @@ const LazyLoadedApp = React.lazy(async () => { function TheRouter(props: { children: ReactNode }) { const normalRouter = conf().NORMAL_ROUTER; - - if (normalRouter) - return - return + + if (normalRouter) return {props.children}; + return {props.children}; } ReactDOM.render( diff --git a/src/state/watched/migrations/v2.ts b/src/state/watched/migrations/v2.ts index 1d145968..0e9c52e8 100644 --- a/src/state/watched/migrations/v2.ts +++ b/src/state/watched/migrations/v2.ts @@ -49,11 +49,13 @@ async function getMetas( searchQuery: `${item.title} ${year}`, type: item.mediaType, }); - const relevantItem = data.find((res) => - yearsAreClose(Number(res.year), year) && compareTitle(res.title, item.title) + const relevantItem = data.find( + (res) => + yearsAreClose(Number(res.year), year) && + compareTitle(res.title, item.title) ); if (!relevantItem) { - console.error("No item found for migration: " + item.title); + console.error(`No item found for migration: ${item.title}`); return; } return { @@ -188,7 +190,10 @@ export async function migrateV2Videos(old: OldData) { }, progress: oldWatched.progress, percentage: oldWatched.percentage, - watchedAt: now + Number(oldWatched.seasonId) * 1000 + Number(oldWatched.episodeId), // There was no watchedAt in V2 + watchedAt: + now + + Number(oldWatched.seasonId) * 1000 + + Number(oldWatched.episodeId), // There was no watchedAt in V2 // JANK ALERT: Put watchedAt in the future to show last episode as most recently }; diff --git a/src/utils/titleMatch.ts b/src/utils/titleMatch.ts index cb69c790..dfdf3883 100644 --- a/src/utils/titleMatch.ts +++ b/src/utils/titleMatch.ts @@ -1,7 +1,11 @@ function normalizeTitle(title: string): string { - return title.trim().toLowerCase().replace(/[\'\"\:]/g, "").replace(/[^a-zA-Z0-9]+/g, "_"); + return title + .trim() + .toLowerCase() + .replace(/['":]/g, "") + .replace(/[^a-zA-Z0-9]+/g, "_"); } export function compareTitle(a: string, b: string): boolean { - return normalizeTitle(a) === normalizeTitle(b); + return normalizeTitle(a) === normalizeTitle(b); } diff --git a/src/video/components/actions/CaptionsSelectionAction.tsx b/src/video/components/actions/CaptionsSelectionAction.tsx index 96a13fdc..d6cc4328 100644 --- a/src/video/components/actions/CaptionsSelectionAction.tsx +++ b/src/video/components/actions/CaptionsSelectionAction.tsx @@ -11,7 +11,7 @@ interface Props { } export function CaptionsSelectionAction(props: Props) { - const { t } = useTranslation() + const { t } = useTranslation(); const descriptor = useVideoPlayerDescriptor(); const controls = useControls(descriptor); const { isMobile } = useIsMobile(); @@ -22,7 +22,7 @@ export function CaptionsSelectionAction(props: Props) { controls.openPopout("captions")} icon={Icons.CAPTIONS} diff --git a/src/video/components/actions/PageTitleAction.tsx b/src/video/components/actions/PageTitleAction.tsx index 171a06f7..21a2bf23 100644 --- a/src/video/components/actions/PageTitleAction.tsx +++ b/src/video/components/actions/PageTitleAction.tsx @@ -9,7 +9,9 @@ export function PageTitleAction() { if (!meta) return null; - const title = isSeries ? `${meta.meta.title} - ${humanizedEpisodeId}` : meta.meta.title; + const title = isSeries + ? `${meta.meta.title} - ${humanizedEpisodeId}` + : meta.meta.title; return ( diff --git a/src/video/components/actions/SeriesSelectionAction.tsx b/src/video/components/actions/SeriesSelectionAction.tsx index abf2082e..2a6b2b35 100644 --- a/src/video/components/actions/SeriesSelectionAction.tsx +++ b/src/video/components/actions/SeriesSelectionAction.tsx @@ -13,7 +13,7 @@ interface Props { } export function SeriesSelectionAction(props: Props) { - const { t } = useTranslation() + const { t } = useTranslation(); const descriptor = useVideoPlayerDescriptor(); const meta = useMeta(descriptor); const videoInterface = useInterface(descriptor); diff --git a/src/video/components/actions/SourceSelectionAction.tsx b/src/video/components/actions/SourceSelectionAction.tsx index 3058a6d2..66784da8 100644 --- a/src/video/components/actions/SourceSelectionAction.tsx +++ b/src/video/components/actions/SourceSelectionAction.tsx @@ -11,7 +11,7 @@ interface Props { } export function SourceSelectionAction(props: Props) { - const { t } = useTranslation() + const { t } = useTranslation(); const descriptor = useVideoPlayerDescriptor(); const videoInterface = useInterface(descriptor); const controls = useControls(descriptor); diff --git a/src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts b/src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts index 7275cc07..88ab2e31 100644 --- a/src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts +++ b/src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"; export function useCurrentSeriesEpisodeInfo(descriptor: string) { const meta = useMeta(descriptor); - const {t} = useTranslation() + const { t } = useTranslation(); const currentSeasonInfo = useMemo(() => { return meta?.seasons?.find( @@ -24,10 +24,10 @@ export function useCurrentSeriesEpisodeInfo(descriptor: string) { ); if (!isSeries) return { isSeries: false }; - + const humanizedEpisodeId = t("videoPlayer.seasonAndEpisode", { season: currentSeasonInfo?.number, - episode: currentEpisodeInfo?.number + episode: currentEpisodeInfo?.number, }); return { diff --git a/src/video/components/popouts/CaptionSelectionPopout.tsx b/src/video/components/popouts/CaptionSelectionPopout.tsx index 995b86ac..e5ecdaeb 100644 --- a/src/video/components/popouts/CaptionSelectionPopout.tsx +++ b/src/video/components/popouts/CaptionSelectionPopout.tsx @@ -15,7 +15,7 @@ function makeCaptionId(caption: MWCaption, isLinked: boolean): string { } export function CaptionSelectionPopout() { - const { t } = useTranslation() + const { t } = useTranslation(); const descriptor = useVideoPlayerDescriptor(); const meta = useMeta(descriptor); diff --git a/src/video/components/popouts/EpisodeSelectionPopout.tsx b/src/video/components/popouts/EpisodeSelectionPopout.tsx index de54b457..1f167731 100644 --- a/src/video/components/popouts/EpisodeSelectionPopout.tsx +++ b/src/video/components/popouts/EpisodeSelectionPopout.tsx @@ -11,14 +11,14 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useMeta } from "@/video/state/logic/meta"; import { useControls } from "@/video/state/logic/controls"; import { useWatchedContext } from "@/state/watched"; -import { PopoutListEntry, PopoutSection } from "./PopoutUtils"; import { useTranslation } from "react-i18next"; +import { PopoutListEntry, PopoutSection } from "./PopoutUtils"; export function EpisodeSelectionPopout() { const params = useParams<{ media: string; }>(); - const { t } = useTranslation() + const { t } = useTranslation(); const descriptor = useVideoPlayerDescriptor(); const meta = useMeta(descriptor); @@ -61,7 +61,7 @@ export function EpisodeSelectionPopout() { // race condition, jank solution but it works. setTimeout(() => { controls.setCurrentEpisode(seasonId, episodeId); - }, 100) + }, 100); }, [controls] ); @@ -141,15 +141,15 @@ export function EpisodeSelectionPopout() { > {currentSeasonInfo ? meta?.seasons?.map?.((season) => ( - setSeason(season.id)} - isOnDarkBackground - > - {season.title} - - )) + setSeason(season.id)} + isOnDarkBackground + > + {season.title} + + )) : "No season"} @@ -166,7 +166,7 @@ export function EpisodeSelectionPopout() { />

{t("videoPLayer.popouts.errors.loadingWentWrong", { - seasonTitle: currentSeasonInfo?.title?.toLowerCase() + seasonTitle: currentSeasonInfo?.title?.toLowerCase(), })}

@@ -175,29 +175,29 @@ export function EpisodeSelectionPopout() {
{currentSeasonEpisodes && currentSeasonInfo ? currentSeasonEpisodes.map((e) => ( - { - if (e.id === meta?.episode?.episodeId) - controls.closePopout(); - else setCurrent(currentSeasonInfo.id, e.id); - }} - percentageCompleted={ - watched.items.find( - (item) => - item.item?.series?.seasonId === - currentSeasonInfo.id && - item.item?.series?.episodeId === e.id - )?.percentage - } - > - {t("videoPlayer.popouts.episode", { - index: e.number, - title: e.title - })} - - )) + { + if (e.id === meta?.episode?.episodeId) + controls.closePopout(); + else setCurrent(currentSeasonInfo.id, e.id); + }} + percentageCompleted={ + watched.items.find( + (item) => + item.item?.series?.seasonId === + currentSeasonInfo.id && + item.item?.series?.episodeId === e.id + )?.percentage + } + > + {t("videoPlayer.popouts.episode", { + index: e.number, + title: e.title, + })} + + )) : "No episodes"}
)} diff --git a/src/video/components/popouts/SourceSelectionPopout.tsx b/src/video/components/popouts/SourceSelectionPopout.tsx index 81f783b5..70e78a1b 100644 --- a/src/video/components/popouts/SourceSelectionPopout.tsx +++ b/src/video/components/popouts/SourceSelectionPopout.tsx @@ -7,12 +7,15 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useMeta } from "@/video/state/logic/meta"; import { useControls } from "@/video/state/logic/controls"; import { MWStream } from "@/backend/helpers/streams"; -import { getEmbedScraperByType, getProviders } from "@/backend/helpers/register"; +import { + getEmbedScraperByType, + getProviders, +} from "@/backend/helpers/register"; import { runEmbedScraper, runProvider } from "@/backend/helpers/run"; import { MWProviderScrapeResult } from "@/backend/helpers/provider"; -import { PopoutListEntry, PopoutSection } from "./PopoutUtils"; import { useTranslation } from "react-i18next"; import { MWEmbed, MWEmbedType } from "@/backend/helpers/embed"; +import { PopoutListEntry, PopoutSection } from "./PopoutUtils"; interface EmbedEntryProps { name: string; @@ -24,35 +27,39 @@ interface EmbedEntryProps { export function EmbedEntry(props: EmbedEntryProps) { const [scrapeEmbed, loading, error] = useLoading(async () => { const scraper = getEmbedScraperByType(props.type); - if (!scraper) throw new Error("Embed scraper not found") + if (!scraper) throw new Error("Embed scraper not found"); const stream = await runEmbedScraper(scraper, { - progress: () => { }, // no progress tracking for inline scraping + progress: () => {}, // no progress tracking for inline scraping url: props.url, - }) + }); props.onSelect(stream); }); - return ( { - scrapeEmbed(); - }} - > - {props.name} - ) + return ( + { + scrapeEmbed(); + }} + > + {props.name} + + ); } export function SourceSelectionPopout() { - const { t } = useTranslation() + const { t } = useTranslation(); const descriptor = useVideoPlayerDescriptor(); const controls = useControls(descriptor); const meta = useMeta(descriptor); const providers = useMemo( () => - meta ? getProviders().filter((v) => v.type.includes(meta.meta.meta.type)) : [], + meta + ? getProviders().filter((v) => v.type.includes(meta.meta.meta.type)) + : [], [meta] ); @@ -71,7 +78,7 @@ export function SourceSelectionPopout() { if (!meta) throw new Error("need meta"); return runProvider(theProvider, { media: meta.meta, - progress: () => { }, + progress: () => {}, type: meta.meta.meta.type, episode: meta.episode?.episodeId as any, season: meta.episode?.seasonId as any, @@ -110,13 +117,13 @@ export function SourceSelectionPopout() { const realStream = v.stream; if (!realStream) { const embed = v?.embeds[0]; - if (!embed) throw new Error("Embed scraper not found") + if (!embed) throw new Error("Embed scraper not found"); const scraper = getEmbedScraperByType(embed.type); - if (!scraper) throw new Error("Embed scraper not found") + if (!scraper) throw new Error("Embed scraper not found"); const stream = await runEmbedScraper(scraper, { - progress: () => { }, // no progress tracking for inline scraping + progress: () => {}, // no progress tracking for inline scraping url: embed.url, - }) + }); selectSource(stream); return; } @@ -142,28 +149,30 @@ export function SourceSelectionPopout() { const embeds = scrapeResult?.embeds || []; // Count embed types to determine if it should show a number behind the name - const embedsPerType: Record = {} + const embedsPerType: Record = + {}; for (const embed of embeds) { if (!embed.type) continue; if (!embedsPerType[embed.type]) embedsPerType[embed.type] = []; embedsPerType[embed.type].push({ ...embed, - displayName: embed.type - }) + displayName: embed.type, + }); } - const embedsRes = Object.entries(embedsPerType).flatMap(([type, entries]) => { - if (entries.length > 1) return entries.map((embed, i) => ({ - ...embed, - displayName: `${embed.type} ${i + 1}` - })) + const embedsRes = Object.entries(embedsPerType).flatMap(([_, entries]) => { + if (entries.length > 1) + return entries.map((embed, i) => ({ + ...embed, + displayName: `${embed.type} ${i + 1}`, + })); return entries; - }) + }); - console.log(embedsRes) + console.log(embedsRes); return embedsRes; - }, [scrapeResult?.embeds]) + }, [scrapeResult?.embeds]); return ( <> @@ -234,27 +243,31 @@ export function SourceSelectionPopout() { Native source ) : null} - {(visibleEmbeds?.length || 0) > 0 ? visibleEmbeds?.map((v) => ( - { - selectSource(stream); - }} - /> - )) : (
-
- 0 ? ( + visibleEmbeds?.map((v) => ( + { + selectSource(stream); + }} /> -

- {t("videoPlayer.popouts.noEmbeds")} -

+ )) + ) : ( +
+
+ +

+ {t("videoPlayer.popouts.noEmbeds")} +

+
-
)} + )} )} diff --git a/src/video/state/types.ts b/src/video/state/types.ts index 5686af1d..6b04a55a 100644 --- a/src/video/state/types.ts +++ b/src/video/state/types.ts @@ -4,7 +4,6 @@ import { MWStreamType, } from "@/backend/helpers/streams"; import { DetailedMeta } from "@/backend/metadata/getmeta"; -import { MWMediaMeta } from "@/backend/metadata/types"; import Hls from "hls.js"; import { VideoPlayerStateProvider } from "./providers/providerTypes"; diff --git a/src/views/media/MediaErrorView.tsx b/src/views/media/MediaErrorView.tsx index 140a3dbe..5dc634f4 100644 --- a/src/views/media/MediaErrorView.tsx +++ b/src/views/media/MediaErrorView.tsx @@ -1,14 +1,11 @@ -import { MWMediaMeta } from "@/backend/metadata/types"; import { ErrorMessage } from "@/components/layout/ErrorBoundary"; -import { Link } from "@/components/text/Link"; import { useGoBack } from "@/hooks/useGoBack"; -import { conf } from "@/setup/config"; import { VideoPlayerHeader } from "@/video/components/parts/VideoPlayerHeader"; import { Helmet } from "react-helmet"; -import { Trans, useTranslation } from "react-i18next"; +import { useTranslation } from "react-i18next"; export function MediaFetchErrorView() { - const { t } = useTranslation() + const { t } = useTranslation(); const goBack = useGoBack(); return ( @@ -20,9 +17,7 @@ export function MediaFetchErrorView() {
-

- {t("media.errors.mediaFailed")} -

+

{t("media.errors.mediaFailed")}

); diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index 242c73ea..7ab7ebee 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -19,13 +19,13 @@ import { ProgressListenerController } from "@/video/components/controllers/Progr import { VideoPlayerMeta } from "@/video/state/types"; import { SeriesController } from "@/video/components/controllers/SeriesController"; import { useWatchedItem } from "@/state/watched"; +import { useTranslation } from "react-i18next"; import { MediaFetchErrorView } from "./MediaErrorView"; import { MediaScrapeLog } from "./MediaScrapeLog"; import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView"; -import { useTranslation } from "react-i18next"; function MediaViewLoading(props: { onGoBack(): void }) { - const { t } = useTranslation() + const { t } = useTranslation(); return (
@@ -37,7 +37,9 @@ function MediaViewLoading(props: { onGoBack(): void }) {
-

{t("videoPlayer.findingBestVideo")}

+

+ {t("videoPlayer.findingBestVideo")} +

); @@ -51,7 +53,7 @@ interface MediaViewScrapingProps { } function MediaViewScraping(props: MediaViewScrapingProps) { const { eventLog, stream, pending } = useScrape(props.meta, props.selected); - const { t } = useTranslation() + const { t } = useTranslation(); useEffect(() => { if (stream) { @@ -78,14 +80,13 @@ function MediaViewScraping(props: MediaViewScrapingProps) { ) : ( <> -

- {t("videoPlayer.noVideos")} -

+

{t("videoPlayer.noVideos")}

)}
diff --git a/src/views/notfound/NotFoundView.tsx b/src/views/notfound/NotFoundView.tsx index a1dfcab0..7061e039 100644 --- a/src/views/notfound/NotFoundView.tsx +++ b/src/views/notfound/NotFoundView.tsx @@ -13,7 +13,7 @@ export function NotFoundWrapper(props: { children?: ReactNode; video?: boolean; }) { - const { t } = useTranslation() + const { t } = useTranslation(); const goBack = useGoBack(); return ( diff --git a/src/views/other/v2Migration.tsx b/src/views/other/v2Migration.tsx index f97c27f7..483b6f77 100644 --- a/src/views/other/v2Migration.tsx +++ b/src/views/other/v2Migration.tsx @@ -2,60 +2,66 @@ import { useEffect, useState } from "react"; import pako from "pako"; function fromBinary(str: string): Uint8Array { - let result = new Uint8Array(str.length); - [...str].forEach((char, i) => { - result[i] = char.charCodeAt(0); - }); - return result; + const result = new Uint8Array(str.length); + [...str].forEach((char, i) => { + result[i] = char.charCodeAt(0); + }); + return result; } - export function V2MigrationView() { - const [done, setDone] = useState(false); - useEffect(() => { - const params = new URLSearchParams(window.location.search ?? ""); - if (!params.has("m-time") || !params.has("m-data")) { - // migration params missing, just redirect - setDone(true); - return; - } - - const data = JSON.parse(pako.inflate(fromBinary(atob(params.get("m-data") as string)), { to: "string" })); - const timeOfMigration = new Date(params.get("m-time") as string); - - const savedTime = localStorage.getItem("mw-migration-date"); - if (savedTime) { - if (new Date(savedTime) >= timeOfMigration) { - // has already migrated this or something newer, skip - setDone(true); - return; - } - } - - // restore migration data - if (data.bookmarks) - localStorage.setItem("mw-bookmarks", JSON.stringify(data.bookmarks)) - if (data.videoProgress) - localStorage.setItem("video-progress", JSON.stringify(data.videoProgress)) - localStorage.setItem("mw-migration-date", timeOfMigration.toISOString()) - - // finished + const [done, setDone] = useState(false); + useEffect(() => { + const params = new URLSearchParams(window.location.search ?? ""); + if (!params.has("m-time") || !params.has("m-data")) { + // migration params missing, just redirect + setDone(true); + return; + } + + const data = JSON.parse( + pako.inflate(fromBinary(atob(params.get("m-data") as string)), { + to: "string", + }) + ); + const timeOfMigration = new Date(params.get("m-time") as string); + + const savedTime = localStorage.getItem("mw-migration-date"); + if (savedTime) { + if (new Date(savedTime) >= timeOfMigration) { + // has already migrated this or something newer, skip setDone(true); - }, []) - - // redirect when done - useEffect(() => { - if (!done) return; - const newUrl = new URL(window.location.href); + return; + } + } + + // restore migration data + if (data.bookmarks) + localStorage.setItem("mw-bookmarks", JSON.stringify(data.bookmarks)); + if (data.videoProgress) + localStorage.setItem( + "video-progress", + JSON.stringify(data.videoProgress) + ); + localStorage.setItem("mw-migration-date", timeOfMigration.toISOString()); + + // finished + setDone(true); + }, []); + + // redirect when done + useEffect(() => { + if (!done) return; + const newUrl = new URL(window.location.href); - const newParams = [] as string[]; - newUrl.searchParams.forEach((_, key)=>newParams.push(key)); - newParams.forEach(v => newUrl.searchParams.delete(v)) + const newParams = [] as string[]; + newUrl.searchParams.forEach((_, key) => newParams.push(key)); + newParams.forEach((v) => newUrl.searchParams.delete(v)); - newUrl.hash = ""; + newUrl.hash = ""; - window.location.href = newUrl.toString(); - }, [done]) + window.location.href = newUrl.toString(); + }, [done]); - return null; + return null; } diff --git a/src/views/search/SearchLoadingView.tsx b/src/views/search/SearchLoadingView.tsx index 1ae0d89c..307ed428 100644 --- a/src/views/search/SearchLoadingView.tsx +++ b/src/views/search/SearchLoadingView.tsx @@ -1,17 +1,18 @@ import { useTranslation } from "react-i18next"; import { Loading } from "@/components/layout/Loading"; -import { MWQuery } from "@/backend/metadata/types"; import { useSearchQuery } from "@/hooks/useSearchQuery"; export function SearchLoadingView() { const { t } = useTranslation(); - const [query] = useSearchQuery() + const [query] = useSearchQuery(); return ( - <> - - + ); } diff --git a/src/views/search/SearchView.tsx b/src/views/search/SearchView.tsx index b65892f8..4201d954 100644 --- a/src/views/search/SearchView.tsx +++ b/src/views/search/SearchView.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useState } from "react"; import Sticky from "react-stickynode"; import { useTranslation } from "react-i18next"; import { Navigation } from "@/components/layout/Navigation"; diff --git a/vite.config.ts b/vite.config.ts index 72f5bf13..3946b798 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react-swc"; import loadVersion from "vite-plugin-package-version"; -import checker from 'vite-plugin-checker' +import checker from "vite-plugin-checker"; import path from "path"; export default defineConfig({ @@ -10,7 +10,14 @@ export default defineConfig({ loadVersion(), checker({ typescript: true, // check typescript build errors in dev server - }) + eslint: { + // check lint errors in dev server + lintCommand: "eslint --ext .tsx,.ts src", + dev: { + logLevel: ["error"], + }, + }, + }), ], resolve: { alias: { From aaf0b56ee754b2ad7ac58790daa7566a90262afd Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sun, 19 Feb 2023 18:36:53 +0100 Subject: [PATCH 128/135] new domain popup start Co-authored-by: Jip Frijlink --- src/components/Button.tsx | 27 ++++++++ src/components/layout/Modal.tsx | 2 +- src/setup/index.css | 5 ++ src/setup/locales/en/translation.json | 6 ++ src/views/search/HomeView.tsx | 95 ++++++++++++++++----------- 5 files changed, 95 insertions(+), 40 deletions(-) create mode 100644 src/components/Button.tsx diff --git a/src/components/Button.tsx b/src/components/Button.tsx new file mode 100644 index 00000000..49343b6f --- /dev/null +++ b/src/components/Button.tsx @@ -0,0 +1,27 @@ +import { Icon, Icons } from "@/components/Icon"; +import { ReactNode } from "react"; + +interface Props { + icon?: Icons; + onClick?: () => void; + children?: ReactNode; +} + +// TODO style button +// TODO transition modal +export function Button(props: Props) { + return ( + + ); +} diff --git a/src/components/layout/Modal.tsx b/src/components/layout/Modal.tsx index fb787259..17f0c4d2 100644 --- a/src/components/layout/Modal.tsx +++ b/src/components/layout/Modal.tsx @@ -18,7 +18,7 @@ export function Modal(props: Props) { export function ModalCard(props: { children?: ReactNode }) { return ( -
+
{props.children}
); diff --git a/src/setup/index.css b/src/setup/index.css index c6ca4617..79bd1613 100644 --- a/src/setup/index.css +++ b/src/setup/index.css @@ -13,6 +13,11 @@ html[data-full], html[data-full] body { overscroll-behavior-y: none; } +body[data-no-scroll] { + overflow-y: hidden; + height: 100vh; +} + #root { padding: 0.05px; min-height: 100vh; diff --git a/src/setup/locales/en/translation.json b/src/setup/locales/en/translation.json index d29abc38..811e33b7 100644 --- a/src/setup/locales/en/translation.json +++ b/src/setup/locales/en/translation.json @@ -77,5 +77,11 @@ "errors": { "fatalError": "The video player encounted a fatal error, please report it to the <0>Discord server or on <1>GitHub." } + }, + "v3": { + "newSiteTitle": "We have a new site!", + "newDomain": "https://movie-web.app", + "newDomainText": "We've moved from domain, you can now access our website on <0>https://movie-web.app. Make sure to change all your bookmarks as <1>the old link will stop working on 25 Febuary 2023.", + "tireless": "We've worked tirelessly on this new update, we hope you will enjoy what we've been cooking up for the past months." } } diff --git a/src/views/search/HomeView.tsx b/src/views/search/HomeView.tsx index 5f730b92..334486f3 100644 --- a/src/views/search/HomeView.tsx +++ b/src/views/search/HomeView.tsx @@ -1,5 +1,5 @@ -import { useTranslation } from "react-i18next"; -import { Icons } from "@/components/Icon"; +import { Trans, useTranslation } from "react-i18next"; +import { Icon, Icons } from "@/components/Icon"; import { SectionHeading } from "@/components/layout/SectionHeading"; import { MediaGrid } from "@/components/media/MediaGrid"; import { @@ -9,10 +9,11 @@ import { import { useWatchedContext } from "@/state/watched"; import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; import { EditButton } from "@/components/buttons/EditButton"; -import { useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { useAutoAnimate } from "@formkit/auto-animate/react"; -import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconButton"; -import { useHistory, useLocation } from "react-router-dom"; +import { useHistory } from "react-router-dom"; +import { Modal, ModalCard } from "@/components/layout/Modal"; +import { Button } from "@/components/Button"; function Bookmarks() { const { t } = useTranslation(); @@ -81,48 +82,64 @@ function Watched() { ); } -function NewDomainInfo() { - const location = useLocation(); +function NewDomainModal() { + const [show, setShow] = useState( + new URLSearchParams(window.location.search).get("migrated") === "1" + ); const history = useHistory(); + const { t } = useTranslation(); + useEffect(() => { + const newParams = new URLSearchParams(history.location.search); + newParams.delete("migrated"); + history.replace({ + search: newParams.toString(), + }); + }, [history]); + + // Hi Isra! (TODO remove this in the future lol) return ( -
- { - const queryParams = new URLSearchParams(location.search); - queryParams.delete("redirected"); - history.replace({ - search: queryParams.toString(), - }); - }} - /> -

Hey there!

-

- Welcome to the long-awaited shiny new update of movie-web. This awesome - updates includes an awesome new look, updated functionality, and even a - fully custom-built video player. -

-

- We also have a new domain! Please be sure to update your bookmarks, as - the old domain is going to stop working on May 31st. - The new domain is movie-web.app -

-
+ + +
+
+
+
+ {t("v3.newDomain")} +
+
+
+
+

+ {t("v3.newSiteTitle")} +

+

+ + + + +

+

{t("v3.tireless")}

+
+
+ +
+ + ); } export function HomeView() { - const location = useLocation(); - - const showNewDomainInfo = useMemo(() => { - return location.search.includes("redirected=1"); - }, [location.search]); - return ( -
- {showNewDomainInfo ? : ""} +
+
From 0c57aa1a73aa5629156d40470e68a5404b57a32b Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sun, 19 Feb 2023 19:54:34 +0100 Subject: [PATCH 129/135] finalized domain redirect modal Co-authored-by: Jip Frijlink Co-authored-by: James Hawkins --- src/components/Button.tsx | 6 ++-- src/components/Overlay.tsx | 8 ++++- src/components/Transition.tsx | 25 +++++++++++++-- src/components/layout/Modal.tsx | 31 +++++++++++++++--- src/setup/locales/en/translation.json | 4 +-- src/views/other/v2Migration.tsx | 9 +++++- src/views/search/HomeView.tsx | 45 +++++++++++++++++++++++---- 7 files changed, 106 insertions(+), 22 deletions(-) diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 49343b6f..9a74f84d 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -7,17 +7,15 @@ interface Props { children?: ReactNode; } -// TODO style button -// TODO transition modal export function Button(props: Props) { return (
); -} +}); From b886443ea72e5cc6ab66c1ea207751c0bc2640b5 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sun, 19 Feb 2023 22:22:01 +0100 Subject: [PATCH 131/135] bunch of chromecast fixes Co-authored-by: Jip Frijlink Co-authored-by: James Hawkins Co-authored-by: William Oldham --- src/components/Transition.tsx | 14 ++-------- src/components/layout/Modal.tsx | 2 -- src/setup/locales/en/translation.json | 10 ++++--- src/video/components/VideoPlayer.tsx | 4 +++ .../components/actions/CastingTextAction.tsx | 22 +++++++++++++++ .../components/internal/CastingInternal.tsx | 8 ++++-- .../internal/VideoElementInternal.tsx | 6 ++++- .../state/providers/castingStateProvider.ts | 27 ++++++++++--------- src/views/search/HomeView.tsx | 2 +- 9 files changed, 61 insertions(+), 34 deletions(-) create mode 100644 src/video/components/actions/CastingTextAction.tsx diff --git a/src/components/Transition.tsx b/src/components/Transition.tsx index 8406a19b..cda3b945 100644 --- a/src/components/Transition.tsx +++ b/src/components/Transition.tsx @@ -12,7 +12,6 @@ interface Props { animation: TransitionAnimations; className?: string; children?: ReactNode; - appearOnMount?: boolean; isChild?: boolean; } @@ -62,23 +61,14 @@ export function Transition(props: Props) { if (props.isChild) { return ( - +
{props.children}
); } return ( - +
{props.children}
); diff --git a/src/components/layout/Modal.tsx b/src/components/layout/Modal.tsx index bd5b9c47..7a1c3b64 100644 --- a/src/components/layout/Modal.tsx +++ b/src/components/layout/Modal.tsx @@ -13,13 +13,11 @@ export function ModalFrame(props: Props) { diff --git a/src/setup/locales/en/translation.json b/src/setup/locales/en/translation.json index 8d80a1dc..8842b58f 100644 --- a/src/setup/locales/en/translation.json +++ b/src/setup/locales/en/translation.json @@ -79,9 +79,13 @@ } }, "v3": { - "newSiteTitle": "Version 3 has released!", + "newSiteTitle": "New version now released!", "newDomain": "https://movie-web.app", - "newDomainText": "We have a new domain. You can now access our website on <0>https://movie-web.app. Make sure to update all your bookmarks as <1>the old link will stop working on {{date}}.", - "tireless": "We've worked tirelessly on this new update, we hope you will enjoy what we've been cooking up for the past months." + "newDomainText": "movie-web will soon be moving to a new domain: <0>https://movie-web.app. Make sure to update all your bookmarks as <1>the old website will stop working on {{date}}.", + "tireless": "We've worked tirelessly on this new update, we hope you will enjoy what we've been cooking up for the past months.", + "leaveAnnouncement": "Take me there!" + }, + "casting": { + "casting": "Casting to device..." } } diff --git a/src/video/components/VideoPlayer.tsx b/src/video/components/VideoPlayer.tsx index 5b9e7dac..688f42e2 100644 --- a/src/video/components/VideoPlayer.tsx +++ b/src/video/components/VideoPlayer.tsx @@ -29,6 +29,7 @@ import { useControls } from "@/video/state/logic/controls"; 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"; type Props = VideoPlayerBaseProps; @@ -94,6 +95,9 @@ export function VideoPlayer(props: Props) { + + + diff --git a/src/video/components/actions/CastingTextAction.tsx b/src/video/components/actions/CastingTextAction.tsx new file mode 100644 index 00000000..d95a0351 --- /dev/null +++ b/src/video/components/actions/CastingTextAction.tsx @@ -0,0 +1,22 @@ +import { Icon, Icons } from "@/components/Icon"; +import { useVideoPlayerDescriptor } from "@/video/state/hooks"; +import { useMisc } from "@/video/state/logic/misc"; +import { useTranslation } from "react-i18next"; + +export function CastingTextAction() { + const { t } = useTranslation(); + + const descriptor = useVideoPlayerDescriptor(); + const misc = useMisc(descriptor); + + if (!misc.isCasting) return null; + + return ( +
+
+ +
+

{t("casting.casting")}

+
+ ); +} diff --git a/src/video/components/internal/CastingInternal.tsx b/src/video/components/internal/CastingInternal.tsx index 72979083..9de8c1b1 100644 --- a/src/video/components/internal/CastingInternal.tsx +++ b/src/video/components/internal/CastingInternal.tsx @@ -16,13 +16,17 @@ export function CastingInternal() { useEffect(() => { if (lastValue.current === isCasting) return; - if (!isCasting) return; lastValue.current = isCasting; + if (!isCasting) return; const provider = createCastingStateProvider(descriptor); setProvider(descriptor, provider); const { destroy } = provider.providerStart(); return () => { - unsetStateProvider(descriptor, provider.getId()); + try { + unsetStateProvider(descriptor, provider.getId()); + } catch { + // ignore errors from missing player state, we need to run destroy()! + } destroy(); }; }, [descriptor, isCasting]); diff --git a/src/video/components/internal/VideoElementInternal.tsx b/src/video/components/internal/VideoElementInternal.tsx index f819bf8f..335e8a9f 100644 --- a/src/video/components/internal/VideoElementInternal.tsx +++ b/src/video/components/internal/VideoElementInternal.tsx @@ -27,7 +27,11 @@ function VideoElement(props: Props) { setProvider(descriptor, provider); const { destroy } = provider.providerStart(); return () => { - unsetStateProvider(descriptor, provider.getId()); + try { + unsetStateProvider(descriptor, provider.getId()); + } catch { + // ignore errors from missing player state, we need to run destroy()! + } destroy(); }; }, [descriptor, initalized, stateProviderId]); diff --git a/src/video/state/providers/castingStateProvider.ts b/src/video/state/providers/castingStateProvider.ts index c2f2432a..d78190c1 100644 --- a/src/video/state/providers/castingStateProvider.ts +++ b/src/video/state/providers/castingStateProvider.ts @@ -18,9 +18,7 @@ import { VideoPlayerStateProvider } from "./providerTypes"; import { updateProgress } from "../logic/progress"; // TODO startAt when switching state providers -// TODO cast -> uncast -> cast will break -// TODO chromecast button has incorrect hitbox and badly styled -// TODO casting text middle of screen +// TODO test HLS export function createCastingStateProvider( descriptor: string ): VideoPlayerStateProvider { @@ -112,8 +110,10 @@ export function createCastingStateProvider( const movieMeta = new chrome.cast.media.MovieMediaMetadata(); movieMeta.title = state.meta?.meta.meta.title ?? ""; - // TODO contentId? - const mediaInfo = new chrome.cast.media.MediaInfo("hello", "video/mp4"); + const mediaInfo = new chrome.cast.media.MediaInfo( + state.meta?.meta.meta.id ?? "hello", + "video/mp4" + ); (mediaInfo as any).contentUrl = source?.source; mediaInfo.streamType = chrome.cast.media.StreamType.BUFFERED; mediaInfo.metadata = movieMeta; @@ -167,17 +167,16 @@ export function createCastingStateProvider( updateProgress(descriptor, state); break; case "mediaInfo": - state.progress.duration = e.value.duration; - updateProgress(descriptor, state); + if (e.value) { + state.progress.duration = e.value.duration; + updateProgress(descriptor, state); + } break; case "playerState": state.mediaPlaying.isLoading = e.value === "BUFFERING"; - updateMediaPlaying(descriptor, state); - break; - case "isPaused": - state.mediaPlaying.isPaused = e.value; - state.mediaPlaying.isPlaying = !e.value; - if (!e.value) state.mediaPlaying.hasPlayedOnce = true; + state.mediaPlaying.isPaused = e.value !== "PLAYING"; + state.mediaPlaying.isPlaying = e.value === "PLAYING"; + if (e.value === "PLAYING") state.mediaPlaying.hasPlayedOnce = true; updateMediaPlaying(descriptor, state); break; case "isMuted": @@ -188,6 +187,7 @@ export function createCastingStateProvider( case "displayStatus": case "canSeek": case "title": + case "isPaused": break; default: console.log(e.type, e.field, e.value); @@ -229,6 +229,7 @@ export function createCastingStateProvider( state.wrapperElement?.removeEventListener("mouseenter", isFocused); state.wrapperElement?.removeEventListener("mouseleave", isFocused); fscreen.removeEventListener("fullscreenchange", fullscreenchange); + ins?.endCurrentSession(true); }, }; }, diff --git a/src/views/search/HomeView.tsx b/src/views/search/HomeView.tsx index f9494f3b..6d1b6795 100644 --- a/src/views/search/HomeView.tsx +++ b/src/views/search/HomeView.tsx @@ -161,7 +161,7 @@ function NewDomainModal() {
From 398644951e6b4e412ac775f48b24b193115c6487 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sun, 19 Feb 2023 22:55:58 +0100 Subject: [PATCH 132/135] more chromecast fixes Co-authored-by: Jip Frijlink Co-authored-by: James Hawkins Co-authored-by: William Oldham --- .../controllers/ProgressListenerController.tsx | 13 +++++++++++++ src/video/state/providers/castingStateProvider.ts | 5 ++++- src/video/state/providers/utils.ts | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/video/components/controllers/ProgressListenerController.tsx b/src/video/components/controllers/ProgressListenerController.tsx index 2739f412..1a1d2bd1 100644 --- a/src/video/components/controllers/ProgressListenerController.tsx +++ b/src/video/components/controllers/ProgressListenerController.tsx @@ -4,6 +4,7 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useMediaPlaying } from "@/video/state/logic/mediaplaying"; import { useProgress } from "@/video/state/logic/progress"; import { useControls } from "@/video/state/logic/controls"; +import { useMisc } from "@/video/state/logic/misc"; interface Props { startAt?: number; @@ -15,6 +16,7 @@ export function ProgressListenerController(props: Props) { const mediaPlaying = useMediaPlaying(descriptor); const progress = useProgress(descriptor); const controls = useControls(descriptor); + const misc = useMisc(descriptor); const didInitialize = useRef(null); const lastTime = useRef(props.startAt ?? 0); @@ -46,6 +48,17 @@ export function ProgressListenerController(props: Props) { didInitialize.current = true; }, [didInitialize, props, progress, mediaPlaying, controls]); + // when switching state providers + // TODO stateProviderId is somehow ALWAYS "video" + const lastStateProviderId = useRef(null); + const stateProviderId = useMemo(() => misc.stateProviderId, [misc]); + useEffect(() => { + if (lastStateProviderId.current === stateProviderId) return; + if (mediaPlaying.isFirstLoading) return; + lastStateProviderId.current = stateProviderId; + controls.setTime(lastTime.current); + }, [controls, mediaPlaying, stateProviderId]); + useEffect(() => { // if it initialized, but media starts loading for the first time again. // reset initalized so it will restore time again diff --git a/src/video/state/providers/castingStateProvider.ts b/src/video/state/providers/castingStateProvider.ts index d78190c1..6c7c66d5 100644 --- a/src/video/state/providers/castingStateProvider.ts +++ b/src/video/state/providers/castingStateProvider.ts @@ -176,7 +176,10 @@ export function createCastingStateProvider( state.mediaPlaying.isLoading = e.value === "BUFFERING"; state.mediaPlaying.isPaused = e.value !== "PLAYING"; state.mediaPlaying.isPlaying = e.value === "PLAYING"; - if (e.value === "PLAYING") state.mediaPlaying.hasPlayedOnce = true; + if (e.value === "PLAYING") { + state.mediaPlaying.hasPlayedOnce = true; + state.mediaPlaying.isFirstLoading = false; + } updateMediaPlaying(descriptor, state); break; case "isMuted": diff --git a/src/video/state/providers/utils.ts b/src/video/state/providers/utils.ts index 273d9fff..9d5d47e5 100644 --- a/src/video/state/providers/utils.ts +++ b/src/video/state/providers/utils.ts @@ -26,11 +26,11 @@ export function unsetStateProvider( !state.stateProvider || state.stateProvider?.getId() !== stateProviderId ) { - state.stateProviderId = "video"; // go back to video when casting stops return; } state.stateProvider = null; state.stateProviderId = "video"; // go back to video when casting stops + updateMisc(descriptor, state); } export function handleBuffered(time: number, buffered: TimeRanges): number { From a2e647297a872275c32df48eef264d0b0de5005c Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sun, 19 Feb 2023 23:03:16 +0100 Subject: [PATCH 133/135] cleanup todos Co-authored-by: Jip Frijlink Co-authored-by: James Hawkins Co-authored-by: William Oldham --- src/index.tsx | 16 ---------------- src/state/watched/migrations/v2.ts | 1 - .../state/providers/castingStateProvider.ts | 4 +--- 3 files changed, 1 insertion(+), 20 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index a6f88858..8de72d2c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -19,22 +19,6 @@ if (key) { } initializeChromecast(); -// TODO video todos: -// - chrome cast support -// - bug: safari fullscreen will make video overlap player controls - -// TODO stuff to test: -// - browser: firefox, chrome, edge, safari desktop -// - phones: android firefox, android chrome, iphone safari -// - devices: ipadOS -// - HLS -// - HLS error handling -// - video player error handling - -// TODO backend system: -// - implement jons providers/embedscrapers -// - AFTER all that: rank providers/embedscrapers - const LazyLoadedApp = React.lazy(async () => { await initializeStores(); return { diff --git a/src/state/watched/migrations/v2.ts b/src/state/watched/migrations/v2.ts index 0e9c52e8..de3dad44 100644 --- a/src/state/watched/migrations/v2.ts +++ b/src/state/watched/migrations/v2.ts @@ -70,7 +70,6 @@ async function getMetas( let keys: (string | null)[][] = [["0", "0"]]; if (item.data.type === "series") { - // TODO sort episodes by season & episode so it shows the "highest" episode as last const meta = await getMetaFromId(item.data.type, item.data.id); if (!meta || !meta?.meta.seasons) return; const seasonNumbers = [ diff --git a/src/video/state/providers/castingStateProvider.ts b/src/video/state/providers/castingStateProvider.ts index 6c7c66d5..e3b56dfb 100644 --- a/src/video/state/providers/castingStateProvider.ts +++ b/src/video/state/providers/castingStateProvider.ts @@ -17,8 +17,7 @@ import { updateMediaPlaying } from "../logic/mediaplaying"; import { VideoPlayerStateProvider } from "./providerTypes"; import { updateProgress } from "../logic/progress"; -// TODO startAt when switching state providers -// TODO test HLS +// TODO HLS for casting? export function createCastingStateProvider( descriptor: string ): VideoPlayerStateProvider { @@ -184,7 +183,6 @@ export function createCastingStateProvider( break; case "isMuted": state.mediaPlaying.volume = e.value ? 1 : 0; - // TODO better mute handling updateMediaPlaying(descriptor, state); break; case "displayStatus": From 68a186963c2f015cdeddfd6723538eac32a6edb0 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sun, 19 Feb 2023 23:04:30 +0100 Subject: [PATCH 134/135] add netlify support Co-authored-by: Jip Frijlink Co-authored-by: James Hawkins Co-authored-by: William Oldham --- public/_redirects | 1 + 1 file changed, 1 insertion(+) create mode 100644 public/_redirects diff --git a/public/_redirects b/public/_redirects new file mode 100644 index 00000000..7797f7c6 --- /dev/null +++ b/public/_redirects @@ -0,0 +1 @@ +/* /index.html 200 From a8c84f7343c9c51ec21c6bceaa0c0672723d7caa Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sun, 19 Feb 2023 23:06:10 +0100 Subject: [PATCH 135/135] remove github pages deployment Co-authored-by: Jip Frijlink Co-authored-by: James Hawkins Co-authored-by: William Oldham --- .github/workflows/deploying.yml | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/.github/workflows/deploying.yml b/.github/workflows/deploying.yml index 12e9a87c..3d4a06f4 100644 --- a/.github/workflows/deploying.yml +++ b/.github/workflows/deploying.yml @@ -31,30 +31,6 @@ jobs: name: production-files path: ./dist - deploy: - name: Deploy - needs: build - runs-on: ubuntu-latest - - steps: - - name: Download artifact - uses: actions/download-artifact@v2 - with: - name: production-files - path: ./dist - - - name: Insert config - env: - DEPLOY_CONFIG: ${{ secrets.DEPLOY_CONFIG }} - run: echo "$DEPLOY_CONFIG" > ./dist/config.js - - - name: Deploy to gh-pages - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./dist - cname: movie.squeezebox.dev - release: name: Release needs: build