translations 🎉

Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
mrjvs 2023-02-18 20:01:19 +01:00
parent ad518a6508
commit 4f682d55a9
27 changed files with 2796 additions and 2874 deletions

View File

@ -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",

View File

@ -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)}`,
{ {

View File

@ -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)}
> >

View File

@ -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} />
)} )}

View File

@ -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&apos;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>

View File

@ -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}
// </>
// );
}

View File

@ -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,19 +28,18 @@ 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
@ -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,13 +62,11 @@ 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">
@ -82,8 +83,7 @@ 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
@ -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,
]} ]}
/> />

View File

@ -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

View File

@ -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;
}

View File

@ -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>."
}
} }
} }

View File

@ -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}

View File

@ -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);

View File

@ -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")}
/> />

View File

@ -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")}
/> />

View File

@ -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(
@ -23,7 +25,10 @@ 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,

View File

@ -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>
); );

View File

@ -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 ? (

View File

@ -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">

View File

@ -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>
@ -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>
@ -184,7 +188,10 @@ export function EpisodeSelectionPopout() {
)?.percentage )?.percentage
} }
> >
E{e.number} - {e.title} {t("videoPlayer.popouts.episode", {
index: e.number,
title: e.title
})}
</PopoutListEntry> </PopoutListEntry>
)) ))
: "No episodes"} : "No episodes"}

View File

@ -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);
@ -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>

View File

@ -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 = () => {

View File

@ -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>

View File

@ -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,20 +72,19 @@ 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&apos;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} />

View File

@ -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">

View File

@ -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 <Loading
className="mt-40 mb-24 " className="mt-40 mb-24 "
text={t("search.loading") || "Fetching your favourite shows..."} text={t(`search.loading_${query.type}`) || t("search.loading") || "Fetching your favourite shows..."}
/> />
</>
); );
} }

View File

@ -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>

5228
yarn.lock

File diff suppressed because it is too large Load Diff