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-react": "7.29.4",
"eslint-plugin-react-hooks": "4.3.0",
"i": "^0.3.7",
"npm": "^9.2.0",
"postcss": "^8.4.20",
"prettier": "^2.5.1",
"prettier-plugin-tailwindcss": "^0.1.7",

View file

@ -20,7 +20,7 @@ registerProvider({
type: [MWMediaType.MOVIE, MWMediaType.SERIES],
async scrape({ media, episode, progress }) {
// // search for relevant item
// search for relevant item
const searchResponse = await proxiedFetch<any>(
`/api/search?keyword=${encodeURIComponent(media.meta.title)}`,
{

View file

@ -67,12 +67,7 @@ export function SearchBarInput(props: SearchBarProps) {
id: MWMediaType.SERIES,
name: t("searchBar.series"),
icon: Icons.CLAPPER_BOARD,
},
// {
// id: MWMediaType.ANIME,
// name: "Anime",
// icon: Icons.DRAGON,
// },
}
]}
onClick={() => setDropdownOpen((old) => !old)}
>

View file

@ -1,6 +1,7 @@
import { Icon, Icons } from "@/components/Icon";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { ButtonControl } from "./ButtonControl";
export interface EditButtonProps {
@ -9,6 +10,7 @@ export interface EditButtonProps {
}
export function EditButton(props: EditButtonProps) {
const { t } = useTranslation()
const [parent] = useAutoAnimate<HTMLSpanElement>();
const onClick = useCallback(() => {
@ -22,7 +24,7 @@ export function EditButton(props: EditButtonProps) {
>
<span ref={parent}>
{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} />
)}

View file

@ -4,6 +4,7 @@ import { Icons } from "@/components/Icon";
import { Link } from "@/components/text/Link";
import { Title } from "@/components/text/Title";
import { conf } from "@/setup/config";
import { Trans, useTranslation } from "react-i18next";
interface ErrorShowcaseProps {
error: {
@ -35,29 +36,24 @@ interface ErrorMessageProps {
}
export function ErrorMessage(props: ErrorMessageProps) {
const { t } = useTranslation()
return (
<div
className={`${
props.localSize ? "h-full" : "min-h-screen"
} flex w-full flex-col items-center justify-center px-4 py-12`}
className={`${props.localSize ? "h-full" : "min-h-screen"
} flex w-full flex-col items-center justify-center px-4 py-12`}
>
<div className="flex flex-col items-center justify-start text-center">
<IconPatch icon={Icons.WARNING} className="mb-6 text-red-400" />
<Title>Whoops, it broke</Title>
<Title>{t("media.errors.genericTitle")}</Title>
{props.children ? (
<p className="my-6 max-w-lg">{props.children}</p>
) : (
<p className="my-6 max-w-lg">
The app encountered an error and wasn&apos;t able to recover, please
report it to the{" "}
<Link url={conf().DISCORD_LINK} newTab>
Discord server
</Link>{" "}
or on{" "}
<Link url={conf().GITHUB_LINK} newTab>
GitHub
</Link>
.
<Trans i18nKey="media.errors.videoFailed">
<Link url={conf().DISCORD_LINK} newTab />
<Link url={conf().GITHUB_LINK} newTab />
</Trans>
</p>
)}
</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 { useTranslation } from "react-i18next";
import { DotList } from "@/components/text/DotList";
import { MWMediaMeta } from "@/backend/metadata/types";
import { JWMediaToId } from "@/backend/metadata/justwatch";
@ -27,20 +28,19 @@ function MediaCardContent({
closable,
onClose,
}: MediaCardProps) {
const { t } = useTranslation();
const percentageString = `${Math.round(percentage ?? 0).toFixed(0)}%`;
const canLink = linkable && !closable;
return (
<div
className={`group -m-3 mb-2 rounded-xl bg-denim-300 bg-opacity-0 transition-colors duration-100 ${
canLink ? "hover:bg-opacity-100" : ""
}`}
className={`group -m-3 mb-2 rounded-xl bg-denim-300 bg-opacity-0 transition-colors duration-100 ${canLink ? "hover:bg-opacity-100" : ""
}`}
>
<article
className={`pointer-events-auto relative mb-2 p-3 transition-transform duration-100 ${
canLink ? "group-hover:scale-95" : ""
}`}
className={`pointer-events-auto relative mb-2 p-3 transition-transform duration-100 ${canLink ? "group-hover:scale-95" : ""
}`}
>
<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"
@ -51,7 +51,10 @@ function MediaCardContent({
{series ? (
<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">
S{series.season} E{series.episode}
{t("seasons.seasonAndEpisode", {
season: series.season,
episode: series.episode
})}
</p>
</div>
) : null}
@ -59,14 +62,12 @@ function MediaCardContent({
{percentage !== undefined ? (
<>
<div
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" : ""
}`}
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" : ""
}`}
/>
<div
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" : ""
}`}
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" : ""
}`}
/>
<div className="absolute inset-x-0 bottom-0 p-3">
<div className="relative h-1 overflow-hidden rounded-full bg-denim-600">
@ -82,9 +83,8 @@ function MediaCardContent({
) : null}
<div
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"
}`}
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"
}`}
>
<IconPatch
clickable
@ -100,7 +100,7 @@ function MediaCardContent({
<DotList
className="text-xs"
content={[
media.type.slice(0, 1).toUpperCase() + media.type.slice(1),
t(`media.${media.type}`),
media.year,
]}
/>

View file

@ -21,12 +21,9 @@ initializeChromecast();
// TODO video todos:
// - chrome cast support
// - bug: unmounting player throws errors in console
// - bug: safari fullscreen will make video overlap player controls
// - improvement: make scrapers use fuzzy matching on normalized titles
// - bug: source selection doesnt work with HLS
// - bug: .ass subtitle files are fucked
// - improvement: episode watch at the ending should not startAt
// TODO stuff to test:
// - browser: firefox, chrome, edge, safari desktop

View file

@ -44,3 +44,8 @@ body[data-no-select] {
-webkit-box-orient: vertical;
overflow: hidden;
}
google-cast-launcher {
@apply pointer-events-auto m-2 text-white flex items-center justify-center p-2;
@apply transition-[background-color,transform] duration-100 rounded-full bg-denim-600 bg-opacity-0 hover:bg-opacity-50 active:bg-denim-500 active:bg-opacity-100 active:scale-110;
}

View file

@ -3,26 +3,34 @@
"name": "movie-web"
},
"search": {
"loading": "Fetching your favourite shows...",
"loading_series": "Fetching your favourite series...",
"loading_movie": "Fetching your favourite movies...",
"loading": "Loading...",
"allResults": "That's all we have!",
"noResults": "We couldn't find anything!",
"allFailed": "Failed to find media, try again!",
"headingTitle": "Search results",
"headingLink": "Back to home",
"bookmarks": "Bookmarks",
"continueWatching": "Continue Watching",
"title": "What do you want to watch?",
"placeholder": "What do you want to watch?"
},
"media": {
"invalidUrl": "Your URL may be invalid",
"arrowText": "Go back"
"movie": "Movie",
"series": "Series",
"stopEditing": "Stop editing",
"errors": {
"genericTitle": "Whoops, it broke!",
"failedMeta": "Failed to load meta",
"mediaFailed": "We failed to request the media you asked for, check your internet connection and try again.",
"videoFailed": "We encountered an error while playing the video you requested. If this keeps happening please report the issue to the <0>Discord server</0> or on <1>GitHub</1>."
}
},
"seasons": {
"season": "Season {{season}}",
"failed": "Failed to get season data"
"seasonAndEpisode": "S{{season}} E{{episode}}"
},
"notFound": {
"genericTitle": "Not found",
"backArrow": "Back to home",
"media": {
"title": "Couldn't find that media",
@ -42,7 +50,31 @@
"series": "Series",
"Search": "Search"
},
"errorBoundary": {
"text": "The app encountered an error and wasn't able to recover, please report it to the"
"videoPlayer": {
"findingBestVideo": "Finding the best video for you",
"noVideos": "Whoops, couldn't find any videos for you",
"loading": "Loading...",
"backToHome": "Back to home",
"seasonAndEpisode": "S{{season}} E{{episode}}",
"buttons": {
"episodes": "Episodes",
"source": "Source",
"captions": "Captions"
},
"popouts": {
"sources": "Sources",
"seasons": "Seasons",
"captions": "Captions",
"episode": "E{{index}} - {{title}}",
"noCaptions": "No captions",
"linkedCaptions": "Linked captions",
"errors": {
"loadingWentWong": "Something went wrong loading the episodes for {{seasonTitle}}",
"embedsError": "Something went wrong loading the embeds for this thing that you like"
}
},
"errors": {
"fatalError": "The video player encounted a fatal error, please report it to the <0>Discord server</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 { PopoutAnchor } from "@/video/components/popouts/PopoutAnchor";
import { useIsMobile } from "@/hooks/useIsMobile";
import { useTranslation } from "react-i18next";
interface Props {
className?: string;
}
export function CaptionsSelectionAction(props: Props) {
const { t } = useTranslation()
const descriptor = useVideoPlayerDescriptor();
const controls = useControls(descriptor);
const { isMobile } = useIsMobile();
@ -20,7 +22,7 @@ export function CaptionsSelectionAction(props: Props) {
<PopoutAnchor for="captions">
<VideoPlayerIconButton
className={props.className}
text={isMobile ? "Captions" : ""}
text={isMobile ? t("videoPlayer.buttons.captions") as string : ""}
wide={isMobile}
onClick={() => controls.openPopout("captions")}
icon={Icons.CAPTIONS}

View file

@ -3,7 +3,6 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks";
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
import { useMisc } from "@/video/state/logic/misc";
// TODO pausing before first frame will infinitely show spinner until unpaused
export function LoadingAction() {
const descriptor = useVideoPlayerDescriptor();
const mediaPlaying = useMediaPlaying(descriptor);

View file

@ -6,12 +6,14 @@ import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconB
import { useControls } from "@/video/state/logic/controls";
import { PopoutAnchor } from "@/video/components/popouts/PopoutAnchor";
import { useInterface } from "@/video/state/logic/interface";
import { useTranslation } from "react-i18next";
interface Props {
className?: string;
}
export function SeriesSelectionAction(props: Props) {
const { t } = useTranslation()
const descriptor = useVideoPlayerDescriptor();
const meta = useMeta(descriptor);
const videoInterface = useInterface(descriptor);
@ -26,7 +28,7 @@ export function SeriesSelectionAction(props: Props) {
<VideoPlayerIconButton
active={videoInterface.popout === "episodes"}
icon={Icons.EPISODES}
text="Episodes"
text={t("videoPlayer.buttons.episodes") as string}
wide
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 { PopoutAnchor } from "@/video/components/popouts/PopoutAnchor";
import { useInterface } from "@/video/state/logic/interface";
import { useTranslation } from "react-i18next";
interface Props {
className?: string;
}
export function SourceSelectionAction(props: Props) {
const { t } = useTranslation()
const descriptor = useVideoPlayerDescriptor();
const videoInterface = useInterface(descriptor);
const controls = useControls(descriptor);
@ -20,8 +22,9 @@ export function SourceSelectionAction(props: Props) {
<PopoutAnchor for="source">
<VideoPlayerIconButton
active={videoInterface.popout === "source"}
icon={Icons.FILE}
text="Source"
icon={Icons.CLAPPER_BOARD}
iconSize="text-xl"
text={t("videoPlayer.buttons.source") as string}
wide
onClick={() => controls.openPopout("source")}
/>

View file

@ -1,9 +1,11 @@
import { MWMediaType } from "@/backend/metadata/types";
import { useMeta } from "@/video/state/logic/meta";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
export function useCurrentSeriesEpisodeInfo(descriptor: string) {
const meta = useMeta(descriptor);
const {t} = useTranslation()
const currentSeasonInfo = useMemo(() => {
return meta?.seasons?.find(
@ -22,8 +24,11 @@ export function useCurrentSeriesEpisodeInfo(descriptor: string) {
);
if (!isSeries) return { isSeries: false };
const humanizedEpisodeId = `S${currentSeasonInfo?.number} E${currentEpisodeInfo?.number}`;
const humanizedEpisodeId = t("videoPlayer.seasonAndEpisode", {
season: currentSeasonInfo?.number,
episode: currentEpisodeInfo?.number
});
return {
isSeries: true,

View file

@ -3,6 +3,7 @@ import { ErrorMessage } from "@/components/layout/ErrorBoundary";
import { Link } from "@/components/text/Link";
import { conf } from "@/setup/config";
import { Component, ReactNode } from "react";
import { Trans } from "react-i18next";
import { VideoPlayerHeader } from "./VideoPlayerHeader";
interface ErrorBoundaryState {
@ -67,15 +68,10 @@ export class VideoErrorBoundary extends Component<
/>
</div>
<ErrorMessage error={this.state.error} localSize>
The video player encounted a fatal error, please report it to the{" "}
<Link url={conf().DISCORD_LINK} newTab>
Discord server
</Link>{" "}
or on{" "}
<Link url={conf().GITHUB_LINK} newTab>
GitHub
</Link>
.
<Trans i18nKey="videoPlayer.errors.fatalError">
<Link url={conf().DISCORD_LINK} newTab />
<Link url={conf().GITHUB_LINK} newTab />
</Trans>
</ErrorMessage>
</div>
);

View file

@ -8,6 +8,7 @@ import {
} from "@/state/bookmark";
import { AirplayAction } from "@/video/components/actions/AirplayAction";
import { ChromecastAction } from "@/video/components/actions/ChromecastAction";
import { useTranslation } from "react-i18next";
interface VideoPlayerHeaderProps {
media?: MWMediaMeta;
@ -21,6 +22,8 @@ export function VideoPlayerHeader(props: VideoPlayerHeaderProps) {
? getIfBookmarkedFromPortable(bookmarkStore.bookmarks, props.media)
: false;
const showDivider = props.media && props.onClick;
const { t } = useTranslation();
return (
<div className="flex 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"
>
<Icon className="mr-2" icon={Icons.ARROW_LEFT} />
<span>Back to home</span>
<span>{t("videoPlayer.backToHome")}</span>
</span>
) : null}
{showDivider ? (

View file

@ -7,6 +7,7 @@ import { useControls } from "@/video/state/logic/controls";
import { useMeta } from "@/video/state/logic/meta";
import { useSource } from "@/video/state/logic/source";
import { useMemo, useRef } from "react";
import { useTranslation } from "react-i18next";
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
function makeCaptionId(caption: MWCaption, isLinked: boolean): string {
@ -14,6 +15,8 @@ function makeCaptionId(caption: MWCaption, isLinked: boolean): string {
}
export function CaptionSelectionPopout() {
const { t } = useTranslation()
const descriptor = useVideoPlayerDescriptor();
const meta = useMeta(descriptor);
const source = useSource(descriptor);
@ -38,7 +41,7 @@ export function CaptionSelectionPopout() {
return (
<>
<PopoutSection className="bg-ash-100 font-bold text-white">
<div>Captions</div>
<div>{t("videoPlayer.popouts.captions")}</div>
</PopoutSection>
<div className="relative overflow-y-auto">
<PopoutSection>
@ -49,13 +52,13 @@ export function CaptionSelectionPopout() {
controls.closePopout();
}}
>
No captions
{t("videoPlayer.popouts.noCaptions")}
</PopoutListEntry>
</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">
<Icon className="text-base" icon={Icons.LINK} />
<span>Linked captions</span>
<span>{t("videoPlayer.popouts.linkedCaptions")}</span>
</p>
<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 { useWatchedContext } from "@/state/watched";
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
import { useTranslation } from "react-i18next";
export function EpisodeSelectionPopout() {
const params = useParams<{
media: string;
}>();
const { t } = useTranslation()
const descriptor = useVideoPlayerDescriptor();
const meta = useMeta(descriptor);
const controls = useControls(descriptor);
@ -119,7 +122,7 @@ export function EpisodeSelectionPopout() {
isPickingSeason ? "opacity-1" : "opacity-0",
].join(" ")}
>
Seasons
{t("videoPlayer.popouts.seasons")}
</span>
</div>
</PopoutSection>
@ -134,15 +137,15 @@ export function EpisodeSelectionPopout() {
>
{currentSeasonInfo
? meta?.seasons?.map?.((season) => (
<PopoutListEntry
key={season.id}
active={meta?.episode?.seasonId === season.id}
onClick={() => setSeason(season.id)}
isOnDarkBackground
>
{season.title}
</PopoutListEntry>
))
<PopoutListEntry
key={season.id}
active={meta?.episode?.seasonId === season.id}
onClick={() => setSeason(season.id)}
isOnDarkBackground
>
{season.title}
</PopoutListEntry>
))
: "No season"}
</PopoutSection>
<PopoutSection className="relative h-full overflow-y-auto">
@ -158,8 +161,9 @@ export function EpisodeSelectionPopout() {
className="text-xl text-bink-600"
/>
<p className="mt-6 w-full text-center">
Something went wrong loading the episodes for{" "}
{currentSeasonInfo?.title?.toLowerCase()}
{t("videoPLayer.popouts.errors.loadingWentWrong", {
seasonTitle: currentSeasonInfo?.title?.toLowerCase()
})}
</p>
</div>
</div>
@ -167,26 +171,29 @@ export function EpisodeSelectionPopout() {
<div>
{currentSeasonEpisodes && currentSeasonInfo
? currentSeasonEpisodes.map((e) => (
<PopoutListEntry
key={e.id}
active={e.id === meta?.episode?.episodeId}
onClick={() => {
if (e.id === meta?.episode?.episodeId)
controls.closePopout();
else setCurrent(currentSeasonInfo.id, e.id);
}}
percentageCompleted={
watched.items.find(
(item) =>
item.item?.series?.seasonId ===
currentSeasonInfo.id &&
item.item?.series?.episodeId === e.id
)?.percentage
}
>
E{e.number} - {e.title}
</PopoutListEntry>
))
<PopoutListEntry
key={e.id}
active={e.id === meta?.episode?.episodeId}
onClick={() => {
if (e.id === meta?.episode?.episodeId)
controls.closePopout();
else setCurrent(currentSeasonInfo.id, e.id);
}}
percentageCompleted={
watched.items.find(
(item) =>
item.item?.series?.seasonId ===
currentSeasonInfo.id &&
item.item?.series?.episodeId === e.id
)?.percentage
}
>
{t("videoPlayer.popouts.episode", {
index: e.number,
title: e.title
})}
</PopoutListEntry>
))
: "No episodes"}
</div>
)}

View file

@ -11,9 +11,11 @@ import { getProviders } from "@/backend/helpers/register";
import { runProvider } from "@/backend/helpers/run";
import { MWProviderScrapeResult } from "@/backend/helpers/provider";
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
import { useTranslation } from "react-i18next";
// TODO HLS does not work
export function SourceSelectionPopout() {
const { t } = useTranslation()
const descriptor = useVideoPlayerDescriptor();
const controls = useControls(descriptor);
const meta = useMeta(descriptor);
@ -42,7 +44,7 @@ export function SourceSelectionPopout() {
tmdbId: "",
meta: meta.meta,
},
progress: () => {},
progress: () => { },
type: meta.meta.type,
episode: meta.episode?.episodeId as any,
season: meta.episode?.seasonId as any,
@ -129,7 +131,7 @@ export function SourceSelectionPopout() {
!showingProvider ? "opacity-1" : "opacity-0",
].join(" ")}
>
Sources
{t("videoPlayer.popouts.sources")}
</span>
</div>
</PopoutSection>
@ -154,8 +156,7 @@ export function SourceSelectionPopout() {
className="text-xl text-bink-600"
/>
<p className="mt-6 w-full text-center">
Something went wrong loading the embeds for this thing that
you like
{t("videoPlayer.popouts.errors.embedsError")}
</p>
</div>
</div>

View file

@ -249,6 +249,7 @@ export function createVideoStateProvider(
};
const canplay = () => {
state.mediaPlaying.isFirstLoading = false;
state.mediaPlaying.isLoading = false;
updateMediaPlaying(descriptor, state);
};
const fullscreenchange = () => {

View file

@ -5,48 +5,23 @@ import { useGoBack } from "@/hooks/useGoBack";
import { conf } from "@/setup/config";
import { VideoPlayerHeader } from "@/video/components/parts/VideoPlayerHeader";
import { Helmet } from "react-helmet";
import { Trans, useTranslation } from "react-i18next";
export function MediaFetchErrorView() {
const { t } = useTranslation()
const goBack = useGoBack();
return (
<div className="h-screen flex-1">
<Helmet>
<title>Failed to load meta</title>
<title>{t("media.errors.failedMeta")}</title>
</Helmet>
<div className="fixed inset-x-0 top-0 py-6 px-8">
<VideoPlayerHeader onClick={goBack} />
</div>
<ErrorMessage>
<p className="my-6 max-w-lg">
We failed to request the media you asked for, check your internet
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>
.
{t("media.errors.mediaFailed")}
</p>
</ErrorMessage>
</div>

View file

@ -22,19 +22,22 @@ import { useWatchedItem } from "@/state/watched";
import { MediaFetchErrorView } from "./MediaErrorView";
import { MediaScrapeLog } from "./MediaScrapeLog";
import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView";
import { useTranslation } from "react-i18next";
function MediaViewLoading(props: { onGoBack(): void }) {
const { t } = useTranslation()
return (
<div className="relative flex h-screen items-center justify-center">
<Helmet>
<title>Loading...</title>
<title>{t("videoPlayer.loading")}</title>
</Helmet>
<div className="absolute inset-x-0 top-0 p-6">
<VideoPlayerHeader onClick={props.onGoBack} />
</div>
<div className="flex flex-col items-center">
<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>
);
@ -48,6 +51,7 @@ interface MediaViewScrapingProps {
}
function MediaViewScraping(props: MediaViewScrapingProps) {
const { eventLog, stream, pending } = useScrape(props.meta, props.selected);
const { t } = useTranslation()
useEffect(() => {
if (stream) {
@ -68,21 +72,20 @@ function MediaViewScraping(props: MediaViewScrapingProps) {
<>
<Loading />
<p className="mb-8 text-denim-700">
Finding the best video for you
{t("videoPlayer.findingBestVideo")}
</p>
</>
) : (
<>
<IconPatch icon={Icons.EYE_SLASH} className="mb-8 text-bink-700" />
<p className="mb-8 text-denim-700">
Whoops, could&apos;t find any videos for you
{t("videoPlayer.noVideos")}
</p>
</>
)}
<div
className={`flex flex-col items-center transition-opacity duration-200 ${
pending ? "opacity-100" : "opacity-0"
}`}
className={`flex flex-col items-center transition-opacity duration-200 ${pending ? "opacity-100" : "opacity-0"
}`}
>
<MediaScrapeLog events={eventLog} />
</div>

View file

@ -13,12 +13,13 @@ export function NotFoundWrapper(props: {
children?: ReactNode;
video?: boolean;
}) {
const { t } = useTranslation()
const goBack = useGoBack();
return (
<div className="h-screen flex-1">
<Helmet>
<title>Not found</title>
<title>{t("notFound.genericTitle")}</title>
</Helmet>
{props.video ? (
<div className="fixed inset-x-0 top-0 py-6 px-8">

View file

@ -1,12 +1,17 @@
import { useTranslation } from "react-i18next";
import { Loading } from "@/components/layout/Loading";
import { MWQuery } from "@/backend/metadata/types";
import { useSearchQuery } from "@/hooks/useSearchQuery";
export function SearchLoadingView() {
const { t } = useTranslation();
const [query] = useSearchQuery()
return (
<Loading
className="mt-40 mb-24 "
text={t("search.loading") || "Fetching your favourite shows..."}
/>
<>
<Loading
className="mt-40 mb-24 "
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">
<Helmet>
<title>movie-web</title>
<title>{t("global.name")}</title>
</Helmet>
<Navigation bg={showBg} />
<ThinContainer>

5228
yarn.lock

File diff suppressed because it is too large Load diff