mirror of
https://github.com/zoriya/Kyoo.git
synced 2026-02-14 15:32:13 -05:00
Handle multi video entries
This commit is contained in:
parent
3c106844aa
commit
65756e2e1c
@ -41,7 +41,9 @@
|
||||
"null": "Mark as not seen"
|
||||
},
|
||||
"nextUp": "Next up",
|
||||
"season": "Season {{number}}"
|
||||
"season": "Season {{number}}",
|
||||
"multiVideos": "Multiples video files available",
|
||||
"videosCount": "{{number}} videos"
|
||||
},
|
||||
"browse": {
|
||||
"mediatypekey": {
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import ExpandMore from "@material-symbols/svg-400/rounded/keyboard_arrow_down-fill.svg";
|
||||
import ExpandLess from "@material-symbols/svg-400/rounded/keyboard_arrow_up-fill.svg";
|
||||
import MultipleVideos from "@material-symbols/svg-400/rounded/subscriptions-fill.svg";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type PressableProps, View } from "react-native";
|
||||
@ -7,12 +6,13 @@ import { EntryContext } from "~/components/items/context-menus";
|
||||
import { ItemProgress } from "~/components/items/item-grid";
|
||||
import type { KImage } from "~/models";
|
||||
import {
|
||||
CroppedText,
|
||||
Heading,
|
||||
IconButton,
|
||||
Icon,
|
||||
Image,
|
||||
ImageBackground,
|
||||
Link,
|
||||
P,
|
||||
PressableFeedback,
|
||||
Skeleton,
|
||||
SubP,
|
||||
tooltip,
|
||||
@ -35,6 +35,7 @@ export const EntryLine = ({
|
||||
watchedPercent,
|
||||
href,
|
||||
className,
|
||||
videosCount,
|
||||
...props
|
||||
}: {
|
||||
slug: string;
|
||||
@ -50,9 +51,9 @@ export const EntryLine = ({
|
||||
runtime: number | null;
|
||||
watchedPercent: number | null;
|
||||
href: string | null;
|
||||
videosCount: number;
|
||||
} & PressableProps) => {
|
||||
const [moreOpened, setMoreOpened] = useState(false);
|
||||
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@ -71,7 +72,7 @@ export const EntryLine = ({
|
||||
quality="low"
|
||||
alt=""
|
||||
className={cn(
|
||||
"m-1 w-1/5 shrink-0 rounded",
|
||||
"mr-1 w-1/5 shrink-0 rounded",
|
||||
poster ? "aspect-2/3" : "aspect-video",
|
||||
"group-hover:ring-3 group-hover:ring-accent group-focus-visible:ring-3 group-focus-visible:ring-accent",
|
||||
)}
|
||||
@ -81,12 +82,12 @@ export const EntryLine = ({
|
||||
)}
|
||||
</ImageBackground>
|
||||
<View className="m-1 mx-2 flex-1">
|
||||
<View className="flex-1 flex-row justify-between">
|
||||
<View className="mb-5 flex-1">
|
||||
<View className="mb-5 flex-1 flex-row">
|
||||
<View className="flex-1 flex-row items-center">
|
||||
<Heading
|
||||
className={cn(
|
||||
"font-medium group-hover:underline group-focus-visible:underline",
|
||||
"text-lg",
|
||||
"shrink font-medium text-lg",
|
||||
"group-hover:underline group-focus-visible:underline",
|
||||
)}
|
||||
>
|
||||
{[displayNumber, name ?? t("show.episodeNoMetadata")]
|
||||
@ -95,47 +96,48 @@ export const EntryLine = ({
|
||||
</Heading>
|
||||
{tagline && <Heading>{tagline}</Heading>}
|
||||
</View>
|
||||
<View className="flex-row items-center">
|
||||
<SubP>
|
||||
{[
|
||||
airDate
|
||||
? // @ts-expect-error Source https://www.i18next.com/translation-function/formatting#datetime
|
||||
t("{{val, datetime}}", { val: airDate })
|
||||
: null,
|
||||
displayRuntime(runtime),
|
||||
]
|
||||
.filter((item) => item != null)
|
||||
.join(" · ")}
|
||||
</SubP>
|
||||
<View className="flex-row">
|
||||
<View className="flex-col-reverse justify-end md:flex-row md:items-center">
|
||||
{videosCount > 1 && (
|
||||
<PressableFeedback
|
||||
className="flex-row items-center rounded-2xl bg-popover p-2 md:mx-4"
|
||||
{...tooltip(t("show.multiVideos"))}
|
||||
>
|
||||
<Icon
|
||||
icon={MultipleVideos}
|
||||
fillClassName="accent-accent dark:accent-slate-400"
|
||||
/>
|
||||
<SubP className="ml-2">
|
||||
{t("show.videosCount", { number: videosCount })}
|
||||
</SubP>
|
||||
</PressableFeedback>
|
||||
)}
|
||||
<SubP>
|
||||
{[
|
||||
airDate
|
||||
? // @ts-expect-error Source https://www.i18next.com/translation-function/formatting#datetime
|
||||
t("{{val, datetime}}", { val: airDate })
|
||||
: null,
|
||||
displayRuntime(runtime),
|
||||
]
|
||||
.filter((item) => item != null)
|
||||
.join(" · ")}
|
||||
</SubP>
|
||||
</View>
|
||||
<EntryContext
|
||||
slug={slug}
|
||||
serieSlug={serieSlug}
|
||||
isOpen={moreOpened}
|
||||
setOpen={(v) => setMoreOpened(v)}
|
||||
className={cn(
|
||||
"ml-3 flex",
|
||||
"not:web:opacity-100 opacity-0 focus-visible:opacity-100 group-focus-within:opacity-100 group-hover:opacity-100",
|
||||
"ml-3 flex native:hidden",
|
||||
"opacity-0 focus-visible:opacity-100 group-focus-within:opacity-100 group-hover:opacity-100",
|
||||
moreOpened && "opacity-100",
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View className="flex-row justify-between">
|
||||
<P numberOfLines={descriptionExpanded ? undefined : 3}>
|
||||
{description}
|
||||
</P>
|
||||
<IconButton
|
||||
className="not:web:opacity-100 opacity-0 focus-visible:opacity-100 group-focus-within:opacity-100 group-hover:opacity-100"
|
||||
icon={descriptionExpanded ? ExpandLess : ExpandMore}
|
||||
{...tooltip(
|
||||
t(descriptionExpanded ? "misc.collapse" : "misc.expand"),
|
||||
)}
|
||||
onPress={(e) => {
|
||||
e.preventDefault();
|
||||
setDescriptionExpanded((isExpanded) => !isExpanded);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<CroppedText numberOfLines={3}>{description}</CroppedText>
|
||||
</View>
|
||||
</Link>
|
||||
);
|
||||
|
||||
@ -21,20 +21,31 @@ const BaseIcon = withUniwind(IconWrapper, {
|
||||
fromClassName: "fillClassName",
|
||||
styleProperty: "accentColor",
|
||||
},
|
||||
width: {
|
||||
fromClassName: "widthClassName",
|
||||
styleProperty: "width",
|
||||
},
|
||||
height: {
|
||||
fromClassName: "heightClassName",
|
||||
styleProperty: "height",
|
||||
},
|
||||
});
|
||||
|
||||
export const Icon = ({
|
||||
className,
|
||||
fillClassName,
|
||||
widthClassName,
|
||||
heightClassName,
|
||||
...props
|
||||
}: ComponentProps<typeof BaseIcon>) => {
|
||||
return (
|
||||
<BaseIcon
|
||||
fillClassName={cn(
|
||||
"accent-slate-600 dark:accent-slate-400",
|
||||
fillClassName,
|
||||
)}
|
||||
className={cn("h-6 w-6 shrink-0", className)}
|
||||
fillClassName={
|
||||
fillClassName ? fillClassName : "accent-slate-600 dark:accent-slate-400"
|
||||
}
|
||||
widthClassName={cn("w-6", widthClassName)}
|
||||
heightClassName={cn("h-6", heightClassName)}
|
||||
className={cn("shrink-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@ -58,7 +69,7 @@ export const IconButton = <AsProps = PressableProps>({
|
||||
<Container
|
||||
focusRipple
|
||||
className={cn(
|
||||
"m-1 self-center overflow-hidden rounded-full p-2",
|
||||
"m-2 h-6 w-6 self-center overflow-hidden rounded-full",
|
||||
"hover:bg-gray-300 focus-visible:bg-gray-300 focus-visible:dark:bg-gray-700 hover:dark:bg-gray-700",
|
||||
className,
|
||||
)}
|
||||
|
||||
@ -7,9 +7,20 @@ import {
|
||||
H6 as EH6,
|
||||
P as EP,
|
||||
} from "@expo/html-elements";
|
||||
import type { ComponentProps, ComponentType } from "react";
|
||||
import { Text } from "react-native";
|
||||
import ExpandMore from "@material-symbols/svg-400/rounded/keyboard_arrow_down-fill.svg";
|
||||
import ExpandLess from "@material-symbols/svg-400/rounded/keyboard_arrow_up-fill.svg";
|
||||
import {
|
||||
type ComponentProps,
|
||||
type ComponentType,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Platform, Text, View, type ViewProps } from "react-native";
|
||||
import { cn } from "~/utils";
|
||||
import { IconButton } from "./icons";
|
||||
import { tooltip } from "./tooltip";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const styleText = (
|
||||
Component: ComponentType<ComponentProps<typeof EP>>,
|
||||
@ -53,3 +64,52 @@ export const LI = ({ children, ...props }: ComponentProps<typeof P>) => {
|
||||
</P>
|
||||
);
|
||||
};
|
||||
|
||||
export const CroppedText = ({
|
||||
className,
|
||||
numberOfLines,
|
||||
onTextLayout,
|
||||
ref,
|
||||
containerProps,
|
||||
children,
|
||||
...props
|
||||
}: { containerProps?: ViewProps } & ComponentProps<typeof P>) => {
|
||||
const desc = useRef<HTMLElement>(null);
|
||||
const [expended, setExpanded] = useState(false);
|
||||
const [needExpand, setNeedExpand] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (Platform.OS !== "web" || !desc.current || expended) return;
|
||||
setNeedExpand(desc.current.scrollHeight > desc.current.clientHeight + 1);
|
||||
});
|
||||
|
||||
return (
|
||||
<View className="flex-row justify-between" {...(containerProps ?? {})}>
|
||||
<P
|
||||
ref={ref}
|
||||
numberOfLines={expended ? undefined : numberOfLines}
|
||||
onTextLayout={(e) => {
|
||||
const visible = e.nativeEvent.lines.reduce(
|
||||
(acc, line) => acc + line.text,
|
||||
"",
|
||||
);
|
||||
setNeedExpand(visible !== children);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</P>
|
||||
{needExpand && (
|
||||
<IconButton
|
||||
icon={expended ? ExpandLess : ExpandMore}
|
||||
{...tooltip(t(expended ? "misc.collapse" : "misc.expand"))}
|
||||
onPress={(e) => {
|
||||
e.preventDefault();
|
||||
setExpanded((isExpanded) => !isExpanded);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@ -113,7 +113,7 @@ export const InfiniteFetch = <Data, Type extends string = string>({
|
||||
contentContainerStyle={{
|
||||
...contentContainerStyle,
|
||||
gap,
|
||||
marginHorizontal: gap,
|
||||
marginHorizontal: numColumns > 1 ? gap : 0,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@ -44,7 +44,9 @@ export const SeasonHeader = ({
|
||||
className={cn("m-1 flex-row", className)}
|
||||
{...props}
|
||||
>
|
||||
<P className="mx-1 w-16 shrink-0 text-center text-2xl">{seasonNumber}</P>
|
||||
<P className="mx-1 w-16 shrink-0 text-center text-2xl text-accent">
|
||||
{seasonNumber}
|
||||
</P>
|
||||
<H2 className="mx-1 flex-1 text-2xl">
|
||||
{name ?? t("show.season", { number: seasonNumber })}
|
||||
</H2>
|
||||
@ -131,6 +133,7 @@ export const EntryList = ({
|
||||
as={EntryLine}
|
||||
{...item}
|
||||
// Don't display "Go to serie"
|
||||
videosCount={item.videos.length}
|
||||
serieSlug={null}
|
||||
displayNumber={entryDisplayNumber(item)}
|
||||
watchedPercent={item.progress.percent}
|
||||
|
||||
@ -43,6 +43,7 @@ export const NextUp = (nextEntry: Entry) => {
|
||||
<EntryLine
|
||||
{...nextEntry}
|
||||
serieSlug={null}
|
||||
videosCount={nextEntry.videos.length}
|
||||
watchedPercent={nextEntry.progress.percent}
|
||||
displayNumber={entryDisplayNumber(nextEntry)}
|
||||
/>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user