mirror of
https://github.com/p-stream/p-stream.git
synced 2026-05-10 06:31:08 +00:00
add expanding episode descriptions
This commit is contained in:
parent
ede3613112
commit
d4969c0540
4 changed files with 325 additions and 78 deletions
|
|
@ -477,7 +477,9 @@
|
||||||
"loadingList": "Loading...",
|
"loadingList": "Loading...",
|
||||||
"loadingTitle": "Loading...",
|
"loadingTitle": "Loading...",
|
||||||
"unairedEpisodes": "One or more episodes in this season have been disabled because they haven't been aired yet.",
|
"unairedEpisodes": "One or more episodes in this season have been disabled because they haven't been aired yet.",
|
||||||
"seasons": "Seasons"
|
"seasons": "Seasons",
|
||||||
|
"showMore": "Show more",
|
||||||
|
"showLess": "Show less"
|
||||||
},
|
},
|
||||||
"playback": {
|
"playback": {
|
||||||
"speedLabel": "Playback speed",
|
"speedLabel": "Playback speed",
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,18 @@ export function EpisodeCarousel({
|
||||||
const [showEpisodeMenu, setShowEpisodeMenu] = useState(false);
|
const [showEpisodeMenu, setShowEpisodeMenu] = useState(false);
|
||||||
const [customSeason, setCustomSeason] = useState("");
|
const [customSeason, setCustomSeason] = useState("");
|
||||||
const [customEpisode, setCustomEpisode] = useState("");
|
const [customEpisode, setCustomEpisode] = useState("");
|
||||||
|
const [expandedEpisodes, setExpandedEpisodes] = useState<{
|
||||||
|
[key: number]: boolean;
|
||||||
|
}>({});
|
||||||
|
const [truncatedEpisodes, setTruncatedEpisodes] = useState<{
|
||||||
|
[key: number]: boolean;
|
||||||
|
}>({});
|
||||||
const episodeMenuRef = useRef<HTMLDivElement>(null);
|
const episodeMenuRef = useRef<HTMLDivElement>(null);
|
||||||
const carouselRef = useRef<HTMLDivElement>(null);
|
const carouselRef = useRef<HTMLDivElement>(null);
|
||||||
const activeEpisodeRef = useRef<HTMLAnchorElement>(null);
|
const activeEpisodeRef = useRef<HTMLAnchorElement>(null);
|
||||||
|
const descriptionRefs = useRef<{
|
||||||
|
[key: number]: HTMLParagraphElement | null;
|
||||||
|
}>({});
|
||||||
|
|
||||||
const handleScroll = (direction: "left" | "right") => {
|
const handleScroll = (direction: "left" | "right") => {
|
||||||
if (!carouselRef.current) return;
|
if (!carouselRef.current) return;
|
||||||
|
|
@ -140,6 +149,40 @@ export function EpisodeCarousel({
|
||||||
(ep) => ep.season_number === selectedSeason,
|
(ep) => ep.season_number === selectedSeason,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const toggleEpisodeExpansion = (
|
||||||
|
episodeId: number,
|
||||||
|
event: React.MouseEvent,
|
||||||
|
) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setExpandedEpisodes((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[episodeId]: !prev[episodeId],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const isTextTruncated = (element: HTMLElement | null) => {
|
||||||
|
if (!element) return false;
|
||||||
|
return element.scrollHeight > element.clientHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check truncation after render and when expanded state changes
|
||||||
|
useEffect(() => {
|
||||||
|
const checkTruncation = () => {
|
||||||
|
const newTruncatedState: { [key: number]: boolean } = {};
|
||||||
|
episodes.forEach((episode) => {
|
||||||
|
if (!expandedEpisodes[episode.id]) {
|
||||||
|
const element = descriptionRefs.current[episode.id];
|
||||||
|
newTruncatedState[episode.id] = isTextTruncated(element);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setTruncatedEpisodes(newTruncatedState);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wait for the transition to complete
|
||||||
|
const timeoutId = setTimeout(checkTruncation, 250);
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}, [episodes, expandedEpisodes]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-6 md:mt-0">
|
<div className="mt-6 md:mt-0">
|
||||||
{/* Season Selector */}
|
{/* Season Selector */}
|
||||||
|
|
@ -252,6 +295,7 @@ export function EpisodeCarousel({
|
||||||
100
|
100
|
||||||
: 0;
|
: 0;
|
||||||
const isAired = hasAired(episode.air_date);
|
const isAired = hasAired(episode.air_date);
|
||||||
|
const isExpanded = expandedEpisodes[episode.id];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
|
|
@ -259,55 +303,102 @@ export function EpisodeCarousel({
|
||||||
to={getEpisodeUrl(episode)}
|
to={getEpisodeUrl(episode)}
|
||||||
ref={isActive ? activeEpisodeRef : null}
|
ref={isActive ? activeEpisodeRef : null}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"flex-shrink-0 w-52 md:w-64 rounded-lg overflow-hidden transition-all duration-200 relative cursor-pointer hover:scale-95",
|
"flex-shrink-0 transition-all duration-200 relative cursor-pointer hover:scale-95 rounded-lg overflow-hidden",
|
||||||
isActive
|
isActive
|
||||||
? "bg-video-context-hoverColor/50 hover:bg-white/5"
|
? "bg-video-context-hoverColor/50 hover:bg-white/5"
|
||||||
: "hover:bg-white/5",
|
: "hover:bg-white/5",
|
||||||
!isAired ? "opacity-50" : "",
|
!isAired ? "opacity-50" : "",
|
||||||
|
isExpanded ? "w-[32rem]" : "w-52 md:w-64",
|
||||||
|
"h-[280px]", // Fixed height for all states
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Thumbnail */}
|
{/* Thumbnail */}
|
||||||
<div className="relative aspect-video w-full bg-video-context-hoverColor">
|
{!isExpanded && (
|
||||||
{episode.still_path ? (
|
<div className="relative h-[158px] w-full bg-video-context-hoverColor">
|
||||||
<img
|
{episode.still_path ? (
|
||||||
src={`https://image.tmdb.org/t/p/w300${episode.still_path}`}
|
<img
|
||||||
alt={episode.name}
|
src={`https://image.tmdb.org/t/p/w300${episode.still_path}`}
|
||||||
className="w-full h-full object-cover"
|
alt={episode.name}
|
||||||
/>
|
className="w-full h-full object-cover"
|
||||||
) : (
|
|
||||||
<div className="w-full h-full flex items-center justify-center bg-black bg-opacity-50">
|
|
||||||
<Icon
|
|
||||||
icon={Icons.FILM}
|
|
||||||
className="text-video-context-type-main opacity-50 text-3xl"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
) : (
|
||||||
)}
|
<div className="w-full h-full flex items-center justify-center bg-black bg-opacity-50">
|
||||||
|
<Icon
|
||||||
|
icon={Icons.FILM}
|
||||||
|
className="text-video-context-type-main opacity-50 text-3xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Episode Number Badge */}
|
{/* Episode Number Badge */}
|
||||||
<div className="absolute top-2 left-2 flex items-center space-x-2">
|
<div className="absolute top-2 left-2 flex items-center space-x-2">
|
||||||
<span className="p-0.5 px-2 rounded inline bg-video-context-hoverColor bg-opacity-80 text-video-context-type-main text-sm">
|
<span className="p-0.5 px-2 rounded inline bg-video-context-hoverColor bg-opacity-80 text-video-context-type-main text-sm">
|
||||||
{t("media.episodeShort")}
|
{t("media.episodeShort")}
|
||||||
{episode.episode_number}
|
{episode.episode_number}
|
||||||
</span>
|
</span>
|
||||||
{!isAired && (
|
{!isAired && (
|
||||||
<span className="text-video-context-type-main/70 text-sm">
|
<span className="text-video-context-type-main/70 text-sm">
|
||||||
{episode.air_date
|
{episode.air_date
|
||||||
? `(${t("details.airs")} - ${new Date(episode.air_date).toLocaleDateString()})`
|
? `(${t("details.airs")} - ${new Date(episode.air_date).toLocaleDateString()})`
|
||||||
: `(${t("media.unreleased")})`}
|
: `(${t("media.unreleased")})`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"p-3",
|
||||||
|
isExpanded ? "h-full" : "h-[122px]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<h3 className="font-bold text-white line-clamp-1">
|
||||||
|
{episode.name}
|
||||||
|
</h3>
|
||||||
|
{!isExpanded && (
|
||||||
|
<span className="p-0.5 px-2 rounded inline bg-video-context-hoverColor bg-opacity-80 text-video-context-type-main text-sm">
|
||||||
|
{t("media.episodeShort")}
|
||||||
|
{episode.episode_number}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="p-3">
|
|
||||||
<h3 className="font-bold text-white line-clamp-1">
|
|
||||||
{episode.name}
|
|
||||||
</h3>
|
|
||||||
{episode.overview && (
|
{episode.overview && (
|
||||||
<p className="text-sm text-white/80 mt-1.5 line-clamp-2">
|
<div className="relative">
|
||||||
{episode.overview}
|
<p
|
||||||
</p>
|
ref={(el) => {
|
||||||
|
descriptionRefs.current[episode.id] = el;
|
||||||
|
}}
|
||||||
|
className={classNames(
|
||||||
|
"text-sm text-white/80 mt-1.5 transition-all duration-200",
|
||||||
|
!isExpanded
|
||||||
|
? "line-clamp-2"
|
||||||
|
: "max-h-[200px] overflow-y-auto scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent pr-2",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{episode.overview}
|
||||||
|
</p>
|
||||||
|
{!isExpanded && truncatedEpisodes[episode.id] && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => toggleEpisodeExpansion(episode.id, e)}
|
||||||
|
className="text-sm text-white/60 hover:text-white transition-opacity duration-200 opacity-0 animate-fade-in"
|
||||||
|
>
|
||||||
|
{t("player.menus.episodes.showMore")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{isExpanded && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => toggleEpisodeExpansion(episode.id, e)}
|
||||||
|
className="mt-2 text-sm text-white/60 hover:text-white transition-opacity duration-200 opacity-0 animate-fade-in"
|
||||||
|
>
|
||||||
|
{t("player.menus.episodes.showLess")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ function SeasonsView({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EpisodesView({
|
export function EpisodesView({
|
||||||
id,
|
id,
|
||||||
selectedSeason,
|
selectedSeason,
|
||||||
goBack,
|
goBack,
|
||||||
|
|
@ -113,6 +113,70 @@ function EpisodesView({
|
||||||
const progress = useProgressStore();
|
const progress = useProgressStore();
|
||||||
const carouselRef = useRef<HTMLDivElement>(null);
|
const carouselRef = useRef<HTMLDivElement>(null);
|
||||||
const activeEpisodeRef = useRef<HTMLDivElement>(null);
|
const activeEpisodeRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [expandedEpisodes, setExpandedEpisodes] = useState<{
|
||||||
|
[key: string]: boolean;
|
||||||
|
}>({});
|
||||||
|
const [truncatedEpisodes, setTruncatedEpisodes] = useState<{
|
||||||
|
[key: string]: boolean;
|
||||||
|
}>({});
|
||||||
|
const descriptionRefs = useRef<{
|
||||||
|
[key: string]: HTMLParagraphElement | null;
|
||||||
|
}>({});
|
||||||
|
|
||||||
|
const isTextTruncated = (element: HTMLElement | null) => {
|
||||||
|
if (!element) return false;
|
||||||
|
return element.scrollHeight > element.clientHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check truncation after render and when expanded state changes
|
||||||
|
useEffect(() => {
|
||||||
|
const checkTruncation = () => {
|
||||||
|
const newTruncatedState: { [key: string]: boolean } = {};
|
||||||
|
if (!loadingState.value) return;
|
||||||
|
|
||||||
|
loadingState.value.season.episodes.forEach((ep) => {
|
||||||
|
// Check medium view
|
||||||
|
if (!expandedEpisodes[`medium-${ep.id}`]) {
|
||||||
|
const mediumElement = descriptionRefs.current[`medium-${ep.id}`];
|
||||||
|
newTruncatedState[`medium-${ep.id}`] = isTextTruncated(mediumElement);
|
||||||
|
}
|
||||||
|
// Check large view
|
||||||
|
if (!expandedEpisodes[`large-${ep.id}`]) {
|
||||||
|
const largeElement = descriptionRefs.current[`large-${ep.id}`];
|
||||||
|
newTruncatedState[`large-${ep.id}`] = isTextTruncated(largeElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setTruncatedEpisodes(newTruncatedState);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial check
|
||||||
|
checkTruncation();
|
||||||
|
|
||||||
|
// Check after a short delay to ensure content is rendered
|
||||||
|
const timeoutId = setTimeout(checkTruncation, 250);
|
||||||
|
|
||||||
|
// Also check when window is resized
|
||||||
|
const handleResize = () => {
|
||||||
|
checkTruncation();
|
||||||
|
};
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
window.removeEventListener("resize", handleResize);
|
||||||
|
};
|
||||||
|
}, [loadingState.value, expandedEpisodes]);
|
||||||
|
|
||||||
|
const toggleEpisodeExpansion = (
|
||||||
|
episodeId: string,
|
||||||
|
event: React.MouseEvent,
|
||||||
|
) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
setExpandedEpisodes((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[episodeId]: !prev[episodeId],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
const playEpisode = useCallback(
|
const playEpisode = useCallback(
|
||||||
(episodeId: string) => {
|
(episodeId: string) => {
|
||||||
|
|
@ -307,9 +371,44 @@ function EpisodesView({
|
||||||
{ep.title}
|
{ep.title}
|
||||||
</h3>
|
</h3>
|
||||||
{ep.overview && (
|
{ep.overview && (
|
||||||
<p className="text-sm text-white/80 mt-1.5 line-clamp-2">
|
<div className="relative">
|
||||||
{ep.overview}
|
<p
|
||||||
</p>
|
ref={(el) => {
|
||||||
|
descriptionRefs.current[`medium-${ep.id}`] = el;
|
||||||
|
}}
|
||||||
|
className={classNames(
|
||||||
|
"text-sm text-white/80 mt-1.5 transition-all duration-200",
|
||||||
|
!expandedEpisodes[`medium-${ep.id}`]
|
||||||
|
? "line-clamp-2"
|
||||||
|
: "max-h-[200px] overflow-y-auto scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent pr-2",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{ep.overview}
|
||||||
|
</p>
|
||||||
|
{!expandedEpisodes[`medium-${ep.id}`] &&
|
||||||
|
truncatedEpisodes[`medium-${ep.id}`] && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) =>
|
||||||
|
toggleEpisodeExpansion(`medium-${ep.id}`, e)
|
||||||
|
}
|
||||||
|
className="text-sm text-white/60 hover:text-white transition-opacity duration-200 opacity-0 animate-fade-in"
|
||||||
|
>
|
||||||
|
{t("player.menus.episodes.showMore")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{expandedEpisodes[`medium-${ep.id}`] && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) =>
|
||||||
|
toggleEpisodeExpansion(`medium-${ep.id}`, e)
|
||||||
|
}
|
||||||
|
className="mt-2 text-sm text-white/60 hover:text-white transition-opacity duration-200 opacity-0 animate-fade-in"
|
||||||
|
>
|
||||||
|
{t("player.menus.episodes.showLess")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -330,54 +429,102 @@ function EpisodesView({
|
||||||
<div
|
<div
|
||||||
onClick={() => playEpisode(ep.id)}
|
onClick={() => playEpisode(ep.id)}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"hidden lg:block flex-shrink-0 w-64 rounded-lg overflow-hidden transition-all duration-200 relative cursor-pointer",
|
"hidden lg:block flex-shrink-0 transition-all duration-200 relative cursor-pointer rounded-lg overflow-hidden",
|
||||||
isActive
|
isActive
|
||||||
? "bg-video-context-hoverColor/50"
|
? "bg-video-context-hoverColor/50"
|
||||||
: "hover:bg-video-context-hoverColor/50",
|
: "hover:bg-video-context-hoverColor/50",
|
||||||
!isAired ? "opacity-50" : "hover:scale-95",
|
!isAired ? "opacity-50" : "hover:scale-95",
|
||||||
|
expandedEpisodes[`large-${ep.id}`] ? "w-[32rem]" : "w-64",
|
||||||
|
"h-[280px]", // Fixed height for all states
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Thumbnail */}
|
{/* Thumbnail */}
|
||||||
<div className="relative aspect-video w-full bg-video-context-hoverColor">
|
{!expandedEpisodes[`large-${ep.id}`] && (
|
||||||
{ep.still_path ? (
|
<div className="relative h-[158px] w-full bg-video-context-hoverColor">
|
||||||
<img
|
{ep.still_path ? (
|
||||||
src={`https://image.tmdb.org/t/p/w300${ep.still_path}`}
|
<img
|
||||||
alt={ep.title}
|
src={`https://image.tmdb.org/t/p/w300${ep.still_path}`}
|
||||||
className="w-full h-full object-cover"
|
alt={ep.title}
|
||||||
/>
|
className="w-full h-full object-cover"
|
||||||
) : (
|
|
||||||
<div className="w-full h-full flex items-center justify-center bg-black bg-opacity-50">
|
|
||||||
<Icon
|
|
||||||
icon={Icons.FILM}
|
|
||||||
className="text-video-context-type-main opacity-50 text-3xl"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
) : (
|
||||||
)}
|
<div className="w-full h-full flex items-center justify-center bg-black bg-opacity-50">
|
||||||
|
<Icon
|
||||||
{/* Episode Number Badge */}
|
icon={Icons.FILM}
|
||||||
<div className="absolute top-2 left-2 flex items-center space-x-2">
|
className="text-video-context-type-main opacity-50 text-3xl"
|
||||||
<span className="p-0.5 px-2 rounded inline bg-video-context-hoverColor bg-opacity-80 text-video-context-type-main text-sm">
|
/>
|
||||||
E{ep.number}
|
</div>
|
||||||
</span>
|
|
||||||
{!isAired && (
|
|
||||||
<span className="text-video-context-type-main/70 text-sm">
|
|
||||||
{ep.air_date
|
|
||||||
? `(${t("details.airs")} - ${new Date(ep.air_date).toLocaleDateString()})`
|
|
||||||
: `(${t("media.unreleased")})`}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Episode Number Badge */}
|
||||||
|
<div className="absolute top-2 left-2 flex items-center space-x-2">
|
||||||
|
<span className="p-0.5 px-2 rounded inline bg-video-context-hoverColor bg-opacity-80 text-video-context-type-main text-sm">
|
||||||
|
E{ep.number}
|
||||||
|
</span>
|
||||||
|
{!isAired && (
|
||||||
|
<span className="text-video-context-type-main/70 text-sm">
|
||||||
|
{ep.air_date
|
||||||
|
? `(${t("details.airs")} - ${new Date(ep.air_date).toLocaleDateString()})`
|
||||||
|
: `(${t("media.unreleased")})`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="p-3">
|
<div
|
||||||
<h3 className="font-bold text-white line-clamp-1">
|
className={classNames(
|
||||||
{ep.title}
|
"p-3",
|
||||||
</h3>
|
expandedEpisodes[`large-${ep.id}`]
|
||||||
|
? "h-full"
|
||||||
|
: "h-[122px]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<h3 className="font-bold text-white line-clamp-1">
|
||||||
|
{ep.title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
{ep.overview && (
|
{ep.overview && (
|
||||||
<p className="text-sm text-white/80 mt-1.5 line-clamp-2">
|
<div className="relative">
|
||||||
{ep.overview}
|
<p
|
||||||
</p>
|
ref={(el) => {
|
||||||
|
descriptionRefs.current[`large-${ep.id}`] = el;
|
||||||
|
}}
|
||||||
|
className={classNames(
|
||||||
|
"text-sm text-white/80 mt-1.5 transition-all duration-200",
|
||||||
|
!expandedEpisodes[`large-${ep.id}`]
|
||||||
|
? "line-clamp-2"
|
||||||
|
: "max-h-[200px] overflow-y-auto scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent pr-2",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{ep.overview}
|
||||||
|
</p>
|
||||||
|
{!expandedEpisodes[`large-${ep.id}`] &&
|
||||||
|
truncatedEpisodes[`large-${ep.id}`] && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) =>
|
||||||
|
toggleEpisodeExpansion(`large-${ep.id}`, e)
|
||||||
|
}
|
||||||
|
className="text-sm text-white/60 hover:text-white transition-opacity duration-200 opacity-0 animate-fade-in"
|
||||||
|
>
|
||||||
|
{t("player.menus.episodes.showMore")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{expandedEpisodes[`large-${ep.id}`] && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) =>
|
||||||
|
toggleEpisodeExpansion(`large-${ep.id}`, e)
|
||||||
|
}
|
||||||
|
className="mt-2 text-sm text-white/60 hover:text-white transition-opacity duration-200 opacity-0 animate-fade-in"
|
||||||
|
>
|
||||||
|
{t("player.menus.episodes.showLess")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -456,7 +603,7 @@ function EpisodesOverlay({
|
||||||
<OverlayPage id={id} path="/" width={343} height={431}>
|
<OverlayPage id={id} path="/" width={343} height={431}>
|
||||||
<SeasonsView setSeason={setSeason} selectedSeason={selectedSeason} />
|
<SeasonsView setSeason={setSeason} selectedSeason={selectedSeason} />
|
||||||
</OverlayPage>
|
</OverlayPage>
|
||||||
<OverlayPage id={id} path="/episodes" width={0} height={360} fullWidth>
|
<OverlayPage id={id} path="/episodes" width={0} height={375} fullWidth>
|
||||||
{selectedSeason.length > 0 ? (
|
{selectedSeason.length > 0 ? (
|
||||||
<EpisodesView
|
<EpisodesView
|
||||||
selectedSeason={selectedSeason}
|
selectedSeason={selectedSeason}
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,15 @@ const config: Config = {
|
||||||
"0%, 40%, 100%": { height: "0.5em", "background-color": "#282336" },
|
"0%, 40%, 100%": { height: "0.5em", "background-color": "#282336" },
|
||||||
"20%": { height: "1em", "background-color": "white" },
|
"20%": { height: "1em", "background-color": "white" },
|
||||||
},
|
},
|
||||||
|
"fade-in": {
|
||||||
|
"0%": { opacity: "0" },
|
||||||
|
"100%": { opacity: "1" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"loading-pin": "loading-pin 1.8s ease-in-out infinite",
|
||||||
|
"fade-in": "fade-in 200ms ease-out forwards",
|
||||||
},
|
},
|
||||||
animation: { "loading-pin": "loading-pin 1.8s ease-in-out infinite" },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue