import 'dart:async'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import '../../../core/contracts/library.dart'; import '../../../shared/constants/breakpoints.dart'; import '../../../shared/mappers/media_image_url.dart'; import '../../../shared/widgets/banner_carousel_mixin.dart'; import '../../../shared/widgets/banner_carousel_scaffold.dart'; class HomeBannerCarousel extends StatefulWidget { final String baseUrl; final String token; final List items; final Map? imageHeaders; final ValueChanged onItemTap; final ValueChanged? onActiveItemChanged; final bool active; const HomeBannerCarousel({ super.key, required this.baseUrl, required this.token, required this.items, required this.onItemTap, this.imageHeaders, this.onActiveItemChanged, this.active = true, }); @override State createState() => _HomeBannerCarouselState(); } class _HomeBannerCarouselState extends State with WidgetsBindingObserver, BannerCarouselMixin { @override int get itemCount => widget.items.length; @override bool get isActive => widget.active; @override void emitActiveItem() { if (!mounted || !widget.active) return; if (widget.items.isEmpty) return; final safeIndex = currentIndex.clamp(0, widget.items.length - 1); widget.onActiveItemChanged?.call(widget.items[safeIndex]); } @override void precacheAdjacentSlides() { if (!mounted || !widget.active || widget.items.length <= 1) return; final next = (currentIndex + 1) % widget.items.length; final nextLogo = _logoUrl(widget.items[next]); final cacheWidth = bannerCacheWidth(); void precacheUrl(String url, {int? cacheWidth}) { unawaited( precacheImage( ResizeImage.resizeIfNeeded( cacheWidth, null, CachedNetworkImageProvider(url, headers: widget.imageHeaders), ), context, onError: (_, _) {}, ), ); } precacheUrl( EmbyImageUrl.detailBanner( baseUrl: widget.baseUrl, token: widget.token, item: widget.items[next], ), cacheWidth: cacheWidth, ); if (nextLogo != null) precacheUrl(nextLogo, cacheWidth: 960); } @override void didUpdateWidget(covariant HomeBannerCarousel oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.items != widget.items) { resetCarouselItems(); } if (oldWidget.active != widget.active) { widget.active ? activateCarousel() : deactivateCarousel(); } } BannerSlideData _slideDataFor(EmbyRawItem item) { final fallbackItemId = (item.SeriesId?.isNotEmpty ?? false) ? item.SeriesId! : item.Id; final genres = (item.extra['Genres'] as List?)?.cast().take(2).toList() ?? const []; return BannerSlideData( imageUrl: EmbyImageUrl.detailBanner( baseUrl: widget.baseUrl, token: widget.token, item: item, ), fallbackImageUrl: EmbyImageUrl.fanart( baseUrl: widget.baseUrl, token: widget.token, itemId: fallbackItemId, ), imageHeaders: widget.imageHeaders, logoUrl: _logoUrl(item), title: item.Name, overview: item.Overview, rating: item.CommunityRating?.toStringAsFixed(1), primaryMetaLabels: [ if (item.ProductionYear != null) item.ProductionYear!.toString(), ], secondaryMetaLabels: genres, ); } String? _logoUrl(EmbyRawItem item) { final logoItemId = (item.SeriesId?.isNotEmpty ?? false) ? item.SeriesId! : item.Id; return EmbyImageUrl.logo( baseUrl: widget.baseUrl, token: widget.token, itemId: logoItemId, tag: logoItemId == item.Id ? (item.ImageTags?['Logo']) : null, ); } @override Widget build(BuildContext context) { return BannerCarouselScaffold( controller: this, reserveLogoArea: true, slides: [for (final item in widget.items) _slideDataFor(item)], onSlideTap: (index) => widget.onItemTap(widget.items[index]), ); } } class HomeBannerSkeleton extends StatelessWidget { const HomeBannerSkeleton({super.key}); @override Widget build(BuildContext context) { final compact = Breakpoints.isCompact(MediaQuery.sizeOf(context).width); if (compact) { final height = MediaQuery.sizeOf(context).height * 0.55; return SizedBox(height: height); } return Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: LayoutBuilder( builder: (context, constraints) { final height = (constraints.maxWidth * 7 / 16).clamp(260.0, 500.0); return SizedBox(height: height); }, ), ); } }