diff --git a/front/packages/models/src/resources/watch-info.ts b/front/packages/models/src/resources/watch-info.ts index 6346c449..9990e935 100644 --- a/front/packages/models/src/resources/watch-info.ts +++ b/front/packages/models/src/resources/watch-info.ts @@ -29,7 +29,7 @@ const getDisplayName = (sub: Track) => { if (lng && sub.title && sub.title !== lng) return `${lng} - ${sub.title}`; if (lng) return lng; if (sub.title) return sub.title; - return `Unknwon (${sub.index})`; + return `Unknown (${sub.index})`; }; /** diff --git a/front/packages/ui/src/components/context-menus.tsx b/front/packages/ui/src/components/context-menus.tsx index ac3aa9c4..3460aeae 100644 --- a/front/packages/ui/src/components/context-menus.tsx +++ b/front/packages/ui/src/components/context-menus.tsx @@ -19,7 +19,7 @@ */ import { IconButton, Menu, tooltip } from "@kyoo/primitives"; -import { ComponentProps } from "react"; +import { ComponentProps, ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import MoreVert from "@material-symbols/svg-400/rounded/more_vert.svg"; import Info from "@material-symbols/svg-400/rounded/info.svg"; @@ -30,6 +30,7 @@ import { watchListIcon } from "./watchlist-info"; import { useDownloader } from "../downloads"; import { Platform } from "react-native"; import { useYoshiki } from "yoshiki/native"; +import { MediaInfoPopup } from "./media-info"; export const EpisodesContext = ({ type = "episode", @@ -49,6 +50,7 @@ export const EpisodesContext = ({ const downloader = useDownloader(); const { css } = useYoshiki(); const { t } = useTranslation(); + const [popup, setPopup] = useState(); const queryClient = useQueryClient(); const mutation = useMutation({ @@ -61,40 +63,65 @@ export const EpisodesContext = ({ }); return ( - - {showSlug && ( - - )} - + - {Object.values(WatchStatusV).map((x) => ( + {showSlug && ( mutation.mutate(x)} - selected={x === status} + label={t("home.episodeMore.goToShow")} + icon={Info} + href={`/show/${showSlug}`} /> - ))} - {status !== null && ( - mutation.mutate(null)} /> )} - - {type !== "show" && ( - downloader(type, slug)} - /> - )} - + + {Object.values(WatchStatusV).map((x) => ( + mutation.mutate(x)} + selected={x === status} + /> + ))} + {status !== null && ( + mutation.mutate(null)} + /> + )} + + {type !== "show" && ( + <> + downloader(type, slug)} + /> + + setPopup( + setPopup(undefined)} + />, + ) + } + /> + + )} + + {popup} + ); }; diff --git a/front/packages/ui/src/components/media-info.tsx b/front/packages/ui/src/components/media-info.tsx new file mode 100644 index 00000000..81944dc2 --- /dev/null +++ b/front/packages/ui/src/components/media-info.tsx @@ -0,0 +1,125 @@ +/* + * Kyoo - A portable and vast media library solution. + * Copyright (c) Kyoo. + * + * See AUTHORS.md and LICENSE file in the project root for full license information. + * + * Kyoo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Kyoo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Kyoo. If not, see . + */ + +import { Audio, Subtitle, WatchInfo, WatchInfoP } from "@kyoo/models"; +import { Button, HR, P, Popup, Skeleton } from "@kyoo/primitives"; +import { Fetch } from "../fetch"; +import { useTranslation } from "react-i18next"; +import { View } from "react-native"; +import { useYoshiki } from "yoshiki"; +import { NativeCssFunc } from "yoshiki/src/native/type"; + +const MediaInfoTable = ({ + mediaInfo: { path, video, container, audios, subtitles }, + css, +}: { + css: NativeCssFunc; + mediaInfo: Partial; +}) => { + const { t } = useTranslation(); + const formatBitrate = (b: number) => `${(b / 1000000).toFixed(2)} Mbps`; + const formatTrackTable = (trackTable: (Audio | Subtitle)[], s: string) => { + if (trackTable.length == 0) { + return undefined; + } + return trackTable.reduce( + (collected, audioTrack, index) => ({ + ...collected, + // If there is only one track, we do not need to show an index + [trackTable.length == 1 ? t(s) : `${t(s)} ${index + 1}`]: [ + audioTrack.displayName, + audioTrack.isDefault ? t("Default") : undefined, + audioTrack.isForced ? t("Forced") : undefined, + audioTrack.codec, + ] + .filter((x) => x !== undefined) + .join(" - "), + }), + {} as Record, + ); + }; + const table = ( + [ + { + [t("mediainfo.file")]: path?.replace(/^\/video\//, ''), + [t("mediainfo.container")]: container, + }, + { + [t("mediainfo.video")]: video + ? `${video.width}x${video.height} (${video.quality}) - ${formatBitrate( + video.bitrate, + )} - ${video.codec}` + : undefined, + }, + audios === undefined + ? { [t("mediainfo.audio")]: undefined } + : formatTrackTable(audios, "mediainfo.audio"), + subtitles === undefined + ? { [t("mediainfo.subtitles")]: undefined } + : formatTrackTable(subtitles, "mediainfo.subtitles"), + ] as const + ).filter((x) => x !== undefined) as Record[]; + return ( + + {table.map((g) => + Object.entries(g).map(([label, value], index, l) => ( + <> + + +

{label}

+
+ + {value ?

{value}

: undefined}
+
+
+ {index == l.length - 1 &&
} + + )), + )} +
+ ); +}; + +export const MediaInfoPopup = ({ + close, + mediaType, + mediaSlug, +}: { + close: () => void; + mediaType: "episode" | "movie"; + mediaSlug: string; +}) => { + const mediaInfoQuery = { + path: ["video", mediaType, mediaSlug, "info"], + parser: WatchInfoP, + }; + return ( + + {({ css }) => ( + <> + + {(mediaInfo) => } + +