Merge pull request #376 from castdrian/multi-search-opensearch

feat: multisearch & opensearch & sitelinks
This commit is contained in:
mrjvs 2023-07-23 12:39:03 +02:00 committed by GitHub
commit 17ff003651
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 466 additions and 389 deletions

View File

@ -8,26 +8,26 @@ 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",
"plugin:prettier/recommended",
"plugin:prettier/recommended"
],
ignorePatterns: ["public/*", "dist/*", "/*.js", "/*.ts"],
ignorePatterns: ["public/*", "dist/*", "/*.js", "/*.ts", "/plugins/*.ts"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: "./tsconfig.json",
tsconfigRootDir: "./",
tsconfigRootDir: "./"
},
settings: {
"import/resolver": {
typescript: {
project: "./tsconfig.json",
},
},
project: "./tsconfig.json"
}
}
},
plugins: ["@typescript-eslint", "import", "prettier"],
rules: {
@ -55,15 +55,15 @@ module.exports = {
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
"react/jsx-filename-extension": [
"error",
{ extensions: [".js", ".tsx", ".jsx"] },
{ extensions: [".js", ".tsx", ".jsx"] }
],
"import/extensions": [
"error",
"ignorePackages",
{
ts: "never",
tsx: "never",
},
tsx: "never"
}
],
"import/order": [
"error",
@ -74,14 +74,14 @@ module.exports = {
"internal",
["sibling", "parent"],
"index",
"unknown",
"unknown"
],
"newlines-between": "always",
alphabetize: {
order: "asc",
caseInsensitive: true,
},
},
caseInsensitive: true
}
}
],
"sort-imports": [
"error",
@ -90,9 +90,9 @@ module.exports = {
ignoreDeclarationSort: true,
ignoreMemberSort: false,
memberSyntaxSortOrder: ["none", "all", "multiple", "single"],
allowSeparatedGroups: true,
},
allowSeparatedGroups: true
}
],
...a11yOff,
},
...a11yOff
}
};

View File

@ -1,3 +1,8 @@
VITE_TMDB_READ_API_KEY=...
VITE_OPENSEARCH_ENABLED=false
# make sure the cors proxy url does NOT have a slash at the end
VITE_CORS_PROXY_URL=...
VITE_TMDB_READ_API_KEY=...
# make sure the domain does NOT have a slash at the end
VITE_APP_DOMAIN=http://localhost:5173

View File

@ -33,6 +33,28 @@
<meta name="referrer" content="no-referrer" />
<title>movie-web</title>
{{#if opensearchEnabled }}
<!-- OpenSearch -->
<link rel="search" type="application/opensearchdescription+xml" title="movie-web" href="/opensearch.xml">
<!-- Google Sitelinks -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"url": "{{ routeDomain }}",
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "{{ routeDomain }}/browse/?q={search_term_string}"
},
"query-input": "required name=search_term_string"
}
}
</script>
{{/if}}
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@ -60,7 +60,6 @@
"@babel/core": "^7.21.3",
"@babel/preset-env": "^7.20.2",
"@babel/preset-typescript": "^7.21.0",
"@tailwindcss/line-clamp": "^0.4.2",
"@types/chromecast-caf-sender": "^1.0.5",
"@types/crypto-js": "^4.1.1",
"@types/dompurify": "^2.4.0",
@ -88,6 +87,8 @@
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "7.29.4",
"eslint-plugin-react-hooks": "4.3.0",
"glob": "^10.3.3",
"handlebars": "^4.7.7",
"jsdom": "^21.1.0",
"postcss": "^8.4.20",
"prettier": "^2.5.1",
@ -99,6 +100,7 @@
"vite-plugin-checker": "^0.5.6",
"vite-plugin-package-version": "^1.0.2",
"vite-plugin-pwa": "^0.14.4",
"vite-plugin-static-copy": "^0.16.0",
"vitest": "^0.28.5",
"workbox-build": "^6.5.4",
"workbox-window": "^6.5.4"

41
plugins/handlebars.ts Normal file
View File

@ -0,0 +1,41 @@
import { globSync } from "glob";
import { viteStaticCopy } from 'vite-plugin-static-copy'
import { PluginOption } from "vite";
import Handlebars from "handlebars";
import path from "path";
export const handlebars = (options: { vars?: Record<string, any> } = {}): PluginOption[] => {
const files = globSync("src/assets/**/**.hbs");
function render(content: string): string {
const template = Handlebars.compile(content);
return template(options?.vars ?? {});
}
return [
{
name: 'hbs-templating',
enforce: "pre",
transformIndexHtml: {
order: 'pre',
handler(html) {
return render(html);
}
},
},
viteStaticCopy({
silent: true,
targets: files.map(file => ({
src: file,
dest: '',
rename: path.basename(file).slice(0, -4), // remove .hbs file extension
transform: {
encoding: 'utf8',
handler(content: string) {
return render(content);
}
}
}))
})
]
}

View File

