mirror of
https://github.com/movie-web/movie-web.git
synced 2024-11-11 04:55:07 +01:00
Localize the rest of everything
This commit is contained in:
parent
7537ebb56c
commit
a4808415db
@ -72,13 +72,25 @@
|
||||
"title": "Goo goo gaa gaa",
|
||||
"text": "Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can you find it in your heart to forgive? UwU 💖",
|
||||
"homeButton": "Go home"
|
||||
},
|
||||
"items": {
|
||||
"pending": "Checking for videos...",
|
||||
"notFound": "Doesn't have the video",
|
||||
"failure": "Error occured"
|
||||
}
|
||||
},
|
||||
"playbackError": {
|
||||
"badge": "Not found",
|
||||
"title": "Goo goo gaa gaa",
|
||||
"text": "Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can you find it in your heart to forgive? UwU 💖",
|
||||
"homeButton": "Go home"
|
||||
"homeButton": "Go home",
|
||||
"errors": {
|
||||
"errorAborted": "The fetching of the associated resource was aborted by the user's request.",
|
||||
"errorNetwork": "Some kind of network error occurred which prevented the media from being successfully fetched, despite having previously been available.",
|
||||
"errorDecode": "Despite having previously been determined to be usable, an error occurred while trying to decode the media resource, resulting in an error.",
|
||||
"errorNotSupported": "The associated resource or media provider object has been found to be unsuitable.",
|
||||
"errorGenericMedia": "Unknown media error occured"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"notFound": {
|
||||
@ -93,6 +105,97 @@
|
||||
"text": "Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can you find it in your heart to forgive? UwU 💖",
|
||||
"homeButton": "Go home"
|
||||
}
|
||||
},
|
||||
"back": {
|
||||
"default": "Back to home",
|
||||
"short": "Back"
|
||||
},
|
||||
"time": {
|
||||
"short": "-{{timeLeft}}",
|
||||
"regular": "{{timeWatched}} / {{duration}}",
|
||||
"remaining": "{{timeLeft}} left • Finish at {{timeFinished, datetime}}"
|
||||
},
|
||||
"nextEpisode": {
|
||||
"next": "Next episode",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"menus": {
|
||||
"settings": {
|
||||
"videoSection": "Video settings",
|
||||
"experienceSection": "Viewing Experience",
|
||||
"enableCaptions": "Enable Captions",
|
||||
"captionItem": "Caption settings",
|
||||
"sourceItem": "Video sources",
|
||||
"playbackItem": "Playback settings",
|
||||
"downloadItem": "Download",
|
||||
"qualityItem": "Quality"
|
||||
},
|
||||
"episodes": {
|
||||
"button": "Episodes",
|
||||
"loadingTitle": "Loading...",
|
||||
"loadingList": "Loading...",
|
||||
"loadingError": "Error loading season",
|
||||
"emptyState": "There are no episodes in this season, check back later!",
|
||||
"episodeBadge": "E{{episode}}"
|
||||
},
|
||||
"sources": {
|
||||
"title": "Sources",
|
||||
"unknownOption": "Unknown",
|
||||
"noStream": {
|
||||
"title": "No stream",
|
||||
"text": "This source has no streams for this movie or show."
|
||||
},
|
||||
"noEmbeds": {
|
||||
"title": "No embeds found",
|
||||
"text": "We were unable to find any embeds for this source, please try another."
|
||||
},
|
||||
"failed": {
|
||||
"title": "Failed to scrape",
|
||||
"text": "We were unable to find any videos for this source. Don't come bitchin' to us about it, just try another source."
|
||||
}
|
||||
},
|
||||
"captions": {
|
||||
"title": "Captions",
|
||||
"customizeLabel": "Customize",
|
||||
"settings": {
|
||||
"fixCapitals": "Fix capitalization",
|
||||
"delay": "Caption delay"
|
||||
},
|
||||
"customChoice": "Upload captions",
|
||||
"offChoice": "Off",
|
||||
"unknownLanguage": "Unknown"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Download",
|
||||
"disclaimer": "Downloads are taken directly from the provider. movie-web does not have control over how the downloads are provided.",
|
||||
"hlsExplanation": "Insert explanation for why you can't download HLS here",
|
||||
"downloadVideo": "Download video",
|
||||
"downloadCaption": "Download current caption",
|
||||
"onPc": {
|
||||
"title": "Downloading on PC",
|
||||
"shortTitle": "Download / PC",
|
||||
"1": "On PC, right click the video and select <bold>Save video as</bold>"
|
||||
},
|
||||
"onAndroid": {
|
||||
"title": "Downloading on Android",
|
||||
"shortTitle": "Download / Android",
|
||||
"1": "To download on Android, <bold>tap and hold</bold> on the video, then select <bold>save</bold>."
|
||||
},
|
||||
"onIos": {
|
||||
"title": "Downloading on iOS",
|
||||
"shortTitle": "Download / iOS",
|
||||
"1": "To download on iOS, click <bold><ios_share /></bold>, then <bold>Save to Files <ios_files /></bold>. All that's left to do now is to pick a nice and cozy folder for your video!"
|
||||
}
|
||||
},
|
||||
"playback": {
|
||||
"title": "Playback settings",
|
||||
"speedLabel": "Playback speed"
|
||||
},
|
||||
"quality": {
|
||||
"title": "Quality",
|
||||
"automaticLabel": "Automatic quality",
|
||||
"hint": "You can try <0>switching source</0> to get different quality options."
|
||||
}
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
|
@ -11,7 +11,7 @@ export function EpisodeTitle() {
|
||||
return (
|
||||
<div>
|
||||
<span className="text-white font-medium mr-3">
|
||||
{t("seasons.seasonAndEpisode", {
|
||||
{t("media.episodeDisplay", {
|
||||
season: meta?.season?.number,
|
||||
episode: meta?.episode?.number,
|
||||
})}
|
||||
|
@ -50,6 +50,7 @@ function SeasonsView({
|
||||
selectedSeason: string;
|
||||
setSeason: (id: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const meta = usePlayerStore((s) => s.meta);
|
||||
const [loadingState, seasons] = useSeasonData(
|
||||
meta?.tmdbId ?? "",
|
||||
@ -73,13 +74,19 @@ function SeasonsView({
|
||||
</Menu.Section>
|
||||
);
|
||||
} else if (loadingState.error)
|
||||
content = <CenteredText>Error loading season</CenteredText>;
|
||||
content = (
|
||||
<CenteredText>{t("player.menus.episodes.loadingError")}</CenteredText>
|
||||
);
|
||||
else if (loadingState.loading)
|
||||
content = <CenteredText>Loading...</CenteredText>;
|
||||
content = (
|
||||
<CenteredText>{t("player.menus.episodes.loadingList")}</CenteredText>
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu.CardWithScrollable>
|
||||
<Menu.Title>{meta?.title}</Menu.Title>
|
||||
<Menu.Title>
|
||||
{meta?.title ?? t("player.menus.episodes.loadingTitle")}
|
||||
</Menu.Title>
|
||||
{content}
|
||||
</Menu.CardWithScrollable>
|
||||
);
|
||||
@ -120,15 +127,19 @@ function EpisodesView({
|
||||
|
||||
let content: ReactNode = null;
|
||||
if (loadingState.error)
|
||||
content = <CenteredText>Error loading season</CenteredText>;
|
||||
content = (
|
||||
<CenteredText>{t("player.menus.episodes.loadingError")}</CenteredText>
|
||||
);
|
||||
else if (loadingState.loading)
|
||||
content = <CenteredText>Loading...</CenteredText>;
|
||||
content = (
|
||||
<CenteredText>{t("player.menus.episodes.loadingList")}</CenteredText>
|
||||
);
|
||||
else if (loadingState.value) {
|
||||
content = (
|
||||
<Menu.ScrollToActiveSection className="pb-6">
|
||||
{loadingState.value.season.episodes.length === 0 ? (
|
||||
<Menu.TextDisplay title="No episodes found">
|
||||
There are no episodes in this season, check back later!
|
||||
{t("player.menus.episodes.emptyState")}
|
||||
</Menu.TextDisplay>
|
||||
) : null}
|
||||
{loadingState.value.season.episodes.map((ep) => {
|
||||
@ -167,7 +178,9 @@ function EpisodesView({
|
||||
: "bg-opacity-50"
|
||||
)}
|
||||
>
|
||||
E{ep.number}
|
||||
{t("player.menus.episodes.episodeBadge", {
|
||||
episode: ep.number,
|
||||
})}
|
||||
</span>
|
||||
<span className="line-clamp-1 break-all">{ep.title}</span>
|
||||
</div>
|
||||
@ -182,7 +195,8 @@ function EpisodesView({
|
||||
return (
|
||||
<Menu.CardWithScrollable>
|
||||
<Menu.BackLink onClick={goBack}>
|
||||
{loadingState?.value?.season.title || t("videoPlayer.loading")}
|
||||
{loadingState?.value?.season.title ||
|
||||
t("player.menus.episodes.loadingTitle")}
|
||||
</Menu.BackLink>
|
||||
{content}
|
||||
</Menu.CardWithScrollable>
|
||||
@ -261,7 +275,7 @@ export function Episodes() {
|
||||
onClick={() => router.open("/episodes")}
|
||||
icon={Icons.EPISODES}
|
||||
>
|
||||
{t("videoPlayer.buttons.episodes")}
|
||||
{t("player.menus.episodes.button")}
|
||||
</VideoPlayerButton>
|
||||
</OverlayAnchor>
|
||||
);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import classNames from "classnames";
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta";
|
||||
@ -41,6 +42,7 @@ export function NextEpisodeButton(props: {
|
||||
controlsShowing: boolean;
|
||||
onChange?: (meta: PlayerMeta) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const duration = usePlayerStore((s) => s.progress.duration);
|
||||
const isHidden = usePlayerStore((s) => s.interface.hideNextEpisodeBtn);
|
||||
const meta = usePlayerStore((s) => s.meta);
|
||||
@ -96,14 +98,14 @@ export function NextEpisodeButton(props: {
|
||||
className="py-px box-content bg-buttons-secondary hover:bg-buttons-secondaryHover bg-opacity-90 text-buttons-secondaryText"
|
||||
onClick={hideNextEpisodeButton}
|
||||
>
|
||||
Cancel
|
||||
{t("player.nextEpisode.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => loadNextEpisode()}
|
||||
className="bg-buttons-primary hover:bg-buttons-primaryHover text-buttons-primaryText flex justify-center items-center"
|
||||
>
|
||||
<Icon className="text-xl mr-1" icon={Icons.SKIP_EPISODE} />
|
||||
Next episode
|
||||
{t("player.nextEpisode.next")}
|
||||
</Button>
|
||||
</div>
|
||||
</Transition>
|
||||
|
@ -14,7 +14,7 @@ export function SkipForward(props: { iconSizeClass?: string }) {
|
||||
|
||||
return (
|
||||
<VideoPlayerButton
|
||||
iconSizeClass={props.iconSizeClass || ""}
|
||||
iconSizeClass={props.iconSizeClass}
|
||||
onClick={commit}
|
||||
icon={Icons.SKIP_FORWARD}
|
||||
/>
|
||||
@ -31,7 +31,7 @@ export function SkipBackward(props: { iconSizeClass?: string }) {
|
||||
|
||||
return (
|
||||
<VideoPlayerButton
|
||||
iconSizeClass={props.iconSizeClass || ""}
|
||||
iconSizeClass={props.iconSizeClass}
|
||||
onClick={commit}
|
||||
icon={Icons.SKIP_BACKWARD}
|
||||
/>
|
||||
|
@ -9,10 +9,14 @@ export function Time(props: { short?: boolean }) {
|
||||
const timeFormat = usePlayerStore((s) => s.interface.timeFormat);
|
||||
const setTimeFormat = usePlayerStore((s) => s.setTimeFormat);
|
||||
|
||||
const { duration, time, draggingTime } = usePlayerStore((s) => s.progress);
|
||||
const {
|
||||
duration: timeDuration,
|
||||
time,
|
||||
draggingTime,
|
||||
} = usePlayerStore((s) => s.progress);
|
||||
const { isSeeking } = usePlayerStore((s) => s.interface);
|
||||
const { t } = useTranslation();
|
||||
const hasHours = durationExceedsHour(duration);
|
||||
const hasHours = durationExceedsHour(timeDuration);
|
||||
|
||||
function toggleMode() {
|
||||
setTimeFormat(
|
||||
@ -24,47 +28,36 @@ export function Time(props: { short?: boolean }) {
|
||||
|
||||
const currentTime = Math.min(
|
||||
Math.max(isSeeking ? draggingTime : time, 0),
|
||||
duration
|
||||
timeDuration
|
||||
);
|
||||
const secondsRemaining = Math.abs(currentTime - duration);
|
||||
const secondsRemaining = Math.abs(currentTime - timeDuration);
|
||||
|
||||
const timeLeft = formatSeconds(
|
||||
secondsRemaining,
|
||||
durationExceedsHour(secondsRemaining)
|
||||
);
|
||||
const timeWatched = formatSeconds(currentTime, hasHours);
|
||||
const timeFinished = new Date(Date.now() + secondsRemaining * 1e3);
|
||||
const duration = formatSeconds(timeDuration, hasHours);
|
||||
|
||||
const formattedTimeFinished = t("videoPlayer.finishAt", {
|
||||
timeFinished,
|
||||
formatParams: {
|
||||
timeFinished: { hour: "numeric", minute: "numeric" },
|
||||
},
|
||||
});
|
||||
|
||||
let timeString;
|
||||
let timeFinishedString;
|
||||
if (props.short) {
|
||||
timeString = formatSeconds(currentTime, hasHours);
|
||||
timeFinishedString = `-${formatSeconds(
|
||||
secondsRemaining,
|
||||
durationExceedsHour(secondsRemaining)
|
||||
)}`;
|
||||
} else {
|
||||
timeString = `${formatSeconds(currentTime, hasHours)} / ${formatSeconds(
|
||||
duration,
|
||||
hasHours
|
||||
)}`;
|
||||
timeFinishedString = `${t("videoPlayer.timeLeft", {
|
||||
timeLeft: formatSeconds(
|
||||
secondsRemaining,
|
||||
durationExceedsHour(secondsRemaining)
|
||||
),
|
||||
})} • ${formattedTimeFinished}`;
|
||||
}
|
||||
|
||||
const child =
|
||||
timeFormat === VideoPlayerTimeFormat.REGULAR ? (
|
||||
<span>{timeString}</span>
|
||||
) : (
|
||||
<span>{timeFinishedString}</span>
|
||||
);
|
||||
let localizationKey = "regular";
|
||||
if (props.short) localizationKey = "short";
|
||||
else if (timeFormat === VideoPlayerTimeFormat.REMAINING)
|
||||
localizationKey = "remaining";
|
||||
|
||||
return (
|
||||
<VideoPlayerButton onClick={() => toggleMode()}>{child}</VideoPlayerButton>
|
||||
<VideoPlayerButton onClick={() => toggleMode()}>
|
||||
<span>
|
||||
{t(`player.time.${localizationKey}`, {
|
||||
timeFinished,
|
||||
timeWatched,
|
||||
timeLeft,
|
||||
duration,
|
||||
formatParams: {
|
||||
timeFinished: { hour: "numeric", minute: "numeric" },
|
||||
},
|
||||
})}
|
||||
</span>
|
||||
</VideoPlayerButton>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import classNames from "classnames";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Toggle } from "@/components/buttons/Toggle";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
@ -213,6 +214,7 @@ export function CaptionSetting(props: {
|
||||
export const colors = ["#ffffff", "#80b1fa", "#e2e535"];
|
||||
|
||||
export function CaptionSettingsView({ id }: { id: string }) {
|
||||
const { t } = useTranslation();
|
||||
const router = useOverlayRouter(id);
|
||||
const styling = useSubtitleStore((s) => s.styling);
|
||||
const overrideCasing = useSubtitleStore((s) => s.overrideCasing);
|
||||
@ -228,7 +230,7 @@ export function CaptionSettingsView({ id }: { id: string }) {
|
||||
</Menu.BackLink>
|
||||
<Menu.Section className="space-y-6">
|
||||
<CaptionSetting
|
||||
label="Caption delay"
|
||||
label={t("player.menus.captions.settings.fixCapitals")}
|
||||
max={10}
|
||||
min={-10}
|
||||
onChange={(v) => setDelay(v)}
|
||||
@ -238,7 +240,9 @@ export function CaptionSettingsView({ id }: { id: string }) {
|
||||
controlButtons
|
||||
/>
|
||||
<div className="flex justify-between items-center">
|
||||
<Menu.FieldTitle>Fix capitalization</Menu.FieldTitle>
|
||||
<Menu.FieldTitle>
|
||||
{t("player.menus.captions.settings.delay")}
|
||||
</Menu.FieldTitle>
|
||||
<div className="flex justify-center items-center">
|
||||
<Toggle
|
||||
enabled={overrideCasing}
|
||||
@ -248,7 +252,7 @@ export function CaptionSettingsView({ id }: { id: string }) {
|
||||
</div>
|
||||
<Menu.Divider />
|
||||
<CaptionSetting
|
||||
label="Background opacity"
|
||||
label={t("settings.captions.backgroundLabel")}
|
||||
max={100}
|
||||
min={0}
|
||||
onChange={(v) => updateStyling({ backgroundOpacity: v / 100 })}
|
||||
@ -256,7 +260,7 @@ export function CaptionSettingsView({ id }: { id: string }) {
|
||||
textTransformer={(s) => `${s}%`}
|
||||
/>
|
||||
<CaptionSetting
|
||||
label="Text size"
|
||||
label={t("settings.captions.textSizeLabel")}
|
||||
max={200}
|
||||
min={1}
|
||||
textTransformer={(s) => `${s}%`}
|
||||
@ -264,7 +268,7 @@ export function CaptionSettingsView({ id }: { id: string }) {
|
||||
value={styling.size * 100}
|
||||
/>
|
||||
<div className="flex justify-between items-center">
|
||||
<Menu.FieldTitle>Color</Menu.FieldTitle>
|
||||
<Menu.FieldTitle>{t("settings.captions.colorLabel")}</Menu.FieldTitle>
|
||||
<div className="flex justify-center items-center">
|
||||
{colors.map((v) => (
|
||||
<ColorOption
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Fuse from "fuse.js";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAsyncFn } from "react-use";
|
||||
import { convert } from "subsrt-ts";
|
||||
|
||||
@ -45,6 +46,7 @@ export function CaptionOption(props: {
|
||||
}
|
||||
|
||||
function CustomCaptionOption() {
|
||||
const { t } = useTranslation();
|
||||
const lang = usePlayerStore((s) => s.caption.selected?.language);
|
||||
const setCaption = usePlayerStore((s) => s.setCaption);
|
||||
const setCustomSubs = useSubtitleStore((s) => s.setCustomSubs);
|
||||
@ -55,7 +57,7 @@ function CustomCaptionOption() {
|
||||
selected={lang === "custom"}
|
||||
onClick={() => fileInput.current?.click()}
|
||||
>
|
||||
Upload captions
|
||||
{t("player.menus.captions.customChoice")}
|
||||
<input
|
||||
className="hidden"
|
||||
ref={fileInput}
|
||||
@ -82,10 +84,12 @@ function CustomCaptionOption() {
|
||||
}
|
||||
|
||||
function useSubtitleList(subs: CaptionListItem[], searchQuery: string) {
|
||||
const { t: translate } = useTranslation();
|
||||
const unknownChoice = translate("player.menus.captions.unknownLanguage");
|
||||
return useMemo(() => {
|
||||
const input = subs.map((t) => ({
|
||||
...t,
|
||||
languageName: getLanguageFromIETF(t.language) ?? "Unknown",
|
||||
languageName: getLanguageFromIETF(t.language) ?? unknownChoice,
|
||||
}));
|
||||
const sorted = sortLangCodes(input.map((t) => t.language));
|
||||
let results = input.sort((a, b) => {
|
||||
@ -102,10 +106,11 @@ function useSubtitleList(subs: CaptionListItem[], searchQuery: string) {
|
||||
}
|
||||
|
||||
return results;
|
||||
}, [subs, searchQuery]);
|
||||
}, [subs, searchQuery, unknownChoice]);
|
||||
}
|
||||
|
||||
export function CaptionsView({ id }: { id: string }) {
|
||||
const { t } = useTranslation();
|
||||
const router = useOverlayRouter(id);
|
||||
const lang = usePlayerStore((s) => s.caption.selected?.language);
|
||||
const [currentlyDownloading, setCurrentlyDownloading] = useState<
|
||||
@ -155,11 +160,11 @@ export function CaptionsView({ id }: { id: string }) {
|
||||
onClick={() => router.navigate("/captions/settings")}
|
||||
className="py-1 -my-1 px-3 -mx-3 rounded tabbable"
|
||||
>
|
||||
Customize
|
||||
{t("player.menus.captions.customizeLabel")}
|
||||
</button>
|
||||
}
|
||||
>
|
||||
Captions
|
||||
{t("player.menus.captions.title")}
|
||||
</Menu.BackLink>
|
||||
<div className="mt-3">
|
||||
<Input value={searchQuery} onInput={setSearchQuery} />
|
||||
@ -167,7 +172,7 @@ export function CaptionsView({ id }: { id: string }) {
|
||||
</div>
|
||||
<Menu.ScrollToActiveSection className="!pt-1 mt-2 pb-3">
|
||||
<CaptionOption onClick={() => disable()} selected={!lang}>
|
||||
Off
|
||||
{t("player.menus.captions.offChoice")}
|
||||
</CaptionOption>
|
||||
<CustomCaptionOption />
|
||||
{content}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { useMemo } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
@ -19,8 +20,26 @@ function useDownloadLink() {
|
||||
return url;
|
||||
}
|
||||
|
||||
function StyleTrans(props: { k: string }) {
|
||||
return (
|
||||
<Trans
|
||||
i18nKey={props.k}
|
||||
components={{
|
||||
bold: <Menu.Highlight />,
|
||||
ios_share: (
|
||||
<Icon icon={Icons.IOS_SHARE} className="inline-block text-xl -mb-1" />
|
||||
),
|
||||
ios_files: (
|
||||
<Icon icon={Icons.IOS_FILES} className="inline-block text-xl -mb-1" />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function DownloadView({ id }: { id: string }) {
|
||||
const router = useOverlayRouter(id);
|
||||
const { t } = useTranslation();
|
||||
const downloadUrl = useDownloadLink();
|
||||
|
||||
const selectedCaption = usePlayerStore((s) => s.caption?.selected);
|
||||
@ -37,31 +56,30 @@ export function DownloadView({ id }: { id: string }) {
|
||||
return (
|
||||
<>
|
||||
<Menu.BackLink onClick={() => router.navigate("/")}>
|
||||
Download
|
||||
{t("player.menus.downloads.title")}
|
||||
</Menu.BackLink>
|
||||
<Menu.Section>
|
||||
<div>
|
||||
<Menu.ChevronLink onClick={() => router.navigate("/download/pc")}>
|
||||
Downloading on PC
|
||||
{t("player.menus.downloads.onPc.title")}
|
||||
</Menu.ChevronLink>
|
||||
<Menu.ChevronLink onClick={() => router.navigate("/download/ios")}>
|
||||
Downloading on iOS
|
||||
{t("player.menus.downloads.onIos.title")}
|
||||
</Menu.ChevronLink>
|
||||
<Menu.ChevronLink
|
||||
onClick={() => router.navigate("/download/android")}
|
||||
>
|
||||
Downloading on Android
|
||||
{t("player.menus.downloads.onAndroid.title")}
|
||||
</Menu.ChevronLink>
|
||||
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Paragraph marginClass="my-6">
|
||||
Downloads are taken directly from the provider. movie-web does not
|
||||
have control over how the downloads are provided.
|
||||
<StyleTrans k="player.menus.downloads.disclaimer" />
|
||||
</Menu.Paragraph>
|
||||
|
||||
<Button className="w-full" href={downloadUrl} theme="purple">
|
||||
Download video
|
||||
{t("player.menus.downloads.downloadVideo")}
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full mt-2"
|
||||
@ -70,7 +88,7 @@ export function DownloadView({ id }: { id: string }) {
|
||||
theme="secondary"
|
||||
download="subtitles.srt"
|
||||
>
|
||||
Download current caption
|
||||
{t("player.menus.downloads.downloadCaption")}
|
||||
</Button>
|
||||
</div>
|
||||
</Menu.Section>
|
||||
@ -80,15 +98,16 @@ export function DownloadView({ id }: { id: string }) {
|
||||
|
||||
export function CantDownloadView({ id }: { id: string }) {
|
||||
const router = useOverlayRouter(id);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.BackLink onClick={() => router.navigate("/")}>
|
||||
Download
|
||||
{t("player.menus.downloads.title")}
|
||||
</Menu.BackLink>
|
||||
<Menu.Section>
|
||||
<Menu.Paragraph>
|
||||
Insert explanation for why you can't download HLS here
|
||||
<StyleTrans k="player.menus.downloads.hlsExplanation" />
|
||||
</Menu.Paragraph>
|
||||
</Menu.Section>
|
||||
</>
|
||||
@ -97,16 +116,16 @@ export function CantDownloadView({ id }: { id: string }) {
|
||||
|
||||
function AndroidExplanationView({ id }: { id: string }) {
|
||||
const router = useOverlayRouter(id);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.BackLink onClick={() => router.navigate("/download")}>
|
||||
Download / Android
|
||||
{t("player.menus.downloads.onAndroid.shortTitle")}
|
||||
</Menu.BackLink>
|
||||
<Menu.Section>
|
||||
<Menu.Paragraph>
|
||||
To download on Android, <Menu.Highlight>tap and hold</Menu.Highlight>{" "}
|
||||
on the video, then select <Menu.Highlight>save</Menu.Highlight>.
|
||||
<StyleTrans k="player.menus.downloads.onAndroid.1" />
|
||||
</Menu.Paragraph>
|
||||
</Menu.Section>
|
||||
</>
|
||||
@ -115,16 +134,16 @@ function AndroidExplanationView({ id }: { id: string }) {
|
||||
|
||||
function PCExplanationView({ id }: { id: string }) {
|
||||
const router = useOverlayRouter(id);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.BackLink onClick={() => router.navigate("/download")}>
|
||||
Download / PC
|
||||
{t("player.menus.downloads.onPc.shortTitle")}
|
||||
</Menu.BackLink>
|
||||
<Menu.Section>
|
||||
<Menu.Paragraph>
|
||||
On PC, right click the video and select{" "}
|
||||
<Menu.Highlight>Save video as</Menu.Highlight>
|
||||
<StyleTrans k="player.menus.downloads.onPc.1" />
|
||||
</Menu.Paragraph>
|
||||
</Menu.Section>
|
||||
</>
|
||||
@ -137,27 +156,11 @@ function IOSExplanationView({ id }: { id: string }) {
|
||||
return (
|
||||
<>
|
||||
<Menu.BackLink onClick={() => router.navigate("/download")}>
|
||||
Download / iOS
|
||||
<StyleTrans k="player.menus.downloads.onIos.shortTitle" />
|
||||
</Menu.BackLink>
|
||||
<Menu.Section>
|
||||
<Menu.Paragraph>
|
||||
To download on iOS, click{" "}
|
||||
<Menu.Highlight>
|
||||
<Icon
|
||||
className="inline-block text-xl -mb-1"
|
||||
icon={Icons.IOS_SHARE}
|
||||
/>
|
||||
</Menu.Highlight>
|
||||
, then{" "}
|
||||
<Menu.Highlight>
|
||||
Save to Files
|
||||
<Icon
|
||||
className="inline-block text-xl -mb-1 mx-1"
|
||||
icon={Icons.IOS_FILES}
|
||||
/>
|
||||
</Menu.Highlight>{" "}
|
||||
. All that's left to do now is to pick a nice and cozy folder for
|
||||
your video!
|
||||
<StyleTrans k="player.menus.downloads.onIos.1" />
|
||||
</Menu.Paragraph>
|
||||
</Menu.Section>
|
||||
</>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import classNames from "classnames";
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||
@ -34,6 +35,7 @@ function ButtonList(props: {
|
||||
}
|
||||
|
||||
export function PlaybackSettingsView({ id }: { id: string }) {
|
||||
const { t } = useTranslation();
|
||||
const router = useOverlayRouter(id);
|
||||
const playbackRate = usePlayerStore((s) => s.mediaPlaying.playbackRate);
|
||||
const display = usePlayerStore((s) => s.display);
|
||||
@ -50,11 +52,13 @@ export function PlaybackSettingsView({ id }: { id: string }) {
|
||||
return (
|
||||
<>
|
||||
<Menu.BackLink onClick={() => router.navigate("/")}>
|
||||
Playback settings
|
||||
{t("player.menus.playback.title")}
|
||||
</Menu.BackLink>
|
||||
<Menu.Section>
|
||||
<div className="space-y-4 mt-3">
|
||||
<Menu.FieldTitle>Playback speed</Menu.FieldTitle>
|
||||
<Menu.FieldTitle>
|
||||
{t("player.menus.playback.speedLabel")}
|
||||
</Menu.FieldTitle>
|
||||
<ButtonList
|
||||
options={options}
|
||||
selected={playbackRate}
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { t } from "i18next";
|
||||
import { useCallback } from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import { Toggle } from "@/components/buttons/Toggle";
|
||||
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||
@ -57,7 +59,7 @@ export function QualityView({ id }: { id: string }) {
|
||||
return (
|
||||
<>
|
||||
<Menu.BackLink onClick={() => router.navigate("/")}>
|
||||
Quality
|
||||
{t("player.menus.quality.title")}
|
||||
</Menu.BackLink>
|
||||
<Menu.Section>
|
||||
{visibleQualities.map((v) => (
|
||||
@ -76,14 +78,14 @@ export function QualityView({ id }: { id: string }) {
|
||||
<Menu.Link
|
||||
rightSide={<Toggle onClick={changeAutomatic} enabled={autoQuality} />}
|
||||
>
|
||||
Automatic quality
|
||||
{t("player.menus.quality.automaticLabel")}
|
||||
</Menu.Link>
|
||||
<Menu.SmallText>
|
||||
You can try{" "}
|
||||
<Menu.Anchor onClick={() => router.navigate("/source")}>
|
||||
switching source
|
||||
</Menu.Anchor>{" "}
|
||||
to get different quality options.
|
||||
<Trans i18nKey="player.menus.quality.hint">
|
||||
<Menu.Anchor onClick={() => router.navigate("/source")}>
|
||||
text
|
||||
</Menu.Anchor>
|
||||
</Trans>
|
||||
</Menu.SmallText>
|
||||
</Menu.Section>
|
||||
</>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Toggle } from "@/components/buttons/Toggle";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
@ -12,6 +13,7 @@ import { useSubtitleStore } from "@/stores/subtitles";
|
||||
import { providers } from "@/utils/providers";
|
||||
|
||||
export function SettingsMenu({ id }: { id: string }) {
|
||||
const { t } = useTranslation();
|
||||
const router = useOverlayRouter(id);
|
||||
const currentQuality = usePlayerStore((s) => s.currentQuality);
|
||||
const selectedCaptionLanguage = usePlayerStore(
|
||||
@ -26,26 +28,29 @@ export function SettingsMenu({ id }: { id: string }) {
|
||||
const { toggleLastUsed } = useCaptions();
|
||||
|
||||
const selectedLanguagePretty = selectedCaptionLanguage
|
||||
? getLanguageFromIETF(selectedCaptionLanguage) ?? "unknown"
|
||||
? getLanguageFromIETF(selectedCaptionLanguage) ??
|
||||
t("player.menus.captions.unknownLanguage")
|
||||
: undefined;
|
||||
|
||||
const source = usePlayerStore((s) => s.source);
|
||||
|
||||
return (
|
||||
<Menu.Card>
|
||||
<Menu.SectionTitle>Video settings</Menu.SectionTitle>
|
||||
<Menu.SectionTitle>
|
||||
{t("player.menus.settings.videoSection")}
|
||||
</Menu.SectionTitle>
|
||||
<Menu.Section>
|
||||
<Menu.ChevronLink
|
||||
onClick={() => router.navigate("/quality")}
|
||||
rightText={currentQuality ? qualityToString(currentQuality) : ""}
|
||||
>
|
||||
Quality
|
||||
{t("player.menus.settings.qualityItem")}
|
||||
</Menu.ChevronLink>
|
||||
<Menu.ChevronLink
|
||||
onClick={() => router.navigate("/source")}
|
||||
rightText={sourceName}
|
||||
>
|
||||
Video source
|
||||
{t("player.menus.settings.sourceItem")}
|
||||
</Menu.ChevronLink>
|
||||
<Menu.Link
|
||||
clickable
|
||||
@ -57,11 +62,13 @@ export function SettingsMenu({ id }: { id: string }) {
|
||||
rightSide={<Icon className="text-xl" icon={Icons.DOWNLOAD} />}
|
||||
className={source?.type === "file" ? "opacity-100" : "opacity-50"}
|
||||
>
|
||||
Download
|
||||
{t("player.menus.settings.downloadItem")}
|
||||
</Menu.Link>
|
||||
</Menu.Section>
|
||||
|
||||
<Menu.SectionTitle>Viewing Experience</Menu.SectionTitle>
|
||||
<Menu.SectionTitle>
|
||||
{t("player.menus.settings.experienceSection")}
|
||||
</Menu.SectionTitle>
|
||||
<Menu.Section>
|
||||
<Menu.Link
|
||||
rightSide={
|
||||
@ -71,16 +78,16 @@ export function SettingsMenu({ id }: { id: string }) {
|
||||
/>
|
||||
}
|
||||
>
|
||||
Enable Captions
|
||||
{t("player.menus.settings.enableCaptions")}
|
||||
</Menu.Link>
|
||||
<Menu.ChevronLink
|
||||
onClick={() => router.navigate("/captions")}
|
||||
rightText={selectedLanguagePretty}
|
||||
rightText={selectedLanguagePretty ?? undefined}
|
||||
>
|
||||
Caption settings
|
||||
{t("player.menus.settings.captionItem")}
|
||||
</Menu.ChevronLink>
|
||||
<Menu.ChevronLink onClick={() => router.navigate("/playback")}>
|
||||
Playback settings
|
||||
{t("player.menus.settings.playbackItem")}
|
||||
</Menu.ChevronLink>
|
||||
</Menu.Section>
|
||||
</Menu.Card>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { ReactNode, useEffect, useMemo, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Loading } from "@/components/layout/Loading";
|
||||
import {
|
||||
@ -27,13 +28,14 @@ export function EmbedOption(props: {
|
||||
sourceId: string;
|
||||
routerId: string;
|
||||
}) {
|
||||
const unknownEmbedName = "Unknown";
|
||||
const { t } = useTranslation();
|
||||
const unknownEmbedName = t("player.menus.sources.unknownOption");
|
||||
|
||||
const embedName = useMemo(() => {
|
||||
if (!props.embedId) return unknownEmbedName;
|
||||
const sourceMeta = providers.getMetadata(props.embedId);
|
||||
return sourceMeta?.name ?? unknownEmbedName;
|
||||
}, [props.embedId]);
|
||||
}, [props.embedId, unknownEmbedName]);
|
||||
|
||||
const { run, errored, loading } = useEmbedScraping(
|
||||
props.routerId,
|
||||
@ -52,6 +54,7 @@ export function EmbedOption(props: {
|
||||
}
|
||||
|
||||
export function EmbedSelectionView({ sourceId, id }: EmbedSelectionViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const router = useOverlayRouter(id);
|
||||
const { run, watching, notfound, loading, items, errored } =
|
||||
useSourceScraping(sourceId, id);
|
||||
@ -79,21 +82,26 @@ export function EmbedSelectionView({ sourceId, id }: EmbedSelectionViewProps) {
|
||||
);
|
||||
else if (notfound)
|
||||
content = (
|
||||
<Menu.TextDisplay title="No stream">
|
||||
This source has no streams for this movie or show.
|
||||
<Menu.TextDisplay
|
||||
title={t("player.menus.sources.noStream.title") ?? undefined}
|
||||
>
|
||||
{t("player.menus.sources.noStream.text")}
|
||||
</Menu.TextDisplay>
|
||||
);
|
||||
else if (items?.length === 0)
|
||||
content = (
|
||||
<Menu.TextDisplay title="No embeds found">
|
||||
We were unable to find any embeds for this source, please try another.
|
||||
<Menu.TextDisplay
|
||||
title={t("player.menus.sources.noEmbeds.title") ?? undefined}
|
||||
>
|
||||
{t("player.menus.sources.noEmbeds.text")}
|
||||
</Menu.TextDisplay>
|
||||
);
|
||||
else if (errored)
|
||||
content = (
|
||||
<Menu.TextDisplay title="Failed to scrape">
|
||||
We were unable to find any videos for this source. Don't come
|
||||
bitchin' to us about it, just try another source.
|
||||
<Menu.TextDisplay
|
||||
title={t("player.menus.sources.failed.title") ?? undefined}
|
||||
>
|
||||
{t("player.menus.sources.failed.text")}
|
||||
</Menu.TextDisplay>
|
||||
);
|
||||
else if (watching)
|
||||
@ -123,6 +131,7 @@ export function SourceSelectionView({
|
||||
id,
|
||||
onChoose,
|
||||
}: SourceSelectionViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const router = useOverlayRouter(id);
|
||||
const metaType = usePlayerStore((s) => s.meta?.type);
|
||||
const currentSourceId = usePlayerStore((s) => s.sourceId);
|
||||
@ -136,7 +145,7 @@ export function SourceSelectionView({
|
||||
return (
|
||||
<>
|
||||
<Menu.BackLink onClick={() => router.navigate("/")}>
|
||||
Sources
|
||||
{t("player.menus.sources.title")}
|
||||
</Menu.BackLink>
|
||||
<Menu.Section>
|
||||
{sources.map((v) => (
|
||||
|
@ -15,8 +15,8 @@ export function BackLink(props: { url: string }) {
|
||||
className="py-1 -my-1 px-2 -mx-2 tabbable rounded-lg flex items-center cursor-pointer text-type-secondary hover:text-white transition-colors duration-200 font-medium"
|
||||
>
|
||||
<Icon className="mr-2" icon={Icons.ARROW_LEFT} />
|
||||
<span className="md:hidden">{t("videoPlayer.backToHomeShort")}</span>
|
||||
<span className="hidden md:block">{t("videoPlayer.backToHome")}</span>
|
||||
<span className="md:hidden">{t("player.back.short")}</span>
|
||||
<span className="hidden md:block">{t("player.back.default")}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
@ -163,7 +163,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
||||
const errorDetails = getMediaErrorDetails(err);
|
||||
emit("error", {
|
||||
errorName: errorDetails.name,
|
||||
message: errorDetails.message,
|
||||
key: errorDetails.key,
|
||||
type: "htmlvideo",
|
||||
});
|
||||
});
|
||||
|
@ -4,7 +4,8 @@ import { Listener } from "@/utils/events";
|
||||
export type DisplayErrorType = "hls" | "htmlvideo";
|
||||
export type DisplayError = {
|
||||
stackTrace?: string;
|
||||
message: string;
|
||||
message?: string;
|
||||
key?: string;
|
||||
errorName: string;
|
||||
type: DisplayErrorType;
|
||||
};
|
||||
|
@ -1 +0,0 @@
|
||||
export function test() {}
|
@ -56,7 +56,7 @@ export function Paragraph(props: {
|
||||
return <p className={props.marginClass ?? "my-3"}>{props.children}</p>;
|
||||
}
|
||||
|
||||
export function Highlight(props: { children: React.ReactNode }) {
|
||||
export function Highlight(props: { children?: React.ReactNode }) {
|
||||
return <span className="text-white">{props.children}</span>;
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ export function HeadUpdater() {
|
||||
);
|
||||
}
|
||||
|
||||
const humanizedEpisodeId = t("videoPlayer.seasonAndEpisode", {
|
||||
const humanizedEpisodeId = t("media.episodeDisplay", {
|
||||
season: meta.season?.number,
|
||||
episode: meta.episode?.number,
|
||||
});
|
||||
|
@ -29,6 +29,7 @@ export function KeyboardEvents() {
|
||||
mediaPlaying,
|
||||
isRolling,
|
||||
time,
|
||||
router,
|
||||
});
|
||||
useEffect(() => {
|
||||
dataRef.current = {
|
||||
@ -41,6 +42,7 @@ export function KeyboardEvents() {
|
||||
mediaPlaying,
|
||||
isRolling,
|
||||
time,
|
||||
router,
|
||||
};
|
||||
}, [
|
||||
setShowVolume,
|
||||
@ -52,6 +54,7 @@ export function KeyboardEvents() {
|
||||
mediaPlaying,
|
||||
isRolling,
|
||||
time,
|
||||
router,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -92,7 +95,7 @@ export function KeyboardEvents() {
|
||||
dataRef.current.display?.[
|
||||
dataRef.current.mediaPlaying.isPaused ? "play" : "pause"
|
||||
]();
|
||||
if (k === "Escape") router.close();
|
||||
if (k === "Escape") dataRef.current.router.close();
|
||||
|
||||
// captions
|
||||
if (k === "c") dataRef.current.toggleLastUsed().catch(() => {}); // ignore errors
|
||||
@ -117,7 +120,7 @@ export function KeyboardEvents() {
|
||||
return () => {
|
||||
window.removeEventListener("keydown", keyEventHandler);
|
||||
};
|
||||
}, [router]);
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -38,6 +38,10 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MetaReporter occasionally reports the progress to the window object at a specific spot
|
||||
* This is used by the PreMid presence to get currently playing data
|
||||
*/
|
||||
export function MetaReporter() {
|
||||
const meta = usePlayerStore((s) => s.meta);
|
||||
const progress = usePlayerStore((s) => s.progress);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import classNames from "classnames";
|
||||
import { ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { StatusCircle } from "@/components/player/internals/StatusCircle";
|
||||
import { Transition } from "@/components/utils/Transition";
|
||||
@ -17,9 +18,9 @@ export interface ScrapeCardProps extends ScrapeItemProps {
|
||||
}
|
||||
|
||||
const statusTextMap: Partial<Record<ScrapeCardProps["status"], string>> = {
|
||||
notfound: "Doesn't have the video",
|
||||
failure: "Error occured",
|
||||
pending: "Checking for videos...",
|
||||
notfound: "player.scraping.items.notFound",
|
||||
failure: "player.scraping.items.failure",
|
||||
pending: "player.scraping.items.pending",
|
||||
};
|
||||
|
||||
const statusMap: Record<ScrapeCardProps["status"], StatusCircle["type"]> = {
|
||||
@ -31,6 +32,7 @@ const statusMap: Record<ScrapeCardProps["status"], StatusCircle["type"]> = {
|
||||
};
|
||||
|
||||
export function ScrapeItem(props: ScrapeItemProps) {
|
||||
const { t } = useTranslation();
|
||||
const text = statusTextMap[props.status];
|
||||
const status = statusMap[props.status];
|
||||
|
||||
@ -46,7 +48,7 @@ export function ScrapeItem(props: ScrapeItemProps) {
|
||||
{props.name}
|
||||
</p>
|
||||
<Transition animation="fade" show={!!text}>
|
||||
<p className="text-[15px] mt-1">{text}</p>
|
||||
<p className="text-[15px] mt-1">{text ? t(text) : ""}</p>
|
||||
</Transition>
|
||||
{props.children}
|
||||
</div>
|
||||
|
@ -1,4 +1,5 @@
|
||||
export function handleBuffered(time: number, buffered: TimeRanges): number {
|
||||
// TODO normalize the buffer sections into one section. they can be stitched together
|
||||
for (let i = 0; i < buffered.length; i += 1) {
|
||||
if (buffered.start(buffered.length - 1 - i) < time) {
|
||||
return buffered.end(buffered.length - 1 - i);
|
||||
|
@ -1,35 +1,30 @@
|
||||
const mediaErrorMap: Record<number, { name: string; message: string }> = {
|
||||
const mediaErrorMap: Record<number, { name: string; key: string }> = {
|
||||
1: {
|
||||
name: "MEDIA_ERR_ABORTED",
|
||||
message:
|
||||
"The fetching of the associated resource was aborted by the user's request.",
|
||||
key: "player.playbackError.errors.errorAborted",
|
||||
},
|
||||
2: {
|
||||
name: "MEDIA_ERR_NETWORK",
|
||||
message:
|
||||
"Some kind of network error occurred which prevented the media from being successfully fetched, despite having previously been available.",
|
||||
key: "player.playbackError.errors.errorNetwork",
|
||||
},
|
||||
3: {
|
||||
name: "MEDIA_ERR_DECODE",
|
||||
message:
|
||||
"Despite having previously been determined to be usable, an error occurred while trying to decode the media resource, resulting in an error.",
|
||||
key: "player.playbackError.errors.errorDecode",
|
||||
},
|
||||
4: {
|
||||
name: "MEDIA_ERR_SRC_NOT_SUPPORTED",
|
||||
message:
|
||||
"The associated resource or media provider object has been found to be unsuitable.",
|
||||
key: "player.playbackError.errors.errorNotSupported",
|
||||
},
|
||||
};
|
||||
|
||||
export function getMediaErrorDetails(err: MediaError | null): {
|
||||
name: string;
|
||||
message: string;
|
||||
} {
|
||||
export function getMediaErrorDetails(
|
||||
err: MediaError | null
|
||||
): (typeof mediaErrorMap)[number] {
|
||||
const item = mediaErrorMap[err?.code ?? -1];
|
||||
if (!item) {
|
||||
return {
|
||||
name: "MediaError",
|
||||
message: "Unknown media error occured",
|
||||
name: "MEDIA_ERR_GENERIC",
|
||||
key: "player.playbackError.errors.errorGenericMedia",
|
||||
};
|
||||
}
|
||||
return item;
|
||||
|
@ -13,8 +13,12 @@ export function ErrorCard(props: { error: DisplayError | string }) {
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const errorMessage =
|
||||
typeof props.error === "string" ? props.error : props.error.message;
|
||||
let errorMessage: string | null = null;
|
||||
if (typeof props.error === "string") errorMessage = props.error;
|
||||
else if (props.error.key)
|
||||
errorMessage = `${props.error.type}: ${t(props.error.key)}`;
|
||||
else if (props.error.message)
|
||||
errorMessage = `${props.error.type}: ${t(props.error.message)}`;
|
||||
|
||||
function copyError() {
|
||||
if (!props.error || !navigator.clipboard) return;
|
||||
|
Loading…
Reference in New Issue
Block a user