mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-20 14:37:43 +01:00
translations 🎉
Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
parent
ad518a6508
commit
4f682d55a9
27 changed files with 2796 additions and 2874 deletions
|
@ -73,8 +73,6 @@
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-react": "7.29.4",
|
"eslint-plugin-react": "7.29.4",
|
||||||
"eslint-plugin-react-hooks": "4.3.0",
|
"eslint-plugin-react-hooks": "4.3.0",
|
||||||
"i": "^0.3.7",
|
|
||||||
"npm": "^9.2.0",
|
|
||||||
"postcss": "^8.4.20",
|
"postcss": "^8.4.20",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.1.7",
|
"prettier-plugin-tailwindcss": "^0.1.7",
|
||||||
|
|
|
@ -20,7 +20,7 @@ registerProvider({
|
||||||
type: [MWMediaType.MOVIE, MWMediaType.SERIES],
|
type: [MWMediaType.MOVIE, MWMediaType.SERIES],
|
||||||
|
|
||||||
async scrape({ media, episode, progress }) {
|
async scrape({ media, episode, progress }) {
|
||||||
// // search for relevant item
|
// search for relevant item
|
||||||
const searchResponse = await proxiedFetch<any>(
|
const searchResponse = await proxiedFetch<any>(
|
||||||
`/api/search?keyword=${encodeURIComponent(media.meta.title)}`,
|
`/api/search?keyword=${encodeURIComponent(media.meta.title)}`,
|
||||||
{
|
{
|
||||||
|
|
|
@ -67,12 +67,7 @@ export function SearchBarInput(props: SearchBarProps) {
|
||||||
id: MWMediaType.SERIES,
|
id: MWMediaType.SERIES,
|
||||||
name: t("searchBar.series"),
|
name: t("searchBar.series"),
|
||||||
icon: Icons.CLAPPER_BOARD,
|
icon: Icons.CLAPPER_BOARD,
|
||||||
},
|
}
|
||||||
// {
|
|
||||||
// id: MWMediaType.ANIME,
|
|
||||||
// name: "Anime",
|
|
||||||
// icon: Icons.DRAGON,
|
|
||||||
// },
|
|
||||||
]}
|
]}
|
||||||
onClick={() => setDropdownOpen((old) => !old)}
|
onClick={() => setDropdownOpen((old) => !old)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { ButtonControl } from "./ButtonControl";
|
import { ButtonControl } from "./ButtonControl";
|
||||||
|
|
||||||
export interface EditButtonProps {
|
export interface EditButtonProps {
|
||||||
|
@ -9,6 +10,7 @@ export interface EditButtonProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditButton(props: EditButtonProps) {
|
export function EditButton(props: EditButtonProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
const [parent] = useAutoAnimate<HTMLSpanElement>();
|
const [parent] = useAutoAnimate<HTMLSpanElement>();
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
|
@ -22,7 +24,7 @@ export function EditButton(props: EditButtonProps) {
|
||||||
>
|
>
|
||||||
<span ref={parent}>
|
<span ref={parent}>
|
||||||
{props.editing ? (
|
{props.editing ? (
|
||||||
<span className="mx-4 whitespace-nowrap">Stop editing</span>
|
<span className="mx-4 whitespace-nowrap">{t("media.stopEditing")}</span>
|
||||||
) : (
|
) : (
|
||||||
<Icon icon={Icons.EDIT} />
|
<Icon icon={Icons.EDIT} />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { Icons } from "@/components/Icon";
|
||||||
import { Link } from "@/components/text/Link";
|
import { Link } from "@/components/text/Link";
|
||||||
import { Title } from "@/components/text/Title";
|
import { Title } from "@/components/text/Title";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface ErrorShowcaseProps {
|
interface ErrorShowcaseProps {
|
||||||
error: {
|
error: {
|
||||||
|
@ -35,29 +36,24 @@ interface ErrorMessageProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ErrorMessage(props: ErrorMessageProps) {
|
export function ErrorMessage(props: ErrorMessageProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${props.localSize ? "h-full" : "min-h-screen"
|
||||||
props.localSize ? "h-full" : "min-h-screen"
|
} flex w-full flex-col items-center justify-center px-4 py-12`}
|
||||||
} flex w-full flex-col items-center justify-center px-4 py-12`}
|
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center justify-start text-center">
|
<div className="flex flex-col items-center justify-start text-center">
|
||||||
<IconPatch icon={Icons.WARNING} className="mb-6 text-red-400" />
|
<IconPatch icon={Icons.WARNING} className="mb-6 text-red-400" />
|
||||||
<Title>Whoops, it broke</Title>
|
<Title>{t("media.errors.genericTitle")}</Title>
|
||||||
{props.children ? (
|
{props.children ? (
|
||||||
<p className="my-6 max-w-lg">{props.children}</p>
|
<p className="my-6 max-w-lg">{props.children}</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="my-6 max-w-lg">
|
<p className="my-6 max-w-lg">
|
||||||
The app encountered an error and wasn't able to recover, please
|
<Trans i18nKey="media.errors.videoFailed">
|
||||||
report it to the{" "}
|
<Link url={conf().DISCORD_LINK} newTab />
|
||||||
<Link url={conf().DISCORD_LINK} newTab>
|
<Link url={conf().GITHUB_LINK} newTab />
|
||||||
Discord server
|
</Trans>
|
||||||
</Link>{" "}
|
|
||||||
or on{" "}
|
|
||||||
<Link url={conf().GITHUB_LINK} newTab>
|
|
||||||
GitHub
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,109 +0,0 @@
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useHistory } from "react-router-dom";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { IconPatch } from "@/components/buttons/IconPatch";
|
|
||||||
import { Dropdown, OptionItem } from "@/components/Dropdown";
|
|
||||||
import { Icons } from "@/components/Icon";
|
|
||||||
import { WatchedEpisode } from "@/components/media/WatchedEpisodeButton";
|
|
||||||
import { useLoading } from "@/hooks/useLoading";
|
|
||||||
import { serializePortableMedia } from "@/hooks/usePortableMedia";
|
|
||||||
|
|
||||||
export interface SeasonsProps {
|
|
||||||
media: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LoadingSeasons(props: { error?: boolean }) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<div className="mb-3 mt-5 h-10 w-56 rounded bg-denim-400 opacity-50" />
|
|
||||||
</div>
|
|
||||||
{!props.error ? (
|
|
||||||
<>
|
|
||||||
<div className="mr-3 mb-3 inline-block h-10 w-10 rounded bg-denim-400 opacity-50" />
|
|
||||||
<div className="mr-3 mb-3 inline-block h-10 w-10 rounded bg-denim-400 opacity-50" />
|
|
||||||
<div className="mr-3 mb-3 inline-block h-10 w-10 rounded bg-denim-400 opacity-50" />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<IconPatch icon={Icons.WARNING} className="text-red-400" />
|
|
||||||
<p>{t("seasons.failed")}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Seasons(props: SeasonsProps) {
|
|
||||||
// const { t } = useTranslation();
|
|
||||||
// const [searchSeasons, loading, error, success] = useLoading(
|
|
||||||
// (portableMedia: MWPortableMedia) => getSeasonDataFromMedia(portableMedia)
|
|
||||||
// );
|
|
||||||
// const history = useHistory();
|
|
||||||
// const [seasons, setSeasons] = useState<MWMediaSeasons>({ seasons: [] });
|
|
||||||
// const seasonSelected = props.media.seasonId as string;
|
|
||||||
// const episodeSelected = props.media.episodeId as string;
|
|
||||||
// useEffect(() => {
|
|
||||||
// (async () => {
|
|
||||||
// const seasonData = await searchSeasons(props.media);
|
|
||||||
// setSeasons(seasonData);
|
|
||||||
// })();
|
|
||||||
// }, [searchSeasons, props.media]);
|
|
||||||
// function navigateToSeasonAndEpisode(seasonId: string, episodeId: string) {
|
|
||||||
// const newMedia: MWMedia = { ...props.media };
|
|
||||||
// newMedia.episodeId = episodeId;
|
|
||||||
// newMedia.seasonId = seasonId;
|
|
||||||
// history.replace(
|
|
||||||
// `/media/${newMedia.mediaType}/${serializePortableMedia(
|
|
||||||
// convertMediaToPortable(newMedia)
|
|
||||||
// )}`
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// const mapSeason = (season: MWMediaSeason) => ({
|
|
||||||
// id: season.id,
|
|
||||||
// name: season.title || `${t("seasons.season", { season: season.sort })}`,
|
|
||||||
// });
|
|
||||||
// const options = seasons.seasons.map(mapSeason);
|
|
||||||
// const foundSeason = seasons.seasons.find(
|
|
||||||
// (season) => season.id === seasonSelected
|
|
||||||
// );
|
|
||||||
// const selectedItem = foundSeason ? mapSeason(foundSeason) : null;
|
|
||||||
// return (
|
|
||||||
// <>
|
|
||||||
// {loading ? <LoadingSeasons /> : null}
|
|
||||||
// {error ? <LoadingSeasons error /> : null}
|
|
||||||
// {success && seasons.seasons.length ? (
|
|
||||||
// <>
|
|
||||||
// <Dropdown
|
|
||||||
// selectedItem={selectedItem as OptionItem}
|
|
||||||
// options={options}
|
|
||||||
// setSelectedItem={(seasonItem) =>
|
|
||||||
// navigateToSeasonAndEpisode(
|
|
||||||
// seasonItem.id,
|
|
||||||
// seasons.seasons.find((s) => s.id === seasonItem.id)?.episodes[0]
|
|
||||||
// .id as string
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// />
|
|
||||||
// {seasons.seasons
|
|
||||||
// .find((s) => s.id === seasonSelected)
|
|
||||||
// ?.episodes.map((v) => (
|
|
||||||
// <WatchedEpisode
|
|
||||||
// key={v.id}
|
|
||||||
// media={{
|
|
||||||
// ...props.media,
|
|
||||||
// seriesData: seasons,
|
|
||||||
// episodeId: v.id,
|
|
||||||
// seasonId: seasonSelected,
|
|
||||||
// }}
|
|
||||||
// active={v.id === episodeSelected}
|
|
||||||
// onClick={() => navigateToSeasonAndEpisode(seasonSelected, v.id)}
|
|
||||||
// />
|
|
||||||
// ))}
|
|
||||||
// </>
|
|
||||||
// ) : null}
|
|
||||||
// </>
|
|
||||||
// );
|
|
||||||
}
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { DotList } from "@/components/text/DotList";
|
import { DotList } from "@/components/text/DotList";
|
||||||
import { MWMediaMeta } from "@/backend/metadata/types";
|
import { MWMediaMeta } from "@/backend/metadata/types";
|
||||||
import { JWMediaToId } from "@/backend/metadata/justwatch";
|
import { JWMediaToId } from "@/backend/metadata/justwatch";
|
||||||
|
@ -27,20 +28,19 @@ function MediaCardContent({
|
||||||
closable,
|
closable,
|
||||||
onClose,
|
onClose,
|
||||||
}: MediaCardProps) {
|
}: MediaCardProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const percentageString = `${Math.round(percentage ?? 0).toFixed(0)}%`;
|
const percentageString = `${Math.round(percentage ?? 0).toFixed(0)}%`;
|
||||||
|
|
||||||
const canLink = linkable && !closable;
|
const canLink = linkable && !closable;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`group -m-3 mb-2 rounded-xl bg-denim-300 bg-opacity-0 transition-colors duration-100 ${
|
className={`group -m-3 mb-2 rounded-xl bg-denim-300 bg-opacity-0 transition-colors duration-100 ${canLink ? "hover:bg-opacity-100" : ""
|
||||||
canLink ? "hover:bg-opacity-100" : ""
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<article
|
<article
|
||||||
className={`pointer-events-auto relative mb-2 p-3 transition-transform duration-100 ${
|
className={`pointer-events-auto relative mb-2 p-3 transition-transform duration-100 ${canLink ? "group-hover:scale-95" : ""
|
||||||
canLink ? "group-hover:scale-95" : ""
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="relative mb-4 aspect-[2/3] w-full overflow-hidden rounded-xl bg-denim-500 bg-cover bg-center transition-[border-radius] duration-100 group-hover:rounded-lg"
|
className="relative mb-4 aspect-[2/3] w-full overflow-hidden rounded-xl bg-denim-500 bg-cover bg-center transition-[border-radius] duration-100 group-hover:rounded-lg"
|
||||||
|
@ -51,7 +51,10 @@ function MediaCardContent({
|
||||||
{series ? (
|
{series ? (
|
||||||
<div className="absolute right-2 top-2 rounded-md bg-denim-200 py-1 px-2 transition-colors group-hover:bg-denim-500">
|
<div className="absolute right-2 top-2 rounded-md bg-denim-200 py-1 px-2 transition-colors group-hover:bg-denim-500">
|
||||||
<p className="text-center text-xs font-bold text-slate-400 transition-colors group-hover:text-white">
|
<p className="text-center text-xs font-bold text-slate-400 transition-colors group-hover:text-white">
|
||||||
S{series.season} E{series.episode}
|
{t("seasons.seasonAndEpisode", {
|
||||||
|
season: series.season,
|
||||||
|
episode: series.episode
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -59,14 +62,12 @@ function MediaCardContent({
|
||||||
{percentage !== undefined ? (
|
{percentage !== undefined ? (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-denim-300 to-transparent transition-colors ${
|
className={`absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-denim-300 to-transparent transition-colors ${canLink ? "group-hover:from-denim-100" : ""
|
||||||
canLink ? "group-hover:from-denim-100" : ""
|
}`}
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-denim-300 to-transparent transition-colors ${
|
className={`absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-denim-300 to-transparent transition-colors ${canLink ? "group-hover:from-denim-100" : ""
|
||||||
canLink ? "group-hover:from-denim-100" : ""
|
}`}
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-x-0 bottom-0 p-3">
|
<div className="absolute inset-x-0 bottom-0 p-3">
|
||||||
<div className="relative h-1 overflow-hidden rounded-full bg-denim-600">
|
<div className="relative h-1 overflow-hidden rounded-full bg-denim-600">
|
||||||
|
@ -82,9 +83,8 @@ function MediaCardContent({
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`absolute inset-0 flex items-center justify-center bg-denim-200 bg-opacity-80 transition-opacity duration-200 ${
|
className={`absolute inset-0 flex items-center justify-center bg-denim-200 bg-opacity-80 transition-opacity duration-200 ${closable ? "opacity-100" : "pointer-events-none opacity-0"
|
||||||
closable ? "opacity-100" : "pointer-events-none opacity-0"
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<IconPatch
|
<IconPatch
|
||||||
clickable
|
clickable
|
||||||
|
@ -100,7 +100,7 @@ function MediaCardContent({
|
||||||
<DotList
|
<DotList
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
content={[
|
content={[
|
||||||
media.type.slice(0, 1).toUpperCase() + media.type.slice(1),
|
t(`media.${media.type}`),
|
||||||
media.year,
|
media.year,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -21,12 +21,9 @@ initializeChromecast();
|
||||||
|
|
||||||
// TODO video todos:
|
// TODO video todos:
|
||||||
// - chrome cast support
|
// - chrome cast support
|
||||||
// - bug: unmounting player throws errors in console
|
|
||||||
// - bug: safari fullscreen will make video overlap player controls
|
// - bug: safari fullscreen will make video overlap player controls
|
||||||
// - improvement: make scrapers use fuzzy matching on normalized titles
|
// - improvement: make scrapers use fuzzy matching on normalized titles
|
||||||
// - bug: source selection doesnt work with HLS
|
|
||||||
// - bug: .ass subtitle files are fucked
|
// - bug: .ass subtitle files are fucked
|
||||||
// - improvement: episode watch at the ending should not startAt
|
|
||||||
|
|
||||||
// TODO stuff to test:
|
// TODO stuff to test:
|
||||||
// - browser: firefox, chrome, edge, safari desktop
|
// - browser: firefox, chrome, edge, safari desktop
|
||||||
|
|
|
@ -44,3 +44,8 @@ body[data-no-select] {
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
google-cast-launcher {
|
||||||
|
@apply pointer-events-auto m-2 text-white flex items-center justify-center p-2;
|
||||||
|
@apply transition-[background-color,transform] duration-100 rounded-full bg-denim-600 bg-opacity-0 hover:bg-opacity-50 active:bg-denim-500 active:bg-opacity-100 active:scale-110;
|
||||||
|
}
|
||||||
|
|
|
@ -3,26 +3,34 @@
|
||||||
"name": "movie-web"
|
"name": "movie-web"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"loading": "Fetching your favourite shows...",
|
"loading_series": "Fetching your favourite series...",
|
||||||
|
"loading_movie": "Fetching your favourite movies...",
|
||||||
|
"loading": "Loading...",
|
||||||
"allResults": "That's all we have!",
|
"allResults": "That's all we have!",
|
||||||
"noResults": "We couldn't find anything!",
|
"noResults": "We couldn't find anything!",
|
||||||
"allFailed": "Failed to find media, try again!",
|
"allFailed": "Failed to find media, try again!",
|
||||||
"headingTitle": "Search results",
|
"headingTitle": "Search results",
|
||||||
"headingLink": "Back to home",
|
|
||||||
"bookmarks": "Bookmarks",
|
"bookmarks": "Bookmarks",
|
||||||
"continueWatching": "Continue Watching",
|
"continueWatching": "Continue Watching",
|
||||||
"title": "What do you want to watch?",
|
"title": "What do you want to watch?",
|
||||||
"placeholder": "What do you want to watch?"
|
"placeholder": "What do you want to watch?"
|
||||||
},
|
},
|
||||||
"media": {
|
"media": {
|
||||||
"invalidUrl": "Your URL may be invalid",
|
"movie": "Movie",
|
||||||
"arrowText": "Go back"
|
"series": "Series",
|
||||||
|
"stopEditing": "Stop editing",
|
||||||
|
"errors": {
|
||||||
|
"genericTitle": "Whoops, it broke!",
|
||||||
|
"failedMeta": "Failed to load meta",
|
||||||
|
"mediaFailed": "We failed to request the media you asked for, check your internet connection and try again.",
|
||||||
|
"videoFailed": "We encountered an error while playing the video you requested. If this keeps happening please report the issue to the <0>Discord server</0> or on <1>GitHub</1>."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"seasons": {
|
"seasons": {
|
||||||
"season": "Season {{season}}",
|
"seasonAndEpisode": "S{{season}} E{{episode}}"
|
||||||
"failed": "Failed to get season data"
|
|
||||||
},
|
},
|
||||||
"notFound": {
|
"notFound": {
|
||||||
|
"genericTitle": "Not found",
|
||||||
"backArrow": "Back to home",
|
"backArrow": "Back to home",
|
||||||
"media": {
|
"media": {
|
||||||
"title": "Couldn't find that media",
|
"title": "Couldn't find that media",
|
||||||
|
@ -42,7 +50,31 @@
|
||||||
"series": "Series",
|
"series": "Series",
|
||||||
"Search": "Search"
|
"Search": "Search"
|
||||||
},
|
},
|
||||||
"errorBoundary": {
|
"videoPlayer": {
|
||||||
"text": "The app encountered an error and wasn't able to recover, please report it to the"
|
"findingBestVideo": "Finding the best video for you",
|
||||||
|
"noVideos": "Whoops, couldn't find any videos for you",
|
||||||
|
"loading": "Loading...",
|
||||||
|
"backToHome": "Back to home",
|
||||||
|
"seasonAndEpisode": "S{{season}} E{{episode}}",
|
||||||
|
"buttons": {
|
||||||
|
"episodes": "Episodes",
|
||||||
|
"source": "Source",
|
||||||
|
"captions": "Captions"
|
||||||
|
},
|
||||||
|
"popouts": {
|
||||||
|
"sources": "Sources",
|
||||||
|
"seasons": "Seasons",
|
||||||
|
"captions": "Captions",
|
||||||
|
"episode": "E{{index}} - {{title}}",
|
||||||
|
"noCaptions": "No captions",
|
||||||
|
"linkedCaptions": "Linked captions",
|
||||||
|
"errors": {
|
||||||
|
"loadingWentWong": "Something went wrong loading the episodes for {{seasonTitle}}",
|
||||||
|
"embedsError": "Something went wrong loading the embeds for this thing that you like"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"fatalError": "The video player encounted a fatal error, please report it to the <0>Discord server</0> or on <1>GitHub</1>."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,14 @@ import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconB
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { PopoutAnchor } from "@/video/components/popouts/PopoutAnchor";
|
import { PopoutAnchor } from "@/video/components/popouts/PopoutAnchor";
|
||||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CaptionsSelectionAction(props: Props) {
|
export function CaptionsSelectionAction(props: Props) {
|
||||||
|
const { t } = useTranslation()
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
const controls = useControls(descriptor);
|
const controls = useControls(descriptor);
|
||||||
const { isMobile } = useIsMobile();
|
const { isMobile } = useIsMobile();
|
||||||
|
@ -20,7 +22,7 @@ export function CaptionsSelectionAction(props: Props) {
|
||||||
<PopoutAnchor for="captions">
|
<PopoutAnchor for="captions">
|
||||||
<VideoPlayerIconButton
|
<VideoPlayerIconButton
|
||||||
className={props.className}
|
className={props.className}
|
||||||
text={isMobile ? "Captions" : ""}
|
text={isMobile ? t("videoPlayer.buttons.captions") as string : ""}
|
||||||
wide={isMobile}
|
wide={isMobile}
|
||||||
onClick={() => controls.openPopout("captions")}
|
onClick={() => controls.openPopout("captions")}
|
||||||
icon={Icons.CAPTIONS}
|
icon={Icons.CAPTIONS}
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
||||||
import { useMisc } from "@/video/state/logic/misc";
|
import { useMisc } from "@/video/state/logic/misc";
|
||||||
|
|
||||||
// TODO pausing before first frame will infinitely show spinner until unpaused
|
|
||||||
export function LoadingAction() {
|
export function LoadingAction() {
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
const mediaPlaying = useMediaPlaying(descriptor);
|
const mediaPlaying = useMediaPlaying(descriptor);
|
||||||
|
|
|
@ -6,12 +6,14 @@ import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconB
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { PopoutAnchor } from "@/video/components/popouts/PopoutAnchor";
|
import { PopoutAnchor } from "@/video/components/popouts/PopoutAnchor";
|
||||||
import { useInterface } from "@/video/state/logic/interface";
|
import { useInterface } from "@/video/state/logic/interface";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SeriesSelectionAction(props: Props) {
|
export function SeriesSelectionAction(props: Props) {
|
||||||
|
const { t } = useTranslation()
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
const meta = useMeta(descriptor);
|
const meta = useMeta(descriptor);
|
||||||
const videoInterface = useInterface(descriptor);
|
const videoInterface = useInterface(descriptor);
|
||||||
|
@ -26,7 +28,7 @@ export function SeriesSelectionAction(props: Props) {
|
||||||
<VideoPlayerIconButton
|
<VideoPlayerIconButton
|
||||||
active={videoInterface.popout === "episodes"}
|
active={videoInterface.popout === "episodes"}
|
||||||
icon={Icons.EPISODES}
|
icon={Icons.EPISODES}
|
||||||
text="Episodes"
|
text={t("videoPlayer.buttons.episodes") as string}
|
||||||
wide
|
wide
|
||||||
onClick={() => controls.openPopout("episodes")}
|
onClick={() => controls.openPopout("episodes")}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -4,12 +4,14 @@ import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconB
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { PopoutAnchor } from "@/video/components/popouts/PopoutAnchor";
|
import { PopoutAnchor } from "@/video/components/popouts/PopoutAnchor";
|
||||||
import { useInterface } from "@/video/state/logic/interface";
|
import { useInterface } from "@/video/state/logic/interface";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SourceSelectionAction(props: Props) {
|
export function SourceSelectionAction(props: Props) {
|
||||||
|
const { t } = useTranslation()
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
const videoInterface = useInterface(descriptor);
|
const videoInterface = useInterface(descriptor);
|
||||||
const controls = useControls(descriptor);
|
const controls = useControls(descriptor);
|
||||||
|
@ -20,8 +22,9 @@ export function SourceSelectionAction(props: Props) {
|
||||||
<PopoutAnchor for="source">
|
<PopoutAnchor for="source">
|
||||||
<VideoPlayerIconButton
|
<VideoPlayerIconButton
|
||||||
active={videoInterface.popout === "source"}
|
active={videoInterface.popout === "source"}
|
||||||
icon={Icons.FILE}
|
icon={Icons.CLAPPER_BOARD}
|
||||||
text="Source"
|
iconSize="text-xl"
|
||||||
|
text={t("videoPlayer.buttons.source") as string}
|
||||||
wide
|
wide
|
||||||
onClick={() => controls.openPopout("source")}
|
onClick={() => controls.openPopout("source")}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { MWMediaType } from "@/backend/metadata/types";
|
import { MWMediaType } from "@/backend/metadata/types";
|
||||||
import { useMeta } from "@/video/state/logic/meta";
|
import { useMeta } from "@/video/state/logic/meta";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export function useCurrentSeriesEpisodeInfo(descriptor: string) {
|
export function useCurrentSeriesEpisodeInfo(descriptor: string) {
|
||||||
const meta = useMeta(descriptor);
|
const meta = useMeta(descriptor);
|
||||||
|
const {t} = useTranslation()
|
||||||
|
|
||||||
const currentSeasonInfo = useMemo(() => {
|
const currentSeasonInfo = useMemo(() => {
|
||||||
return meta?.seasons?.find(
|
return meta?.seasons?.find(
|
||||||
|
@ -22,8 +24,11 @@ export function useCurrentSeriesEpisodeInfo(descriptor: string) {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isSeries) return { isSeries: false };
|
if (!isSeries) return { isSeries: false };
|
||||||
|
|
||||||
const humanizedEpisodeId = `S${currentSeasonInfo?.number} E${currentEpisodeInfo?.number}`;
|
const humanizedEpisodeId = t("videoPlayer.seasonAndEpisode", {
|
||||||
|
season: currentSeasonInfo?.number,
|
||||||
|
episode: currentEpisodeInfo?.number
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isSeries: true,
|
isSeries: true,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { ErrorMessage } from "@/components/layout/ErrorBoundary";
|
||||||
import { Link } from "@/components/text/Link";
|
import { Link } from "@/components/text/Link";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
import { Component, ReactNode } from "react";
|
import { Component, ReactNode } from "react";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
import { VideoPlayerHeader } from "./VideoPlayerHeader";
|
import { VideoPlayerHeader } from "./VideoPlayerHeader";
|
||||||
|
|
||||||
interface ErrorBoundaryState {
|
interface ErrorBoundaryState {
|
||||||
|
@ -67,15 +68,10 @@ export class VideoErrorBoundary extends Component<
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ErrorMessage error={this.state.error} localSize>
|
<ErrorMessage error={this.state.error} localSize>
|
||||||
The video player encounted a fatal error, please report it to the{" "}
|
<Trans i18nKey="videoPlayer.errors.fatalError">
|
||||||
<Link url={conf().DISCORD_LINK} newTab>
|
<Link url={conf().DISCORD_LINK} newTab />
|
||||||
Discord server
|
<Link url={conf().GITHUB_LINK} newTab />
|
||||||
</Link>{" "}
|
</Trans>
|
||||||
or on{" "}
|
|
||||||
<Link url={conf().GITHUB_LINK} newTab>
|
|
||||||
GitHub
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</ErrorMessage>
|
</ErrorMessage>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
} from "@/state/bookmark";
|
} from "@/state/bookmark";
|
||||||
import { AirplayAction } from "@/video/components/actions/AirplayAction";
|
import { AirplayAction } from "@/video/components/actions/AirplayAction";
|
||||||
import { ChromecastAction } from "@/video/components/actions/ChromecastAction";
|
import { ChromecastAction } from "@/video/components/actions/ChromecastAction";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface VideoPlayerHeaderProps {
|
interface VideoPlayerHeaderProps {
|
||||||
media?: MWMediaMeta;
|
media?: MWMediaMeta;
|
||||||
|
@ -21,6 +22,8 @@ export function VideoPlayerHeader(props: VideoPlayerHeaderProps) {
|
||||||
? getIfBookmarkedFromPortable(bookmarkStore.bookmarks, props.media)
|
? getIfBookmarkedFromPortable(bookmarkStore.bookmarks, props.media)
|
||||||
: false;
|
: false;
|
||||||
const showDivider = props.media && props.onClick;
|
const showDivider = props.media && props.onClick;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="flex flex-1 items-center">
|
<div className="flex flex-1 items-center">
|
||||||
|
@ -31,7 +34,7 @@ export function VideoPlayerHeader(props: VideoPlayerHeaderProps) {
|
||||||
className="flex cursor-pointer items-center py-1 text-white opacity-50 transition-opacity hover:opacity-100"
|
className="flex cursor-pointer items-center py-1 text-white opacity-50 transition-opacity hover:opacity-100"
|
||||||
>
|
>
|
||||||
<Icon className="mr-2" icon={Icons.ARROW_LEFT} />
|
<Icon className="mr-2" icon={Icons.ARROW_LEFT} />
|
||||||
<span>Back to home</span>
|
<span>{t("videoPlayer.backToHome")}</span>
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
{showDivider ? (
|
{showDivider ? (
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { useControls } from "@/video/state/logic/controls";
|
||||||
import { useMeta } from "@/video/state/logic/meta";
|
import { useMeta } from "@/video/state/logic/meta";
|
||||||
import { useSource } from "@/video/state/logic/source";
|
import { useSource } from "@/video/state/logic/source";
|
||||||
import { useMemo, useRef } from "react";
|
import { useMemo, useRef } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
|
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
|
||||||
|
|
||||||
function makeCaptionId(caption: MWCaption, isLinked: boolean): string {
|
function makeCaptionId(caption: MWCaption, isLinked: boolean): string {
|
||||||
|
@ -14,6 +15,8 @@ function makeCaptionId(caption: MWCaption, isLinked: boolean): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CaptionSelectionPopout() {
|
export function CaptionSelectionPopout() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
const meta = useMeta(descriptor);
|
const meta = useMeta(descriptor);
|
||||||
const source = useSource(descriptor);
|
const source = useSource(descriptor);
|
||||||
|
@ -38,7 +41,7 @@ export function CaptionSelectionPopout() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PopoutSection className="bg-ash-100 font-bold text-white">
|
<PopoutSection className="bg-ash-100 font-bold text-white">
|
||||||
<div>Captions</div>
|
<div>{t("videoPlayer.popouts.captions")}</div>
|
||||||
</PopoutSection>
|
</PopoutSection>
|
||||||
<div className="relative overflow-y-auto">
|
<div className="relative overflow-y-auto">
|
||||||
<PopoutSection>
|
<PopoutSection>
|
||||||
|
@ -49,13 +52,13 @@ export function CaptionSelectionPopout() {
|
||||||
controls.closePopout();
|
controls.closePopout();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
No captions
|
{t("videoPlayer.popouts.noCaptions")}
|
||||||
</PopoutListEntry>
|
</PopoutListEntry>
|
||||||
</PopoutSection>
|
</PopoutSection>
|
||||||
|
|
||||||
<p className="sticky top-0 z-10 flex items-center space-x-1 bg-ash-200 px-5 py-3 text-sm font-bold uppercase">
|
<p className="sticky top-0 z-10 flex items-center space-x-1 bg-ash-200 px-5 py-3 text-sm font-bold uppercase">
|
||||||
<Icon className="text-base" icon={Icons.LINK} />
|
<Icon className="text-base" icon={Icons.LINK} />
|
||||||
<span>Linked captions</span>
|
<span>{t("videoPlayer.popouts.linkedCaptions")}</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<PopoutSection className="pt-0">
|
<PopoutSection className="pt-0">
|
||||||
|
|
|
@ -12,11 +12,14 @@ import { useMeta } from "@/video/state/logic/meta";
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { useWatchedContext } from "@/state/watched";
|
import { useWatchedContext } from "@/state/watched";
|
||||||
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
|
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export function EpisodeSelectionPopout() {
|
export function EpisodeSelectionPopout() {
|
||||||
const params = useParams<{
|
const params = useParams<{
|
||||||
media: string;
|
media: string;
|
||||||
}>();
|
}>();
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
const meta = useMeta(descriptor);
|
const meta = useMeta(descriptor);
|
||||||
const controls = useControls(descriptor);
|
const controls = useControls(descriptor);
|
||||||
|
@ -119,7 +122,7 @@ export function EpisodeSelectionPopout() {
|
||||||
isPickingSeason ? "opacity-1" : "opacity-0",
|
isPickingSeason ? "opacity-1" : "opacity-0",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
Seasons
|
{t("videoPlayer.popouts.seasons")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</PopoutSection>
|
</PopoutSection>
|
||||||
|
@ -134,15 +137,15 @@ export function EpisodeSelectionPopout() {
|
||||||
>
|
>
|
||||||
{currentSeasonInfo
|
{currentSeasonInfo
|
||||||
? meta?.seasons?.map?.((season) => (
|
? meta?.seasons?.map?.((season) => (
|
||||||
<PopoutListEntry
|
<PopoutListEntry
|
||||||
key={season.id}
|
key={season.id}
|
||||||
active={meta?.episode?.seasonId === season.id}
|
active={meta?.episode?.seasonId === season.id}
|
||||||
onClick={() => setSeason(season.id)}
|
onClick={() => setSeason(season.id)}
|
||||||
isOnDarkBackground
|
isOnDarkBackground
|
||||||
>
|
>
|
||||||
{season.title}
|
{season.title}
|
||||||
</PopoutListEntry>
|
</PopoutListEntry>
|
||||||
))
|
))
|
||||||
: "No season"}
|
: "No season"}
|
||||||
</PopoutSection>
|
</PopoutSection>
|
||||||
<PopoutSection className="relative h-full overflow-y-auto">
|
<PopoutSection className="relative h-full overflow-y-auto">
|
||||||
|
@ -158,8 +161,9 @@ export function EpisodeSelectionPopout() {
|
||||||
className="text-xl text-bink-600"
|
className="text-xl text-bink-600"
|
||||||
/>
|
/>
|
||||||
<p className="mt-6 w-full text-center">
|
<p className="mt-6 w-full text-center">
|
||||||
Something went wrong loading the episodes for{" "}
|
{t("videoPLayer.popouts.errors.loadingWentWrong", {
|
||||||
{currentSeasonInfo?.title?.toLowerCase()}
|
seasonTitle: currentSeasonInfo?.title?.toLowerCase()
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -167,26 +171,29 @@ export function EpisodeSelectionPopout() {
|
||||||
<div>
|
<div>
|
||||||
{currentSeasonEpisodes && currentSeasonInfo
|
{currentSeasonEpisodes && currentSeasonInfo
|
||||||
? currentSeasonEpisodes.map((e) => (
|
? currentSeasonEpisodes.map((e) => (
|
||||||
<PopoutListEntry
|
<PopoutListEntry
|
||||||
key={e.id}
|
key={e.id}
|
||||||
active={e.id === meta?.episode?.episodeId}
|
active={e.id === meta?.episode?.episodeId}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (e.id === meta?.episode?.episodeId)
|
if (e.id === meta?.episode?.episodeId)
|
||||||
controls.closePopout();
|
controls.closePopout();
|
||||||
else setCurrent(currentSeasonInfo.id, e.id);
|
else setCurrent(currentSeasonInfo.id, e.id);
|
||||||
}}
|
}}
|
||||||
percentageCompleted={
|
percentageCompleted={
|
||||||
watched.items.find(
|
watched.items.find(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.item?.series?.seasonId ===
|
item.item?.series?.seasonId ===
|
||||||
currentSeasonInfo.id &&
|
currentSeasonInfo.id &&
|
||||||
item.item?.series?.episodeId === e.id
|
item.item?.series?.episodeId === e.id
|
||||||
)?.percentage
|
)?.percentage
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
E{e.number} - {e.title}
|
{t("videoPlayer.popouts.episode", {
|
||||||
</PopoutListEntry>
|
index: e.number,
|
||||||
))
|
title: e.title
|
||||||
|
})}
|
||||||
|
</PopoutListEntry>
|
||||||
|
))
|
||||||
: "No episodes"}
|
: "No episodes"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -11,9 +11,11 @@ import { getProviders } from "@/backend/helpers/register";
|
||||||
import { runProvider } from "@/backend/helpers/run";
|
import { runProvider } from "@/backend/helpers/run";
|
||||||
import { MWProviderScrapeResult } from "@/backend/helpers/provider";
|
import { MWProviderScrapeResult } from "@/backend/helpers/provider";
|
||||||
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
|
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
// TODO HLS does not work
|
|
||||||
export function SourceSelectionPopout() {
|
export function SourceSelectionPopout() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
const controls = useControls(descriptor);
|
const controls = useControls(descriptor);
|
||||||
const meta = useMeta(descriptor);
|
const meta = useMeta(descriptor);
|
||||||
|
@ -42,7 +44,7 @@ export function SourceSelectionPopout() {
|
||||||
tmdbId: "",
|
tmdbId: "",
|
||||||
meta: meta.meta,
|
meta: meta.meta,
|
||||||
},
|
},
|
||||||
progress: () => {},
|
progress: () => { },
|
||||||
type: meta.meta.type,
|
type: meta.meta.type,
|
||||||
episode: meta.episode?.episodeId as any,
|
episode: meta.episode?.episodeId as any,
|
||||||
season: meta.episode?.seasonId as any,
|
season: meta.episode?.seasonId as any,
|
||||||
|
@ -129,7 +131,7 @@ export function SourceSelectionPopout() {
|
||||||
!showingProvider ? "opacity-1" : "opacity-0",
|
!showingProvider ? "opacity-1" : "opacity-0",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
Sources
|
{t("videoPlayer.popouts.sources")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</PopoutSection>
|
</PopoutSection>
|
||||||
|
@ -154,8 +156,7 @@ export function SourceSelectionPopout() {
|
||||||
className="text-xl text-bink-600"
|
className="text-xl text-bink-600"
|
||||||
/>
|
/>
|
||||||
<p className="mt-6 w-full text-center">
|
<p className="mt-6 w-full text-center">
|
||||||
Something went wrong loading the embeds for this thing that
|
{t("videoPlayer.popouts.errors.embedsError")}
|
||||||
you like
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -249,6 +249,7 @@ export function createVideoStateProvider(
|
||||||
};
|
};
|
||||||
const canplay = () => {
|
const canplay = () => {
|
||||||
state.mediaPlaying.isFirstLoading = false;
|
state.mediaPlaying.isFirstLoading = false;
|
||||||
|
state.mediaPlaying.isLoading = false;
|
||||||
updateMediaPlaying(descriptor, state);
|
updateMediaPlaying(descriptor, state);
|
||||||
};
|
};
|
||||||
const fullscreenchange = () => {
|
const fullscreenchange = () => {
|
||||||
|
|
|
@ -5,48 +5,23 @@ import { useGoBack } from "@/hooks/useGoBack";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
import { VideoPlayerHeader } from "@/video/components/parts/VideoPlayerHeader";
|
import { VideoPlayerHeader } from "@/video/components/parts/VideoPlayerHeader";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export function MediaFetchErrorView() {
|
export function MediaFetchErrorView() {
|
||||||
|
const { t } = useTranslation()
|
||||||
const goBack = useGoBack();
|
const goBack = useGoBack();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex-1">
|
<div className="h-screen flex-1">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>Failed to load meta</title>
|
<title>{t("media.errors.failedMeta")}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="fixed inset-x-0 top-0 py-6 px-8">
|
<div className="fixed inset-x-0 top-0 py-6 px-8">
|
||||||
<VideoPlayerHeader onClick={goBack} />
|
<VideoPlayerHeader onClick={goBack} />
|
||||||
</div>
|
</div>
|
||||||
<ErrorMessage>
|
<ErrorMessage>
|
||||||
<p className="my-6 max-w-lg">
|
<p className="my-6 max-w-lg">
|
||||||
We failed to request the media you asked for, check your internet
|
{t("media.errors.mediaFailed")}
|
||||||
connection and try again.
|
|
||||||
</p>
|
|
||||||
</ErrorMessage>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MediaPlaybackErrorView(props: { media?: MWMediaMeta }) {
|
|
||||||
const goBack = useGoBack();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-screen flex-1">
|
|
||||||
<div className="fixed inset-x-0 top-0 py-6 px-8">
|
|
||||||
<VideoPlayerHeader onClick={goBack} media={props.media} />
|
|
||||||
</div>
|
|
||||||
<ErrorMessage>
|
|
||||||
<p className="my-6 max-w-lg">
|
|
||||||
We encountered an error while playing the video you requested. If this
|
|
||||||
keeps happening please report the issue to the
|
|
||||||
<Link url={conf().DISCORD_LINK} newTab>
|
|
||||||
Discord server
|
|
||||||
</Link>{" "}
|
|
||||||
or on{" "}
|
|
||||||
<Link url={conf().GITHUB_LINK} newTab>
|
|
||||||
GitHub
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</p>
|
</p>
|
||||||
</ErrorMessage>
|
</ErrorMessage>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -22,19 +22,22 @@ import { useWatchedItem } from "@/state/watched";
|
||||||
import { MediaFetchErrorView } from "./MediaErrorView";
|
import { MediaFetchErrorView } from "./MediaErrorView";
|
||||||
import { MediaScrapeLog } from "./MediaScrapeLog";
|
import { MediaScrapeLog } from "./MediaScrapeLog";
|
||||||
import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView";
|
import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
function MediaViewLoading(props: { onGoBack(): void }) {
|
function MediaViewLoading(props: { onGoBack(): void }) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-screen items-center justify-center">
|
<div className="relative flex h-screen items-center justify-center">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>Loading...</title>
|
<title>{t("videoPlayer.loading")}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="absolute inset-x-0 top-0 p-6">
|
<div className="absolute inset-x-0 top-0 p-6">
|
||||||
<VideoPlayerHeader onClick={props.onGoBack} />
|
<VideoPlayerHeader onClick={props.onGoBack} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<Loading className="mb-4" />
|
<Loading className="mb-4" />
|
||||||
<p className="mb-8 text-denim-700">Finding the best video for you</p>
|
<p className="mb-8 text-denim-700">{t("videoPlaye.findingBestVideo")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -48,6 +51,7 @@ interface MediaViewScrapingProps {
|
||||||
}
|
}
|
||||||
function MediaViewScraping(props: MediaViewScrapingProps) {
|
function MediaViewScraping(props: MediaViewScrapingProps) {
|
||||||
const { eventLog, stream, pending } = useScrape(props.meta, props.selected);
|
const { eventLog, stream, pending } = useScrape(props.meta, props.selected);
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (stream) {
|
if (stream) {
|
||||||
|
@ -68,21 +72,20 @@ function MediaViewScraping(props: MediaViewScrapingProps) {
|
||||||
<>
|
<>
|
||||||
<Loading />
|
<Loading />
|
||||||
<p className="mb-8 text-denim-700">
|
<p className="mb-8 text-denim-700">
|
||||||
Finding the best video for you
|
{t("videoPlayer.findingBestVideo")}
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<IconPatch icon={Icons.EYE_SLASH} className="mb-8 text-bink-700" />
|
<IconPatch icon={Icons.EYE_SLASH} className="mb-8 text-bink-700" />
|
||||||
<p className="mb-8 text-denim-700">
|
<p className="mb-8 text-denim-700">
|
||||||
Whoops, could't find any videos for you
|
{t("videoPlayer.noVideos")}
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={`flex flex-col items-center transition-opacity duration-200 ${
|
className={`flex flex-col items-center transition-opacity duration-200 ${pending ? "opacity-100" : "opacity-0"
|
||||||
pending ? "opacity-100" : "opacity-0"
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<MediaScrapeLog events={eventLog} />
|
<MediaScrapeLog events={eventLog} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,12 +13,13 @@ export function NotFoundWrapper(props: {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
video?: boolean;
|
video?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
const goBack = useGoBack();
|
const goBack = useGoBack();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex-1">
|
<div className="h-screen flex-1">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>Not found</title>
|
<title>{t("notFound.genericTitle")}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
{props.video ? (
|
{props.video ? (
|
||||||
<div className="fixed inset-x-0 top-0 py-6 px-8">
|
<div className="fixed inset-x-0 top-0 py-6 px-8">
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Loading } from "@/components/layout/Loading";
|
import { Loading } from "@/components/layout/Loading";
|
||||||
|
import { MWQuery } from "@/backend/metadata/types";
|
||||||
|
import { useSearchQuery } from "@/hooks/useSearchQuery";
|
||||||
|
|
||||||
export function SearchLoadingView() {
|
export function SearchLoadingView() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [query] = useSearchQuery()
|
||||||
return (
|
return (
|
||||||
<Loading
|
<>
|
||||||
className="mt-40 mb-24 "
|
<Loading
|
||||||
text={t("search.loading") || "Fetching your favourite shows..."}
|
className="mt-40 mb-24 "
|
||||||
/>
|
text={t(`search.loading_${query.type}`) || t("search.loading") || "Fetching your favourite shows..."}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ export function SearchView() {
|
||||||
<>
|
<>
|
||||||
<div className="relative z-10 mb-24">
|
<div className="relative z-10 mb-24">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>movie-web</title>
|
<title>{t("global.name")}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Navigation bg={showBg} />
|
<Navigation bg={showBg} />
|
||||||
<ThinContainer>
|
<ThinContainer>
|
||||||
|
|
Loading…
Reference in a new issue