@ -0,0 +1,6 @@
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>movie-web</ShortName>
<Description>The place for your favorite movies &amp; shows</Description>
<InputEncoding>UTF-8</InputEncoding>
<Url type="text/html" template="{{ routeDomain }}/browse/?q={searchTerms}" />
</OpenSearchDescription>

View File

@ -6,7 +6,6 @@ import {
TMDBMediaToMediaType,
formatTMDBMeta,
getEpisodes,
getExternalIds,
getMediaDetails,
getMediaPoster,
getMovieFromExternalId,
@ -19,6 +18,7 @@ import {
} from "./types/justwatch";
import { MWMediaMeta, MWMediaType } from "./types/mw";
import {
TMDBContentTypes,
TMDBMediaResult,
TMDBMovieData,
TMDBSeasonMetaResult,
@ -74,8 +74,7 @@ export async function getMetaFromId(
if (!details) return null;
const externalIds = await getExternalIds(id, mediaTypeToTMDB(type));
const imdbId = externalIds.imdb_id ?? undefined;
const imdbId = details.external_ids.imdb_id ?? undefined;
let seasonData: TMDBSeasonMetaResult | undefined;
@ -165,7 +164,13 @@ export async function getLegacyMetaFromId(
}
export function isLegacyUrl(url: string): boolean {
if (url.startsWith("/media/JW")) return true;
if (url.startsWith("/media/JW") || url.startsWith("/media/tmdb-show"))
return true;
return false;
}
export function isLegacyMediaType(url: string): boolean {
if (url.startsWith("/media/tmdb-show")) return true;
return false;
}
@ -177,7 +182,16 @@ export async function convertLegacyUrl(
const urlParts = url.split("/").slice(2);
const [, type, id] = urlParts[0].split("-", 3);
const mediaType = TMDBMediaToMediaType(type);
if (isLegacyMediaType(url)) {
const details = await getMediaDetails(id, TMDBContentTypes.TV);
return `/media/${TMDBIdToUrlId(
MWMediaType.SERIES,
details.id.toString(),
details.name
)}`;
}
const mediaType = TMDBMediaToMediaType(type as TMDBContentTypes);
const meta = await getLegacyMetaFromId(mediaType, id);
if (!meta) return undefined;

View File

@ -1,26 +1,21 @@
import { SimpleCache } from "@/utils/cache";
import {
formatTMDBMeta,
formatTMDBSearchResult,
mediaTypeToTMDB,
searchMedia,
} from "./tmdb";
import { formatTMDBMeta, formatTMDBSearchResult, multiSearch } from "./tmdb";
import { MWMediaMeta, MWQuery } from "./types/mw";
const cache = new SimpleCache<MWQuery, MWMediaMeta[]>();
cache.setCompare((a, b) => {
return a.type === b.type && a.searchQuery.trim() === b.searchQuery.trim();
return a.searchQuery.trim() === b.searchQuery.trim();
});
cache.initialize();
export async function searchForMedia(query: MWQuery): Promise<MWMediaMeta[]> {
if (cache.has(query)) return cache.get(query) as MWMediaMeta[];
const { searchQuery, type } = query;
const { searchQuery } = query;
const data = await searchMedia(searchQuery, mediaTypeToTMDB(type));
const results = data.results.map((v) => {
const formattedResult = formatTMDBSearchResult(v, mediaTypeToTMDB(type));
const data = await multiSearch(searchQuery);
const results = data.map((v) => {
const formattedResult = formatTMDBSearchResult(v, v.media_type);
return formatTMDBMeta(formattedResult);
});

View File

@ -7,33 +7,26 @@ import {
ExternalIdMovieSearchResult,
TMDBContentTypes,
TMDBEpisodeShort,
TMDBExternalIds,
TMDBMediaResult,
TMDBMovieData,
TMDBMovieExternalIds,
TMDBMovieResponse,
TMDBMovieResult,
TMDBMovieSearchResult,
TMDBSearchResult,
TMDBSeason,
TMDBSeasonMetaResult,
TMDBShowData,
TMDBShowExternalIds,
TMDBShowResponse,
TMDBShowResult,
TMDBShowSearchResult,
} from "./types/tmdb";
import { mwFetch } from "../helpers/fetch";
export function mediaTypeToTMDB(type: MWMediaType): TMDBContentTypes {
if (type === MWMediaType.MOVIE) return "movie";
if (type === MWMediaType.SERIES) return "show";
if (type === MWMediaType.MOVIE) return TMDBContentTypes.MOVIE;
if (type === MWMediaType.SERIES) return TMDBContentTypes.TV;
throw new Error("unsupported type");
}
export function TMDBMediaToMediaType(type: string): MWMediaType {
if (type === "movie") return MWMediaType.MOVIE;
if (type === "show") return MWMediaType.SERIES;
export function TMDBMediaToMediaType(type: TMDBContentTypes): MWMediaType {
if (type === TMDBContentTypes.MOVIE) return MWMediaType.MOVIE;
if (type === TMDBContentTypes.TV) return MWMediaType.SERIES;
throw new Error("unsupported type");
}
@ -103,7 +96,7 @@ export function decodeTMDBId(
if (prefix !== "tmdb") return null;
let mediaType;
try {
mediaType = TMDBMediaToMediaType(type);
mediaType = TMDBMediaToMediaType(type as TMDBContentTypes);
} catch {
return null;
}
@ -131,40 +124,10 @@ async function get<T>(url: string, params?: object): Promise<T> {
return res;
}
export async function searchMedia(
query: string,
type: TMDBContentTypes
): Promise<TMDBMovieResponse | TMDBShowResponse> {
let data;
switch (type) {
case "movie":
data = await get<TMDBMovieResponse>("search/movie", {
query,
include_adult: false,
language: "en-US",
page: 1,
});
break;
case "show":
data = await get<TMDBShowResponse>("search/tv", {
query,
include_adult: false,
language: "en-US",
page: 1,
});
break;
default:
throw new Error("Invalid media type");
}
return data;
}
export async function multiSearch(
query: string
): Promise<(TMDBMovieSearchResult | TMDBShowSearchResult)[]> {
const data = await get<TMDBSearchResult>(`search/multi`, {
const data = await get<TMDBSearchResult>("search/multi", {
query,
include_adult: false,
language: "en-US",
@ -172,7 +135,9 @@ export async function multiSearch(
});
// filter out results that aren't movies or shows
const results = data.results.filter(
(r) => r.media_type === "movie" || r.media_type === "tv"
(r) =>
r.media_type === TMDBContentTypes.MOVIE ||
r.media_type === TMDBContentTypes.TV
);
return results;
}
@ -183,20 +148,21 @@ export async function generateQuickSearchMediaUrl(
const data = await multiSearch(query);
if (data.length === 0) return undefined;
const result = data[0];
const type = result.media_type === "movie" ? "movie" : "show";
const title = result.media_type === "movie" ? result.title : result.name;
const title =
result.media_type === TMDBContentTypes.MOVIE ? result.title : result.name;
return `/media/${TMDBIdToUrlId(
TMDBMediaToMediaType(type),
TMDBMediaToMediaType(result.media_type),
result.id.toString(),
title
)}`;
}
// Conditional type which for inferring the return type based on the content type
type MediaDetailReturn<T extends TMDBContentTypes> = T extends "movie"
type MediaDetailReturn<T extends TMDBContentTypes> =
T extends TMDBContentTypes.MOVIE
? TMDBMovieData
: T extends "show"
: T extends TMDBContentTypes.TV
? TMDBShowData
: never;
@ -204,11 +170,11 @@ export function getMediaDetails<
T extends TMDBContentTypes,
TReturn = MediaDetailReturn<T>
>(id: string, type: T): Promise<TReturn> {
if (type === "movie") {
return get<TReturn>(`/movie/${id}`);
if (type === TMDBContentTypes.MOVIE) {
return get<TReturn>(`/movie/${id}`, { append_to_response: "external_ids" });
}
if (type === "show") {
return get<TReturn>(`/tv/${id}`);
if (type === TMDBContentTypes.TV) {
return get<TReturn>(`/tv/${id}`, { append_to_response: "external_ids" });
}
throw new Error("Invalid media type");
}
@ -229,26 +195,6 @@ export async function getEpisodes(
}));
}
export async function getExternalIds(
id: string,
type: TMDBContentTypes
): Promise<TMDBExternalIds> {
let data;
switch (type) {
case "movie":
data = await get<TMDBMovieExternalIds>(`/movie/${id}/external_ids`);
break;
case "show":
data = await get<TMDBShowExternalIds>(`/tv/${id}/external_ids`);
break;
default:
throw new Error("Invalid media type");
}
return data;
}
export async function getMovieFromExternalId(
imdbId: string
): Promise<string | undefined> {
@ -263,12 +209,12 @@ export async function getMovieFromExternalId(
}
export function formatTMDBSearchResult(
result: TMDBShowResult | TMDBMovieResult,
result: TMDBMovieSearchResult | TMDBShowSearchResult,
mediatype: TMDBContentTypes
): TMDBMediaResult {
const type = TMDBMediaToMediaType(mediatype);
if (type === MWMediaType.SERIES) {
const show = result as TMDBShowResult;
const show = result as TMDBShowSearchResult;
return {
title: show.name,
poster: getMediaPoster(show.poster_path),
@ -277,7 +223,8 @@ export function formatTMDBSearchResult(
object_type: mediatype,
};
}
const movie = result as TMDBMovieResult;
const movie = result as TMDBMovieSearchResult;
return {
title: movie.title,

View File

@ -43,7 +43,6 @@ export type MWMediaMeta = MWMediaMetaBase & MWMediaMetaSpecific;
export interface MWQuery {
searchQuery: string;
type: MWMediaType;
}
export interface DetailedMeta {

View File

@ -1,4 +1,7 @@
export type TMDBContentTypes = "movie" | "show";
export enum TMDBContentTypes {
MOVIE = "movie",
TV = "tv",
}
export type TMDBSeasonShort = {
title: string;
@ -121,6 +124,9 @@ export interface TMDBShowData {
type: string;
vote_average: number;
vote_count: number;
external_ids: {
imdb_id: string | null;
};
}
export interface TMDBMovieData {
@ -169,6 +175,9 @@ export interface TMDBMovieData {
video: boolean;
vote_average: number;
vote_count: number;
external_ids: {
imdb_id: string | null;
};
}
export interface TMDBEpisodeResult {
@ -183,54 +192,6 @@ export interface TMDBEpisodeResult {
};
}
export interface TMDBShowResult {
adult: boolean;
backdrop_path: string | null;
genre_ids: number[];
id: number;
origin_country: string[];
original_language: string;
original_name: string;
overview: string;
popularity: number;
poster_path: string | null;
first_air_date: string;
name: string;
vote_average: number;
vote_count: number;
}
export interface TMDBShowResponse {
page: number;
results: TMDBShowResult[];
total_pages: number;
total_results: number;
}
export interface TMDBMovieResult {
adult: boolean;
backdrop_path: string | null;
genre_ids: number[];
id: number;
original_language: string;
original_title: string;
overview: string;
popularity: number;
poster_path: string | null;
release_date: string;
title: string;
video: boolean;
vote_average: number;
vote_count: number;
}
export interface TMDBMovieResponse {
page: number;
results: TMDBMovieResult[];
total_pages: number;
total_results: number;
}
export interface TMDBEpisode {
air_date: string;
episode_number: number;
@ -259,30 +220,6 @@ export interface TMDBSeason {
season_number: number;
}
export interface TMDBShowExternalIds {
id: number;
imdb_id: null | string;
freebase_mid: null | string;
freebase_id: null | string;
tvdb_id: number;
tvrage_id: null | string;
wikidata_id: null | string;
facebook_id: null | string;
instagram_id: null | string;
twitter_id: null | string;
}
export interface TMDBMovieExternalIds {
id: number;
imdb_id: null | string;
wikidata_id: null | string;
facebook_id: null | string;
instagram_id: null | string;
twitter_id: null | string;
}
export type TMDBExternalIds = TMDBShowExternalIds | TMDBMovieExternalIds;
export interface ExternalIdMovieSearchResult {
movie_results: {
adult: boolean;
@ -316,7 +253,7 @@ export interface TMDBMovieSearchResult {
original_title: string;
overview: string;
poster_path: string;
media_type: "movie";
media_type: TMDBContentTypes.MOVIE;
genre_ids: number[];
popularity: number;
release_date: string;
@ -334,7 +271,7 @@ export interface TMDBShowSearchResult {
original_name: string;
overview: string;
poster_path: string;
media_type: "tv";
media_type: TMDBContentTypes.TV;
genre_ids: number[];
popularity: number;
first_air_date: string;

View File

@ -1,14 +1,9 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { MWQuery } from "@/backend/metadata/types/mw";
import { MWMediaType, MWQuery } from "@/backend/metadata/types/mw";
import { DropdownButton } from "./buttons/DropdownButton";
import { Icon, Icons } from "./Icon";
import { TextInputControl } from "./text-inputs/TextInputControl";
export interface SearchBarProps {
buttonText?: string;
placeholder?: string;
onChange: (value: MWQuery, force: boolean) => void;
onUnFocus: () => void;
@ -16,9 +11,6 @@ export interface SearchBarProps {
}
export function SearchBarInput(props: SearchBarProps) {
const { t } = useTranslation();
const [dropdownOpen, setDropdownOpen] = useState(false);
function setSearch(value: string) {
props.onChange(
{
@ -28,15 +20,6 @@ export function SearchBarInput(props: SearchBarProps) {
false
);
}
function setType(type: string) {
props.onChange(
{
...props.value,
type: type as MWMediaType,
},
true
);
}
return (
<div className="relative flex flex-col rounded-[28px] bg-denim-400 transition-colors focus-within:bg-denim-400 hover:bg-denim-500 sm:flex-row sm:items-center">
@ -51,31 +34,6 @@ export function SearchBarInput(props: SearchBarProps) {
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}
/>
<div className="px-4 py-4 pt-0 sm:px-2 sm:py-2">
<DropdownButton
icon={Icons.SEARCH}
open={dropdownOpen}
setOpen={(val) => 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,
},
]}
onClick={() => setDropdownOpen((old) => !old)}
>
{props.buttonText || t("searchBar.search")}
</DropdownButton>
</div>
</div>
);
}

View File

@ -117,7 +117,7 @@ function MediaCardContent({
/>
</div>
</div>
<h1 className="mb-1 max-h-[4.5rem] text-ellipsis break-words font-bold text-white line-clamp-3">
<h1 className="mb-1 line-clamp-3 max-h-[4.5rem] text-ellipsis break-words font-bold text-white">
<span>{media.title}</span>
</h1>
<DotList className="text-xs" content={dotListContent} />

View File

@ -1,14 +1,16 @@
import { useState } from "react";
import { generatePath, useHistory, useRouteMatch } from "react-router-dom";
import { generatePath, useHistory, useParams } from "react-router-dom";
import { MWMediaType, MWQuery } from "@/backend/metadata/types/mw";
import { MWQuery } from "@/backend/metadata/types/mw";
import { useQueryParams } from "@/hooks/useQueryParams";
function getInitialValue(params: { type: string; query: string }) {
const type =
Object.values(MWMediaType).find((v) => params.type === v) ||
MWMediaType.MOVIE;
const searchQuery = decodeURIComponent(params.query || "");
return { type, searchQuery };
function getInitialValue(
query: Record<string, string>,
params: Record<string, string>
) {
let searchQuery = decodeURIComponent(params.query || "");
if (query.q) searchQuery = query.q;
return { searchQuery };
}
export function useSearchQuery(): [
@ -17,28 +19,34 @@ export function useSearchQuery(): [
() => void
] {
const history = useHistory();
const { path, params } = useRouteMatch<{ type: string; query: string }>();
const [search, setSearch] = useState<MWQuery>(getInitialValue(params));
const query = useQueryParams();
const params = useParams<{ query: string }>();
const [search, setSearch] = useState<MWQuery>(getInitialValue(query, params));
const updateParams = (inp: Partial<MWQuery>, force: boolean) => {
const copySearch: MWQuery = { ...search };
const copySearch = { ...search };
Object.assign(copySearch, inp);
setSearch(copySearch);
if (!force) return;
if (copySearch.searchQuery.length === 0) {
history.replace("/");
return;
}
history.replace(
generatePath(path, {
query:
copySearch.searchQuery.length === 0 ? undefined : inp.searchQuery,
type: copySearch.type,
generatePath("/browse/:query", {
query: copySearch.searchQuery,
})
);
};
const onUnFocus = () => {
if (search.searchQuery.length === 0) {
history.replace("/");
return;
}
history.replace(
generatePath(path, {
query: search.searchQuery.length === 0 ? undefined : search.searchQuery,
type: search.type,
generatePath("/browse/:query", {
query: search.searchQuery,
})
);
};

View File

@ -10,7 +10,6 @@ import {
import { convertLegacyUrl, isLegacyUrl } from "@/backend/metadata/getmeta";
import { generateQuickSearchMediaUrl } from "@/backend/metadata/tmdb";
import { MWMediaType } from "@/backend/metadata/types/mw";
import { BannerContextProvider } from "@/hooks/useBanner";
import { Layout } from "@/setup/Layout";
import { BookmarkContextProvider } from "@/state/bookmark";
@ -64,9 +63,6 @@ function App() {
<Switch>
{/* functional routes */}
<Route exact path="/v2-migration" component={V2MigrationView} />
<Route exact path="/">
<Redirect to={`/search/${MWMediaType.MOVIE}`} />
</Route>
<Route exact path="/s/:query">
<QuickSearch />
</Route>
@ -82,9 +78,15 @@ function App() {
<MediaView />
</LegacyUrlView>
</Route>
<Route exact path="/search/:type/:query?">
<Redirect to="/browse/:query" />
</Route>
<Route exact path="/search/:type">
<Redirect to="/browse" />
</Route>
<Route
exact
path="/search/:type/:query?"
path={["/browse/:query?", "/"]}
component={SearchView}
/>

View File

@ -3,8 +3,6 @@
"name": "movie-web"
},
"search": {
"loading_series": "Načítání Vašich oblíbených seriálů...",
"loading_movie": "Načítání Vašich oblíbených filmů...",
"loading": "Načítání...",
"allResults": "To je vše co máme!",
"noResults": "Nemohli jsme nic najít!",

View File

@ -3,8 +3,6 @@
"name": "movie-web"
},
"search": {
"loading_series": "Auf der Suche nach deiner Lieblingsserie...",
"loading_movie": "Auf der Suche nach deinen Lieblingsfilmen...",
"loading": "Wird geladen...",
"allResults": "Das ist alles, was wir haben!",
"noResults": "Wir haben nichts gefunden!",

View File

@ -3,8 +3,6 @@
"name": "movie-web"
},
"search": {
"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!",
@ -73,7 +71,7 @@
"sources": "Sources",
"close": "Close",
"seasons": {
"title":"Seasons",
"title": "Seasons",
"other": "Other seasons",
"noSeason": "No season"
},

View File

@ -3,8 +3,6 @@
"name": "movie-web"
},
"search": {
"loading_series": "Recherche de votre série préférée...",
"loading_movie": "Recherche de vos films préférés...",
"loading": "Chargement...",
"allResults": "C'est tout ce que nous avons!",
"noResults": "Nous n'avons rien trouvé!",

View File

@ -3,8 +3,6 @@
"name": "movie-web"
},
"search": {
"loading_series": "Recupero delle tue serie preferite...",
"loading_movie": "Recupero dei tuoi film preferiti...",
"loading": "Caricamento...",
"allResults": "Ecco tutto ciò che abbiamo!",
"noResults": "Non abbiamo trovato nulla!",

View File

@ -3,8 +3,6 @@
"name": "movie-web"
},
"search": {
"loading_series": "We zoeken je favoriete series...",
"loading_movie": "We zoeken je favoriete films...",
"loading": "Aan het zoeken...",
"allResults": "Dat is het!",
"noResults": "We konden helaas niets vinden.",

View File

@ -3,8 +3,6 @@
"name": "movie-web"
},
"search": {
"loading_series": "Fetchin' yer favorite series...",
"loading_movie": "Fetchin' yer favorite movies...",
"loadin'": "Loadin'...",
"allResults": "That be all we 'ave, me hearty!",
"noResults": "We couldn't find anythin' that matches yer search!",

View File

@ -3,8 +3,6 @@
"name": "movie-web"
},
"search": {
"loading_series": "Szukamy twoich ulubionych seriali...",
"loading_movie": "Szukamy twoich ulubionych filmów...",
"loading": "Wczytywanie...",
"allResults": "To wszystko co mamy!",
"noResults": "Nie mogliśmy niczego znaleźć!",
@ -71,7 +69,7 @@
"popouts": {
"close": "Zamknąć",
"seasons": {
"title":"Sezony",
"title": "Sezony",
"other": "Inne sezony",
"noSeason": "Brak sezonu"
},

View File

@ -3,8 +3,6 @@
"name": "movie-web"
},
"search": {
"loading_series": "Favori dizileriniz aranıyor...",
"loading_movie": "Favori filmleriniz aranıyor...",
"loading": "Yükleniyor...",
"allResults": "Bu kadarını bulabildik!",
"noResults": "Hiçbir şey bulamadık!",
@ -71,9 +69,9 @@
"popouts": {
"back": "Geri git",
"sources": "Kaynaklar",
"close":"Kapat",
"close": "Kapat",
"seasons": {
"title":"Sezonlar",
"title": "Sezonlar",
"other": "Diğer sezonlar",
"noSeason": "Sezon yok"
},

View File

@ -3,8 +3,6 @@
"name": "movie-web"
},
"search": {
"loading_series": "Đang tìm chương trình yêu thích của bạn...",
"loading_movie": "Đang tìm bộ phim yêu thích của bạn...",
"loading": "Đang tải...",
"allResults": "Đó là tất cả chúng tôi có!",
"noResults": "Chúng tôi không thể tìm thấy gì!",

View File

@ -3,8 +3,6 @@
"name": "movie-web"
},
"search": {
"loading_series": "正在获取您最喜欢的连续剧……",
"loading_movie": "正在获取您最喜欢的影片……",
"loading": "载入中……",
"allResults": "以上是我们能找到的所有结果!",
"noResults": "我们找不到任何结果!",

View File

@ -48,7 +48,6 @@ async function getMetas(
const year = Number(item.year.toString().split("-")[0]);
const data = await searchForMedia({
searchQuery: `${item.title} ${year}`,
type: item.mediaType,
});
const relevantItem = data.find(
(res) =>

View File

@ -5,6 +5,7 @@ import {
getMovieFromExternalId,
} from "@/backend/metadata/tmdb";
import { MWMediaType } from "@/backend/metadata/types/mw";
import { TMDBContentTypes } from "@/backend/metadata/types/tmdb";
import { BookmarkStoreData } from "@/state/bookmark/types";
import { isNotNull } from "@/utils/typeguard";
@ -59,7 +60,10 @@ export async function migrateV3Videos(
clone.item.meta.id = migratedId;
if (clone.item.series) {
const series = clone.item.series;
const details = await getMediaDetails(migratedId, "show");
const details = await getMediaDetails(
migratedId,
TMDBContentTypes.TV
);
const season = details.seasons.find(
(v) => v.season_number === series.season

View File

@ -1,19 +1,10 @@
import { useTranslation } from "react-i18next";
import { Loading } from "@/components/layout/Loading";
import { useSearchQuery } from "@/hooks/useSearchQuery";
export function SearchLoadingView() {
const { t } = useTranslation();
const [query] = useSearchQuery();
return (
<Loading
className="mb-24 mt-40 "
text={
t(`search.loading_${query.type}`) ||
t("search.loading") ||
"Fetching your favourite shows..."
}
/>
<Loading className="mb-24 mt-40 " text={t("search.loading") || "..."} />
);
}

View File

@ -42,5 +42,5 @@ module.exports = {
animation: { "loading-pin": "loading-pin 1.8s ease-in-out infinite" }
}
},
plugins: [require("tailwind-scrollbar"), require("@tailwindcss/line-clamp")]
plugins: [require("tailwind-scrollbar")]
};

View File

@ -4,9 +4,21 @@ import loadVersion from "vite-plugin-package-version";
import { VitePWA } from "vite-plugin-pwa";
import checker from "vite-plugin-checker";
import path from "path";
import { handlebars } from "./plugins/handlebars";
import { loadEnv } from "vite";
export default defineConfig({
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd());
return {
plugins: [
handlebars({
vars: {
opensearchEnabled: env.VITE_OPENSEARCH_ENABLED === "true",
routeDomain: env.VITE_APP_DOMAIN + (env.VITE_NORMAL_ROUTER !== 'true' ? "/#" : ""),
domain: env.VITE_APP_DOMAIN,
env,
},
}),
react({
babel: {
presets: [
@ -91,4 +103,5 @@ export default defineConfig({
test: {
environment: "jsdom",
},
};
});

178
yarn.lock
View File

@ -1141,6 +1141,18 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==
dependencies:
string-width "^5.1.2"
string-width-cjs "npm:string-width@^4.2.0"
strip-ansi "^7.0.1"
strip-ansi-cjs "npm:strip-ansi@^6.0.1"
wrap-ansi "^8.1.0"
wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098"
@ -1207,6 +1219,11 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@pkgjs/parseargs@^0.11.0":
version "0.11.0"
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@react-spring/animated@~9.7.2":
version "9.7.2"
resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.7.2.tgz#0119db8075e91d693ec45c42575541e01b104a70"
@ -1391,11 +1408,6 @@
magic-string "^0.25.0"
string.prototype.matchall "^4.0.6"
"@tailwindcss/line-clamp@^0.4.2":
version "0.4.4"
resolved "https://registry.yarnpkg.com/@tailwindcss/line-clamp/-/line-clamp-0.4.4.tgz#767cf8e5d528a5d90c9740ca66eb079f5e87d423"
integrity sha512-5U6SY5z8N42VtrCrKlsTAA35gy2VSyYtHWCsg1H87NU1SXnEfekTVlrga9fzUDrrHcGi2Lb5KenUWb4lRQT5/g==
"@tootallnate/once@2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
@ -1836,7 +1848,7 @@ ansi-styles@^3.2.1:
dependencies:
color-convert "^1.9.0"
ansi-styles@^4.1.0:
ansi-styles@^4.0.0, ansi-styles@^4.1.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
@ -1848,7 +1860,7 @@ ansi-styles@^5.0.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
ansi-styles@^6.0.0:
ansi-styles@^6.0.0, ansi-styles@^6.1.0:
version "6.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
@ -2238,7 +2250,7 @@ core-js@^3.29.1, core-js@^3.6.5:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.30.1.tgz#fc9c5adcc541d8e9fa3e381179433cbf795628ba"
integrity sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ==
cross-spawn@^7.0.2:
cross-spawn@^7.0.0, cross-spawn@^7.0.2:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
@ -2453,6 +2465,11 @@ electron-to-chromium@^1.4.284:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.369.tgz#a98d838cdd79be4471cd04e9b4dffe891d037874"
integrity sha512-LfxbHXdA/S+qyoTEA4EbhxGjrxx7WK2h6yb5K2v0UCOufUKX+VZaHbl3svlzZfv9sGseym/g3Ne4DpsgRULmqg==
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
emoji-regex@^9.2.2:
version "9.2.2"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
@ -2866,6 +2883,17 @@ fast-diff@^1.1.2:
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
fast-glob@^3.2.11:
version "3.3.0"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.0.tgz#7c40cb491e1e2ed5664749e87bfb516dbe8727c0"
integrity sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
glob-parent "^5.1.2"
merge2 "^1.3.0"
micromatch "^4.0.4"
fast-glob@^3.2.12, fast-glob@^3.2.7, fast-glob@^3.2.9:
version "3.2.12"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
@ -2958,6 +2986,14 @@ for-each@^0.3.3:
dependencies:
is-callable "^1.1.3"
foreground-child@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d"
integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==
dependencies:
cross-spawn "^7.0.0"
signal-exit "^4.0.1"
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
@ -3089,6 +3125,17 @@ glob@7.1.6:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^10.3.3:
version "10.3.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.3.tgz#8360a4ffdd6ed90df84aa8d52f21f452e86a123b"
integrity sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw==
dependencies:
foreground-child "^3.1.0"
jackspeak "^2.0.3"
minimatch "^9.0.1"
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
path-scurry "^1.10.1"
glob@^7.1.3, glob@^7.1.6, glob@^7.2.0:
version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
@ -3149,6 +3196,18 @@ grapheme-splitter@^1.0.4:
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
handlebars@^4.7.7:
version "4.7.7"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1"
integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==
dependencies:
minimist "^1.2.5"
neo-async "^2.6.0"
source-map "^0.6.1"
wordwrap "^1.0.0"
optionalDependencies:
uglify-js "^3.1.4"
has-bigints@^1.0.1, has-bigints@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
@ -3397,6 +3456,11 @@ is-extglob@^2.1.1:
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
is-fullwidth-code-point@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
is-fullwidth-code-point@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz#fae3167c729e7463f8461ce512b080a49268aa88"
@ -3541,6 +3605,15 @@ isexe@^2.0.0:
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
jackspeak@^2.0.3:
version "2.2.1"
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.2.1.tgz#655e8cf025d872c9c03d3eb63e8f0c024fef16a6"
integrity sha512-MXbxovZ/Pm42f6cDIDkl3xpwv1AGwObKwfmjs2nQePiy85tP3fatofl3FC1aBsOtP/6fq5SbtgHwWcMsLP+bDw==
dependencies:
"@isaacs/cliui" "^8.0.2"
optionalDependencies:
"@pkgjs/parseargs" "^0.11.0"
jake@^10.8.5:
version "10.8.5"
resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46"
@ -3815,6 +3888,11 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
"lru-cache@^9.1.1 || ^10.0.0":
version "10.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.0.tgz#b9e2a6a72a129d81ab317202d93c7691df727e61"
integrity sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw==
magic-string@^0.25.0, magic-string@^0.25.7:
version "0.25.9"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"
@ -3878,11 +3956,23 @@ minimatch@^5.0.1:
dependencies:
brace-expansion "^2.0.1"
minimist@^1.2.0, minimist@^1.2.6:
minimatch@^9.0.1:
version "9.0.3"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825"
integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==
dependencies:
brace-expansion "^2.0.1"
minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
"minipass@^5.0.0 || ^6.0.2 || ^7.0.0":
version "7.0.2"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.2.tgz#58a82b7d81c7010da5bd4b2c0c85ac4b4ec5131e"
integrity sha512-eL79dXrE1q9dBbDCLg7xfn/vl7MS4F1gvJAgjJrQli/jbQWdUttuVawphqpffoIYfRdq78LHx6GP4bU/EQ2ATA==
mlly@^1.1.0, mlly@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.2.0.tgz#f0f6c2fc8d2d12ea6907cd869066689b5031b613"
@ -3946,6 +4036,11 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
neo-async@^2.6.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
node-fetch-native@^1.0.2:
version "1.1.0"
resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.1.0.tgz#a530f5c4cadb49b382dcf81d8f5f19ed0f457fbe"
@ -4151,6 +4246,14 @@ path-parse@^1.0.7:
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-scurry@^1.10.1:
version "1.10.1"
resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698"
integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==
dependencies:
lru-cache "^9.1.1 || ^10.0.0"
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
path-to-regexp@^1.7.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
@ -4751,6 +4854,11 @@ siginfo@^2.0.0:
resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30"
integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==
signal-exit@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.0.2.tgz#ff55bb1d9ff2114c13b400688fa544ac63c36967"
integrity sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==
slash@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
@ -4850,7 +4958,17 @@ stop-iteration-iterator@^1.0.0:
dependencies:
internal-slot "^1.0.4"
string-width@^5.0.0:
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
name string-width-cjs
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==
@ -4909,7 +5027,7 @@ stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -5241,6 +5359,11 @@ ufo@^1.1.0, ufo@^1.1.1:
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.1.1.tgz#e70265e7152f3aba425bd013d150b2cdf4056d7c"
integrity sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==
uglify-js@^3.1.4:
version "3.17.4"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c"
integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==
unbox-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
@ -5388,6 +5511,16 @@ vite-plugin-pwa@^0.14.4:
workbox-build "^6.5.4"
workbox-window "^6.5.4"
vite-plugin-static-copy@^0.16.0:
version "0.16.0"
resolved "https://registry.yarnpkg.com/vite-plugin-static-copy/-/vite-plugin-static-copy-0.16.0.tgz#2f65227037f17fc99c0782fd0b344e962935e69e"
integrity sha512-dMVEg5Z2SwYRgQnHZaeokvSKB4p/TOTf65JU4sP3U6ccSBsukqdtDOjpmT+xzTFHAA8WJjcS31RMLjUdWQCBzw==
dependencies:
chokidar "^3.5.3"
fast-glob "^3.2.11"
fs-extra "^11.1.0"
picocolors "^1.0.0"
"vite@^3.0.0 || ^4.0.0", vite@^4.0.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.3.1.tgz#9badb1377f995632cdcf05f32103414db6fbb95a"
@ -5577,6 +5710,11 @@ word-wrap@^1.2.3, word-wrap@~1.2.3:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
wordwrap@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
workbox-background-sync@6.5.4:
version "6.5.4"
resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.5.4.tgz#3141afba3cc8aa2ae14c24d0f6811374ba8ff6a9"
@ -5735,6 +5873,24 @@ workbox-window@6.5.4, workbox-window@^6.5.4:
"@types/trusted-types" "^2.0.2"
workbox-core "6.5.4"
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==
dependencies:
ansi-styles "^6.1.0"
string-width "^5.0.1"
strip-ansi "^7.0.1"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"