mirror of
https://github.com/p-stream/p-stream.git
synced 2026-07-26 14:32:12 +00:00
make details modal load 10x faster
make each component load individually instead of waiting for them all
This commit is contained in:
parent
54e26c8a52
commit
4976d38830
4 changed files with 108 additions and 26 deletions
|
|
@ -345,10 +345,36 @@ type MediaDetailReturn<T extends TMDBContentTypes> =
|
|||
? TMDBShowData
|
||||
: never;
|
||||
|
||||
export async function getSeasonDetails(
|
||||
id: string,
|
||||
season: number,
|
||||
): Promise<
|
||||
Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
episode_number: number;
|
||||
overview: string;
|
||||
still_path: string | null;
|
||||
air_date: string;
|
||||
season_number: number;
|
||||
}>
|
||||
> {
|
||||
const seasonData = await get<TMDBSeason>(`/tv/${id}/season/${season}`);
|
||||
return seasonData.episodes.map((episode) => ({
|
||||
id: episode.id,
|
||||
name: episode.name,
|
||||
episode_number: episode.episode_number,
|
||||
overview: episode.overview,
|
||||
still_path: episode.still_path,
|
||||
air_date: episode.air_date,
|
||||
season_number: season,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getMediaDetails<
|
||||
T extends TMDBContentTypes,
|
||||
TReturn = MediaDetailReturn<T>,
|
||||
>(id: string, type: T): Promise<TReturn> {
|
||||
>(id: string, type: T, fetchEpisodes: boolean = true): Promise<TReturn> {
|
||||
if (type === TMDBContentTypes.MOVIE) {
|
||||
return get<TReturn>(`/movie/${id}`, {
|
||||
append_to_response: "external_ids,credits,release_dates",
|
||||
|
|
@ -359,6 +385,13 @@ export async function getMediaDetails<
|
|||
append_to_response: "external_ids,credits,content_ratings",
|
||||
});
|
||||
|
||||
if (!fetchEpisodes) {
|
||||
return {
|
||||
...showData,
|
||||
episodes: [],
|
||||
} as TReturn;
|
||||
}
|
||||
|
||||
// Fetch episodes for each season
|
||||
const showDetails = showData as TMDBShowData;
|
||||
const allEpisodesBySeason = new Array(showDetails.seasons.length);
|
||||
|
|
@ -375,18 +408,8 @@ export async function getMediaDetails<
|
|||
const item = seasonsQueue.shift();
|
||||
if (!item) break;
|
||||
const { season, index } = item;
|
||||
const seasonData = await get<TMDBSeason>(
|
||||
`/tv/${id}/season/${season.season_number}`,
|
||||
);
|
||||
allEpisodesBySeason[index] = seasonData.episodes.map((episode) => ({
|
||||
id: episode.id,
|
||||
name: episode.name,
|
||||
episode_number: episode.episode_number,
|
||||
overview: episode.overview,
|
||||
still_path: episode.still_path,
|
||||
air_date: episode.air_date,
|
||||
season_number: season.season_number,
|
||||
}));
|
||||
const episodes = await getSeasonDetails(id, season.season_number);
|
||||
allEpisodesBySeason[index] = episodes;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,12 @@ import { Icon, Icons } from "@/components/Icon";
|
|||
import { Modal, ModalCard, useModal } from "@/components/overlays/Modal";
|
||||
import { hasAired } from "@/components/player/utils/aired";
|
||||
import { useBookmarkStore } from "@/stores/bookmarks";
|
||||
import { getProgressPercentage, useProgressStore } from "@/stores/progress";
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import {
|
||||
ProgressEpisodeItem,
|
||||
getProgressPercentage,
|
||||
useProgressStore,
|
||||
} from "@/stores/progress";
|
||||
|
||||
import { EpisodeCarouselProps } from "../../types";
|
||||
|
||||
|
|
@ -256,16 +261,15 @@ export function EpisodeCarousel({
|
|||
const watchedStats = useMemo(() => {
|
||||
if (!mediaId || !totalEpisodes) return { watched: 0, percentage: 0 };
|
||||
|
||||
const item = progress[mediaId.toString()];
|
||||
if (!item?.episodes) return { watched: 0, percentage: 0 };
|
||||
|
||||
let watchedCount = 0;
|
||||
episodes.forEach((episode) => {
|
||||
const episodeProgress =
|
||||
progress[mediaId.toString()]?.episodes?.[episode.id];
|
||||
const percentage = episodeProgress
|
||||
? getProgressPercentage(
|
||||
episodeProgress.progress.watched,
|
||||
episodeProgress.progress.duration,
|
||||
)
|
||||
: 0;
|
||||
Object.values<ProgressEpisodeItem>(item.episodes).forEach((episode) => {
|
||||
const percentage = getProgressPercentage(
|
||||
episode.progress.watched,
|
||||
episode.progress.duration,
|
||||
);
|
||||
if (percentage > 90) {
|
||||
watchedCount += 1;
|
||||
}
|
||||
|
|
@ -274,7 +278,7 @@ export function EpisodeCarousel({
|
|||
const percentage = Math.round((watchedCount / totalEpisodes) * 100);
|
||||
|
||||
return { watched: watchedCount, percentage };
|
||||
}, [episodes, progress, mediaId, totalEpisodes]);
|
||||
}, [progress, mediaId, totalEpisodes]);
|
||||
|
||||
// Load favorite episodes when favorites is selected
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { t } from "i18next";
|
|||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCopyToClipboard } from "react-use";
|
||||
|
||||
import { getSeasonDetails } from "@/backend/metadata/tmdb";
|
||||
import { getNetworkContent } from "@/backend/metadata/traktApi";
|
||||
import { TMDBContentTypes } from "@/backend/metadata/types/tmdb";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
|
|
@ -33,6 +34,12 @@ export function DetailsContent({ data, minimal = false }: DetailsContentProps) {
|
|||
const [showTrailer, setShowTrailer] = useState(false);
|
||||
const [showCollection, setShowCollection] = useState(false);
|
||||
const [selectedSeason, setSelectedSeason] = useState<number>(1);
|
||||
const [fetchedSeasons, setFetchedSeasons] = useState<Record<number, any[]>>(
|
||||
{},
|
||||
);
|
||||
const [loadingSeasons, setLoadingSeasons] = useState<Record<number, boolean>>(
|
||||
{},
|
||||
);
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
const [hasCopiedShare, setHasCopiedShare] = useState(false);
|
||||
const [logoHeight, setLogoHeight] = useState<number>(0);
|
||||
|
|
@ -69,6 +76,54 @@ export function DetailsContent({ data, minimal = false }: DetailsContentProps) {
|
|||
}
|
||||
}, [showProgress]);
|
||||
|
||||
// Fetch episodes for selected season
|
||||
useEffect(() => {
|
||||
const fetchSeason = async (seasonNumber: number) => {
|
||||
if (
|
||||
!data.id ||
|
||||
seasonNumber === -1 ||
|
||||
fetchedSeasons[seasonNumber] ||
|
||||
loadingSeasons[seasonNumber]
|
||||
)
|
||||
return;
|
||||
|
||||
setLoadingSeasons((prev) => ({ ...prev, [seasonNumber]: true }));
|
||||
try {
|
||||
const episodes = await getSeasonDetails(
|
||||
data.id.toString(),
|
||||
seasonNumber,
|
||||
);
|
||||
setFetchedSeasons((prev) => ({ ...prev, [seasonNumber]: episodes }));
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch season details:", err);
|
||||
} finally {
|
||||
setLoadingSeasons((prev) => ({ ...prev, [seasonNumber]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
if (data.type === "show") {
|
||||
if (selectedSeason !== -1) {
|
||||
fetchSeason(selectedSeason);
|
||||
} else if (data.seasonData?.seasons) {
|
||||
// Fetch all seasons for favorites
|
||||
data.seasonData.seasons.forEach((season) => {
|
||||
fetchSeason(season.season_number);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [
|
||||
data.id,
|
||||
data.type,
|
||||
selectedSeason,
|
||||
fetchedSeasons,
|
||||
loadingSeasons,
|
||||
data.seasonData,
|
||||
]);
|
||||
|
||||
const allEpisodes = useMemo(() => {
|
||||
return Object.values(fetchedSeasons).flat();
|
||||
}, [fetchedSeasons]);
|
||||
|
||||
// Add effect to measure logo height
|
||||
useEffect(() => {
|
||||
if (logoRef.current) {
|
||||
|
|
@ -403,7 +458,7 @@ export function DetailsContent({ data, minimal = false }: DetailsContentProps) {
|
|||
{/* Episodes Carousel for TV Shows */}
|
||||
{data.type === "show" && data.seasonData && !minimal && (
|
||||
<EpisodeCarousel
|
||||
episodes={data.seasonData.episodes}
|
||||
episodes={allEpisodes}
|
||||
showProgress={showProgress}
|
||||
progress={progress}
|
||||
selectedSeason={selectedSeason}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export function DetailsModal({
|
|||
try {
|
||||
const type =
|
||||
data.type === "movie" ? TMDBContentTypes.MOVIE : TMDBContentTypes.TV;
|
||||
const details = await getMediaDetails(data.id.toString(), type);
|
||||
const details = await getMediaDetails(data.id.toString(), type, false);
|
||||
const backdropUrl = getMediaBackdrop(details.backdrop_path);
|
||||
const logoUrl = await getMediaLogo(data.id.toString(), type);
|
||||
if (type === TMDBContentTypes.MOVIE) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue