diff --git a/.eslintrc.js b/.eslintrc.js index d75c57d9..c1a2b2a7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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 + } }; diff --git a/example.env b/example.env index d191d741..38c690a5 100644 --- a/example.env +++ b/example.env @@ -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 diff --git a/index.html b/index.html index 23063c5a..5c674b2f 100644 --- a/index.html +++ b/index.html @@ -33,6 +33,28 @@ movie-web + + {{#if opensearchEnabled }} + + + + + + {{/if}} diff --git a/package.json b/package.json index d315bd23..71fef462 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/plugins/handlebars.ts b/plugins/handlebars.ts new file mode 100644 index 00000000..a8ab0170 --- /dev/null +++ b/plugins/handlebars.ts @@ -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 } = {}): 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); + } + } + })) + }) + ] +} diff --git a/src/assets/opensearch.xml.hbs b/src/assets/opensearch.xml.hbs new file mode 100644 index 00000000..e0ce3a5a --- /dev/null +++ b/src/assets/opensearch.xml.hbs @@ -0,0 +1,6 @@ + + movie-web + The place for your favorite movies & shows + UTF-8 + + diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index 6081e5ad..199efccb 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -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; diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 0d8f561f..6f6ecc99 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -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(); 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 { 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); }); diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index 64304900..d4e75e57 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -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(url: string, params?: object): Promise { return res; } -export async function searchMedia( - query: string, - type: TMDBContentTypes -): Promise { - let data; - - switch (type) { - case "movie": - data = await get("search/movie", { - query, - include_adult: false, - language: "en-US", - page: 1, - }); - break; - case "show": - data = await get("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(`search/multi`, { + const data = await get("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,32 +148,33 @@ 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 "movie" - ? TMDBMovieData - : T extends "show" - ? TMDBShowData - : never; +type MediaDetailReturn = + T extends TMDBContentTypes.MOVIE + ? TMDBMovieData + : T extends TMDBContentTypes.TV + ? TMDBShowData + : never; export function getMediaDetails< T extends TMDBContentTypes, TReturn = MediaDetailReturn >(id: string, type: T): Promise { - if (type === "movie") { - return get(`/movie/${id}`); + if (type === TMDBContentTypes.MOVIE) { + return get(`/movie/${id}`, { append_to_response: "external_ids" }); } - if (type === "show") { - return get(`/tv/${id}`); + if (type === TMDBContentTypes.TV) { + return get(`/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 { - let data; - - switch (type) { - case "movie": - data = await get(`/movie/${id}/external_ids`); - break; - case "show": - data = await get(`/tv/${id}/external_ids`); - break; - default: - throw new Error("Invalid media type"); - } - - return data; -} - export async function getMovieFromExternalId( imdbId: string ): Promise { @@ -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, diff --git a/src/backend/metadata/types/mw.ts b/src/backend/metadata/types/mw.ts index e7cc26fe..3f0f452f 100644 --- a/src/backend/metadata/types/mw.ts +++ b/src/backend/metadata/types/mw.ts @@ -43,7 +43,6 @@ export type MWMediaMeta = MWMediaMetaBase & MWMediaMetaSpecific; export interface MWQuery { searchQuery: string; - type: MWMediaType; } export interface DetailedMeta { diff --git a/src/backend/metadata/types/tmdb.ts b/src/backend/metadata/types/tmdb.ts index 8f6bf14b..19a85c54 100644 --- a/src/backend/metadata/types/tmdb.ts +++ b/src/backend/metadata/types/tmdb.ts @@ -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; diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index 431de337..1d2ce354 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -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 (
@@ -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} /> - -
- 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")} - -
); } diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index e05fbebb..828f6bd7 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -117,7 +117,7 @@ function MediaCardContent({ /> -

+

{media.title}

diff --git a/src/hooks/useSearchQuery.ts b/src/hooks/useSearchQuery.ts index cb8c3171..fe2b451e 100644 --- a/src/hooks/useSearchQuery.ts +++ b/src/hooks/useSearchQuery.ts @@ -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, + params: Record +) { + 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(getInitialValue(params)); + const query = useQueryParams(); + const params = useParams<{ query: string }>(); + const [search, setSearch] = useState(getInitialValue(query, params)); const updateParams = (inp: Partial, 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, }) ); }; diff --git a/src/setup/App.tsx b/src/setup/App.tsx index 53f3c131..6bb6b957 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -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() { {/* functional routes */} - - - @@ -82,9 +78,15 @@ function App() { + + + + + + diff --git a/src/setup/locales/cs/translation.json b/src/setup/locales/cs/translation.json index a7e5e826..f2b1b3f1 100644 --- a/src/setup/locales/cs/translation.json +++ b/src/setup/locales/cs/translation.json @@ -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!", diff --git a/src/setup/locales/de/translation.json b/src/setup/locales/de/translation.json index d58923c0..fa24c345 100644 --- a/src/setup/locales/de/translation.json +++ b/src/setup/locales/de/translation.json @@ -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!", diff --git a/src/setup/locales/en/translation.json b/src/setup/locales/en/translation.json index d90b568b..8f48a9d6 100644 --- a/src/setup/locales/en/translation.json +++ b/src/setup/locales/en/translation.json @@ -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" }, diff --git a/src/setup/locales/fr/translation.json b/src/setup/locales/fr/translation.json index 408570df..84e81495 100644 --- a/src/setup/locales/fr/translation.json +++ b/src/setup/locales/fr/translation.json @@ -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é!", diff --git a/src/setup/locales/it/translation.json b/src/setup/locales/it/translation.json index 7c28992c..fbd5bf3c 100644 --- a/src/setup/locales/it/translation.json +++ b/src/setup/locales/it/translation.json @@ -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!", @@ -88,7 +86,7 @@ "customCaption": "Sottotitolo personalizzato", "uploadCustomCaption": "Carica sottotitolo", "noEmbeds": "Nessun embed è stato trovato per questa fonte", - + "errors": { "loadingWentWong": "Si è verificato un problema durante il caricamento degli episodi per {{seasonTitle}}", "embedsError": "Si è verificato un problema durante il caricamento degli embed per questa cosa che ti piace" diff --git a/src/setup/locales/nl/translation.json b/src/setup/locales/nl/translation.json index 23ecb618..5a97ae13 100644 --- a/src/setup/locales/nl/translation.json +++ b/src/setup/locales/nl/translation.json @@ -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.", diff --git a/src/setup/locales/pirate/translation.json b/src/setup/locales/pirate/translation.json index a4a92117..949d05ad 100644 --- a/src/setup/locales/pirate/translation.json +++ b/src/setup/locales/pirate/translation.json @@ -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!", diff --git a/src/setup/locales/pl/translation.json b/src/setup/locales/pl/translation.json index a85d9c70..b9fda283 100644 --- a/src/setup/locales/pl/translation.json +++ b/src/setup/locales/pl/translation.json @@ -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" }, diff --git a/src/setup/locales/tr/translation.json b/src/setup/locales/tr/translation.json index bab6cd1d..7b22a1c4 100644 --- a/src/setup/locales/tr/translation.json +++ b/src/setup/locales/tr/translation.json @@ -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" }, diff --git a/src/setup/locales/vi/translation.json b/src/setup/locales/vi/translation.json index d56b3595..d0c70768 100644 --- a/src/setup/locales/vi/translation.json +++ b/src/setup/locales/vi/translation.json @@ -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ì!", diff --git a/src/setup/locales/zh/translation.json b/src/setup/locales/zh/translation.json index 39b0998d..83f7e8a7 100644 --- a/src/setup/locales/zh/translation.json +++ b/src/setup/locales/zh/translation.json @@ -3,8 +3,6 @@ "name": "movie-web" }, "search": { - "loading_series": "正在获取您最喜欢的连续剧……", - "loading_movie": "正在获取您最喜欢的影片……", "loading": "载入中……", "allResults": "以上是我们能找到的所有结果!", "noResults": "我们找不到任何结果!", diff --git a/src/state/watched/migrations/v2.ts b/src/state/watched/migrations/v2.ts index 94f1141b..2e92224e 100644 --- a/src/state/watched/migrations/v2.ts +++ b/src/state/watched/migrations/v2.ts @@ -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) => diff --git a/src/state/watched/migrations/v3.ts b/src/state/watched/migrations/v3.ts index dffae637..f546c943 100644 --- a/src/state/watched/migrations/v3.ts +++ b/src/state/watched/migrations/v3.ts @@ -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 diff --git a/src/views/search/SearchLoadingView.tsx b/src/views/search/SearchLoadingView.tsx index 45971860..0c59f5d9 100644 --- a/src/views/search/SearchLoadingView.tsx +++ b/src/views/search/SearchLoadingView.tsx @@ -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 ( - + ); } diff --git a/tailwind.config.js b/tailwind.config.js index e22b3b3a..3a6816a6 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -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")] }; diff --git a/vite.config.ts b/vite.config.ts index 77a83ad2..ce5746ad 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,91 +4,104 @@ 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({ - plugins: [ - react({ - babel: { - presets: [ - "@babel/preset-typescript", - [ - "@babel/preset-env", - { - modules: false, - useBuiltIns: "entry", - corejs: { - version: "3.29", +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: [ + "@babel/preset-typescript", + [ + "@babel/preset-env", + { + modules: false, + useBuiltIns: "entry", + corejs: { + version: "3.29", + }, }, + ], + ], + }, + }), + VitePWA({ + registerType: "autoUpdate", + workbox: { + globIgnores: ["**ping.txt**"], + }, + includeAssets: [ + "favicon.ico", + "apple-touch-icon.png", + "safari-pinned-tab.svg", + ], + manifest: { + name: "movie-web", + short_name: "movie-web", + description: "The place for your favourite movies & shows", + theme_color: "#120f1d", + background_color: "#120f1d", + display: "standalone", + start_url: "/", + icons: [ + { + src: "android-chrome-192x192.png", + sizes: "192x192", + type: "image/png", + purpose: "any", + }, + { + src: "android-chrome-512x512.png", + sizes: "512x512", + type: "image/png", + purpose: "any", + }, + { + src: "android-chrome-192x192.png", + sizes: "192x192", + type: "image/png", + purpose: "maskable", + }, + { + src: "android-chrome-512x512.png", + sizes: "512x512", + type: "image/png", + purpose: "maskable", }, ], - ], - }, - }), - VitePWA({ - registerType: "autoUpdate", - workbox: { - globIgnores: ["**ping.txt**"], - }, - includeAssets: [ - "favicon.ico", - "apple-touch-icon.png", - "safari-pinned-tab.svg", - ], - manifest: { - name: "movie-web", - short_name: "movie-web", - description: "The place for your favourite movies & shows", - theme_color: "#120f1d", - background_color: "#120f1d", - display: "standalone", - start_url: "/", - icons: [ - { - src: "android-chrome-192x192.png", - sizes: "192x192", - type: "image/png", - purpose: "any", - }, - { - src: "android-chrome-512x512.png", - sizes: "512x512", - type: "image/png", - purpose: "any", - }, - { - src: "android-chrome-192x192.png", - sizes: "192x192", - type: "image/png", - purpose: "maskable", - }, - { - src: "android-chrome-512x512.png", - sizes: "512x512", - type: "image/png", - purpose: "maskable", - }, - ], - }, - }), - 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"], }, + }), + 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: { + "@": path.resolve(__dirname, "./src"), }, - }), - ], - resolve: { - alias: { - "@": path.resolve(__dirname, "./src"), }, - }, - test: { - environment: "jsdom", - }, + test: { + environment: "jsdom", + }, + }; }); diff --git a/yarn.lock b/yarn.lock index 5d687f2a..8b7759b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"