pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/

Merge remote-tracking branch 'upstream/show-split-pause-overlay-on-scraping-part'

dunkirk.sh f5f621ef d54190d0

verified
+184 -52
+184 -52
src/pages/parts/player/ScrapingPart.tsx
··· 9 9 scrapePartsToProviderMetric, 10 10 useReportProviders, 11 11 } from "@/backend/helpers/report"; 12 + import { getMediaDetails, getMediaLogo } from "@/backend/metadata/tmdb"; 13 + import { TMDBContentTypes } from "@/backend/metadata/types/tmdb"; 12 14 import { Button } from "@/components/buttons/Button"; 13 15 import { Loading } from "@/components/layout/Loading"; 14 16 import { 15 17 ScrapeCard, 16 18 ScrapeItem, 17 19 } from "@/components/player/internals/ScrapeCard"; 20 + import { useIsMobile } from "@/hooks/useIsMobile"; 18 21 import { 19 22 ScrapingItems, 20 23 ScrapingSegment, ··· 23 26 } from "@/hooks/useProviderScrape"; 24 27 import { playerStatus } from "@/stores/player/slices/source"; 25 28 import { usePlayerStore } from "@/stores/player/store"; 29 + import { usePreferencesStore } from "@/stores/preferences"; 30 + 31 + interface ScrapingMediaDetails { 32 + voteAverage: number | null; 33 + genres: string[]; 34 + } 26 35 27 36 export interface ScrapingProps { 28 37 media: ScrapeMedia; ··· 43 52 const setStatus = usePlayerStore((s) => s.setStatus); 44 53 const addFailedSource = usePlayerStore((s) => s.addFailedSource); 45 54 const sourceId = usePlayerStore((s) => s.sourceId); 55 + const meta = usePlayerStore((s) => s.meta); 56 + const enablePauseOverlay = usePreferencesStore((s) => s.enablePauseOverlay); 57 + const enableImageLogos = usePreferencesStore((s) => s.enableImageLogos); 58 + const { isMobile } = useIsMobile(); 59 + 60 + const showMediaColumn = enablePauseOverlay && !isMobile && !!meta; 61 + const [logoUrl, setLogoUrl] = useState<string | null>(null); 62 + const [details, setDetails] = useState<ScrapingMediaDetails>({ 63 + voteAverage: null, 64 + genres: [], 65 + }); 66 + 67 + useEffect(() => { 68 + if (!showMediaColumn || !meta?.tmdbId) return; 69 + let mounted = true; 70 + const fetchLogo = async () => { 71 + if (!enableImageLogos) { 72 + setLogoUrl(null); 73 + return; 74 + } 75 + try { 76 + const type = 77 + meta.type === "movie" ? TMDBContentTypes.MOVIE : TMDBContentTypes.TV; 78 + const url = await getMediaLogo(meta.tmdbId, type); 79 + if (mounted) setLogoUrl(url || null); 80 + } catch { 81 + if (mounted) setLogoUrl(null); 82 + } 83 + }; 84 + fetchLogo(); 85 + return () => { 86 + mounted = false; 87 + }; 88 + }, [showMediaColumn, meta?.tmdbId, meta?.type, enableImageLogos]); 89 + 90 + useEffect(() => { 91 + if (!showMediaColumn || !meta?.tmdbId) return; 92 + let mounted = true; 93 + const fetchDetails = async () => { 94 + try { 95 + const type = 96 + meta.type === "movie" ? TMDBContentTypes.MOVIE : TMDBContentTypes.TV; 97 + const data = await getMediaDetails(meta.tmdbId, type, false); 98 + if (mounted && data) { 99 + const voteAverage = 100 + typeof data.vote_average === "number" ? data.vote_average : null; 101 + const genres = (data.genres ?? []).map( 102 + (g: { name: string }) => g.name, 103 + ); 104 + setDetails({ voteAverage, genres }); 105 + } 106 + } catch { 107 + if (mounted) setDetails({ voteAverage: null, genres: [] }); 108 + } 109 + }; 110 + fetchDetails(); 111 + return () => { 112 + mounted = false; 113 + }; 114 + }, [showMediaColumn, meta?.tmdbId, meta?.type]); 46 115 47 116 const containerRef = useRef<HTMLDivElement | null>(null); 48 117 const listRef = useRef<HTMLDivElement | null>(null); ··· 125 194 if (currentProviderIndex === -1) 126 195 currentProviderIndex = sourceOrder.length - 1; 127 196 197 + const overview = 198 + meta && (meta.type === "show" ? meta.episode?.overview : meta.overview); 199 + const hasMediaDetails = 200 + details.voteAverage !== null || details.genres.length > 0; 201 + const hasMediaContent = 202 + showMediaColumn && 203 + meta && 204 + (overview || logoUrl || meta.title || hasMediaDetails); 205 + 128 206 return ( 129 207 <div 130 - className="h-full w-full relative dir-neutral:origin-top-left flex" 208 + className={classNames( 209 + "h-full w-full relative dir-neutral:origin-top-left flex", 210 + showMediaColumn && "gap-8 lg:gap-12", 211 + )} 131 212 ref={containerRef} 132 213 > 133 - {!sourceOrder || sourceOrder.length === 0 ? ( 134 - <div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-center flex flex-col justify-center z-0"> 135 - <Loading className="mb-8" /> 136 - <p>{t("player.scraping.items.pending")}</p> 214 + {showMediaColumn && hasMediaContent && ( 215 + <div className="flex-shrink-0 w-80 max-w-[min(20rem,40%)] flex items-center py-6"> 216 + <div className="max-w-sm"> 217 + {logoUrl ? ( 218 + <img 219 + src={logoUrl} 220 + alt={meta.title} 221 + className="mb-6 max-h-32 object-contain drop-shadow-lg" 222 + /> 223 + ) : ( 224 + <h1 className="mb-4 text-4xl font-bold text-white drop-shadow-lg"> 225 + {meta.title} 226 + </h1> 227 + )} 228 + 229 + {meta.type === "show" && meta.episode && ( 230 + <h2 className="mb-2 text-2xl font-semibold text-white/90 drop-shadow-md"> 231 + {meta.episode.title} 232 + </h2> 233 + )} 234 + 235 + {hasMediaDetails && ( 236 + <div className="mb-3 flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-white/80 drop-shadow-md"> 237 + {details.voteAverage !== null && ( 238 + <span> 239 + {details.voteAverage.toFixed(1)} 240 + <span className="text-white/60 ml-0.5">/10</span> 241 + </span> 242 + )} 243 + {details.genres.length > 0 && ( 244 + <> 245 + {details.voteAverage !== null && ( 246 + <span className="text-white/60">•</span> 247 + )} 248 + <span>{details.genres.slice(0, 4).join(", ")}</span> 249 + </> 250 + )} 251 + </div> 252 + )} 253 + 254 + {overview && ( 255 + <p className="text-lg text-white/80 drop-shadow-md line-clamp-6"> 256 + {overview} 257 + </p> 258 + )} 259 + </div> 137 260 </div> 138 - ) : null} 139 - <div 140 - className={classNames({ 141 - "absolute transition-[transform,opacity] opacity-0 dir-neutral:left-0": true, 142 - "!opacity-100": renderedOnce, 143 - })} 144 - ref={listRef} 145 - > 146 - {sourceOrder.map((order) => { 147 - const source = sources[order.id]; 148 - const distance = Math.abs( 149 - sourceOrder.findIndex((o) => o.id === order.id) - 150 - currentProviderIndex, 151 - ); 152 - return ( 153 - <div 154 - className="transition-opacity duration-100" 155 - style={{ opacity: Math.max(0, 1 - distance * 0.3) }} 156 - key={order.id} 157 - > 158 - <ScrapeCard 159 - id={order.id} 160 - name={source.name} 161 - status={source.status} 162 - hasChildren={order.children.length > 0} 163 - percentage={source.percentage} 261 + )} 262 + 263 + <div className="flex-1 min-w-0 relative flex"> 264 + {!sourceOrder || sourceOrder.length === 0 ? ( 265 + <div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-center flex flex-col justify-center z-0"> 266 + <Loading className="mb-8" /> 267 + <p>{t("player.scraping.items.pending")}</p> 268 + </div> 269 + ) : null} 270 + <div 271 + className={classNames({ 272 + "absolute transition-[transform,opacity] opacity-0 dir-neutral:left-0": true, 273 + "!opacity-100": renderedOnce, 274 + })} 275 + ref={listRef} 276 + > 277 + {sourceOrder.map((order) => { 278 + const source = sources[order.id]; 279 + const distance = Math.abs( 280 + sourceOrder.findIndex((o) => o.id === order.id) - 281 + currentProviderIndex, 282 + ); 283 + return ( 284 + <div 285 + className="transition-opacity duration-100" 286 + style={{ opacity: Math.max(0, 1 - distance * 0.3) }} 287 + key={order.id} 164 288 > 165 - <div 166 - className={classNames({ 167 - "space-y-6 mt-8": order.children.length > 0, 168 - })} 289 + <ScrapeCard 290 + id={order.id} 291 + name={source.name} 292 + status={source.status} 293 + hasChildren={order.children.length > 0} 294 + percentage={source.percentage} 169 295 > 170 - {order.children.map((embedId) => { 171 - const embed = sources[embedId]; 172 - return ( 173 - <ScrapeItem 174 - id={embedId} 175 - name={embed.name} 176 - status={embed.status} 177 - percentage={embed.percentage} 178 - key={embedId} 179 - /> 180 - ); 181 - })} 182 - </div> 183 - </ScrapeCard> 184 - </div> 185 - ); 186 - })} 296 + <div 297 + className={classNames({ 298 + "space-y-6 mt-8": order.children.length > 0, 299 + })} 300 + > 301 + {order.children.map((embedId) => { 302 + const embed = sources[embedId]; 303 + return ( 304 + <ScrapeItem 305 + id={embedId} 306 + name={embed.name} 307 + status={embed.status} 308 + percentage={embed.percentage} 309 + key={embedId} 310 + /> 311 + ); 312 + })} 313 + </div> 314 + </ScrapeCard> 315 + </div> 316 + ); 317 + })} 318 + </div> 187 319 </div> 188 320 </div> 189 321 );