LCOV - code coverage report
Current view: top level - app\presentation\pages\details\movie_details_page.dart - movie_details_page.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 163 163 100.0 %
Date: Wed Aug 6 23:43:55 2025 Functions: 0 0 -

          Line data    Source code
       1             : import 'package:flutter/material.dart';
       2             : import 'package:provider/provider.dart';
       3             : import 'package:cached_network_image/cached_network_image.dart';
       4             : 
       5             : import '../../providers/favorites_provider.dart';
       6             : 
       7             : import '../../../domain/entities/models/mdl_the_movie.dart';
       8             : 
       9             : /// Displays detailed information about a specific movie.
      10             : ///
      11             : /// The [MovieDetailsPage] widget shows the movie's poster, title, release date, genres, rating, and other details.
      12             : /// It also allows users to mark the movie as a favorite and view additional information.
      13             : ///
      14             : /// ### Properties
      15             : /// - [movie]: The [TheMovie] instance to display details for.
      16             : ///
      17             : /// ### Methods
      18             : /// - [build]: Builds the widget tree for the movie details page.
      19             : /// - [_buildBackdropImage]: Builds the backdrop image with Hero animation.
      20             : /// - [_buildBackdropPlaceholder]: Builds a placeholder for the backdrop image.
      21             : /// - [_buildPosterImage]: Builds the poster image.
      22             : /// - [_buildPosterPlaceholder]: Builds a placeholder for the poster image.
      23             : /// - [_buildInfoRow]: Builds a row for additional info.
      24             : /// - [_formatReleaseDate]: Formats the release date string.
      25             : /// - [_getGenreName]: Returns the genre name for a given genre ID.
      26             : /// - [_toggleFavorite]: Toggles the favorite status and shows a SnackBar.
      27             : ///
      28             : /// ### Example
      29             : /// ```dart
      30             : /// MovieDetailsPage(movie: myMovie)
      31             : /// ```
      32             : class MovieDetailsPage extends StatelessWidget {
      33             :   /// The movie to display details for.
      34             :   final TheMovie movie;
      35             : 
      36             :   /// Creates a `MovieDetailsPage` instance.
      37             :   ///
      38             :   /// **Parameters:**
      39             :   /// - `movie` (TheMovie): The movie to display details for.
      40           3 :   const MovieDetailsPage({super.key, required this.movie});
      41             : 
      42           2 :   @override
      43             :   /// Builds the widget tree for the movie details page.
      44             :   ///
      45             :   /// **Parameters:**
      46             :   /// - `context` (BuildContext): The build context.
      47             :   ///
      48             :   /// **Returns:**
      49             :   /// - A `Widget` representing the movie details page.
      50             :   Widget build(BuildContext context) {
      51           2 :     return Scaffold(
      52           2 :       body: CustomScrollView(
      53           2 :         slivers: [
      54             :           /// App Bar with backdrop image
      55           2 :           SliverAppBar(
      56             :             expandedHeight: 300,
      57             :             pinned: true,
      58           4 :             flexibleSpace: FlexibleSpaceBar(background: _buildBackdropImage()),
      59           2 :             actions: [
      60           2 :               Consumer<FavoritesProvider>(
      61           2 :                 builder: (context, favoritesProvider, child) {
      62           6 :                   final isFavorite = favoritesProvider.isFavorite(movie.id);
      63           2 :                   return Container(
      64             :                     margin: const EdgeInsets.only(right: 8),
      65           2 :                     decoration: BoxDecoration(
      66           2 :                       color: Colors.black.withValues(alpha: 0.6),
      67             :                       shape: BoxShape.circle,
      68             :                     ),
      69           2 :                     child: IconButton(
      70           2 :                       icon: Icon(
      71             :                         isFavorite ? Icons.favorite : Icons.favorite_border,
      72             :                         color: isFavorite ? Colors.red : Colors.white,
      73             :                       ),
      74           2 :                       onPressed: () => _toggleFavorite(context),
      75             :                     ),
      76             :                   );
      77             :                 },
      78             :               ),
      79             :             ],
      80             :           ),
      81             : 
      82             :           /// Movie details content
      83           2 :           SliverToBoxAdapter(
      84           2 :             child: Padding(
      85             :               padding: const EdgeInsets.all(16.0),
      86           2 :               child: Column(
      87             :                 crossAxisAlignment: CrossAxisAlignment.start,
      88           2 :                 children: [
      89             :                   // Movie poster and basic info
      90           2 :                   Row(
      91             :                     crossAxisAlignment: CrossAxisAlignment.start,
      92           2 :                     children: [
      93             :                       // Poster
      94           2 :                       Container(
      95             :                         width: 120,
      96             :                         height: 180,
      97           2 :                         decoration: BoxDecoration(
      98           2 :                           borderRadius: BorderRadius.circular(12),
      99           2 :                           boxShadow: [
     100           2 :                             BoxShadow(
     101           2 :                               color: Colors.black.withValues(alpha: 0.3),
     102             :                               blurRadius: 8,
     103             :                               offset: const Offset(0, 4),
     104             :                             ),
     105             :                           ],
     106             :                         ),
     107           2 :                         child: ClipRRect(
     108           2 :                           borderRadius: BorderRadius.circular(12),
     109           2 :                           child: _buildPosterImage(),
     110             :                         ),
     111             :                       ),
     112             :                       const SizedBox(width: 16),
     113             : 
     114             :                       // Basic info
     115           2 :                       Expanded(
     116           2 :                         child: Column(
     117             :                           crossAxisAlignment: CrossAxisAlignment.start,
     118           2 :                           children: [
     119             :                             // Title
     120           2 :                             Text(
     121           4 :                               movie.title,
     122           6 :                               style: Theme.of(context).textTheme.headlineMedium
     123           2 :                                   ?.copyWith(fontWeight: FontWeight.bold),
     124             :                             ),
     125             :                             const SizedBox(height: 8),
     126             : 
     127             :                             // Original title (if different)
     128          10 :                             if (movie.originalTitle != movie.title)
     129           2 :                               Text(
     130           4 :                                 movie.originalTitle,
     131           6 :                                 style: Theme.of(context).textTheme.titleMedium
     132           2 :                                     ?.copyWith(
     133           2 :                                       color: Colors.grey[600],
     134             :                                       fontStyle: FontStyle.italic,
     135             :                                     ),
     136             :                               ),
     137           2 :                             const SizedBox(height: 8),
     138             : 
     139             :                             // Release date
     140           6 :                             if (movie.releaseDate.isNotEmpty)
     141           2 :                               Row(
     142           2 :                                 children: [
     143           2 :                                   Icon(
     144             :                                     Icons.calendar_month,
     145             :                                     size: 16,
     146           2 :                                     color: Colors.grey[600],
     147             :                                   ),
     148             :                                   const SizedBox(width: 4),
     149           2 :                                   Text(
     150           6 :                                     _formatReleaseDate(movie.releaseDate),
     151           2 :                                     style: Theme.of(
     152             :                                       context,
     153           4 :                                     ).textTheme.bodyMedium,
     154             :                                   ),
     155             :                                 ],
     156             :                               ),
     157           2 :                             const SizedBox(height: 8),
     158             : 
     159             :                             // Rating
     160           6 :                             if (movie.voteAverage > 0)
     161           2 :                               Row(
     162           2 :                                 children: [
     163             :                                   const Icon(
     164             :                                     Icons.star,
     165             :                                     color: Colors.amber,
     166             :                                     size: 20,
     167             :                                   ),
     168             :                                   const SizedBox(width: 4),
     169           2 :                                   Text(
     170           8 :                                     '${movie.voteAverage.toStringAsFixed(1)}/10',
     171           2 :                                     style: Theme.of(context)
     172           2 :                                         .textTheme
     173           2 :                                         .titleMedium
     174           2 :                                         ?.copyWith(fontWeight: FontWeight.w600),
     175             :                                   ),
     176             :                                   const SizedBox(width: 8),
     177           2 :                                   Text(
     178           6 :                                     '(${movie.voteCount} votos)',
     179           6 :                                     style: Theme.of(context).textTheme.bodySmall
     180           4 :                                         ?.copyWith(color: Colors.grey[600]),
     181             :                                   ),
     182             :                                 ],
     183             :                               ),
     184           2 :                             const SizedBox(height: 8),
     185             : 
     186             :                             // Adult content indicator
     187           4 :                             if (movie.adult)
     188           1 :                               Container(
     189             :                                 padding: const EdgeInsets.symmetric(
     190             :                                   horizontal: 8,
     191             :                                   vertical: 4,
     192             :                                 ),
     193           1 :                                 decoration: BoxDecoration(
     194             :                                   color: Colors.red,
     195           1 :                                   borderRadius: BorderRadius.circular(4),
     196             :                                 ),
     197             :                                 child: const Text(
     198             :                                   '18+',
     199             :                                   style: TextStyle(
     200             :                                     color: Colors.white,
     201             :                                     fontSize: 12,
     202             :                                     fontWeight: FontWeight.bold,
     203             :                                   ),
     204             :                                 ),
     205             :                               ),
     206             :                           ],
     207             :                         ),
     208             :                       ),
     209             :                     ],
     210             :                   ),
     211             :                   const SizedBox(height: 24),
     212             : 
     213             :                   // Overview section
     214           7 :                   if (movie.overview.isNotEmpty) ...[
     215           1 :                     Text(
     216             :                       'Sinopsis',
     217           4 :                       style: Theme.of(context).textTheme.titleLarge?.copyWith(
     218             :                         fontWeight: FontWeight.bold,
     219             :                       ),
     220             :                     ),
     221             :                     const SizedBox(height: 8),
     222           1 :                     Text(
     223           2 :                       movie.overview,
     224           1 :                       style: Theme.of(
     225             :                         context,
     226           3 :                       ).textTheme.bodyLarge?.copyWith(height: 1.5),
     227             :                       textAlign: TextAlign.justify,
     228             :                     ),
     229             :                     const SizedBox(height: 24),
     230             :                   ],
     231             : 
     232             :                   // Genres section
     233           7 :                   if (movie.genreIds.isNotEmpty) ...[
     234           1 :                     Text(
     235             :                       'Géneros',
     236           4 :                       style: Theme.of(context).textTheme.titleLarge?.copyWith(
     237             :                         fontWeight: FontWeight.bold,
     238             :                       ),
     239             :                     ),
     240             :                     const SizedBox(height: 8),
     241           1 :                     Wrap(
     242             :                       spacing: 8,
     243             :                       runSpacing: 8,
     244           4 :                       children: movie.genreIds.map((genreId) {
     245           1 :                         return Chip(
     246           2 :                           label: Text(_getGenreName(genreId)),
     247           1 :                           backgroundColor: Theme.of(
     248             :                             context,
     249           3 :                           ).colorScheme.primary.withValues(alpha: 0.1),
     250             :                         );
     251           1 :                       }).toList(),
     252             :                     ),
     253             :                     const SizedBox(height: 24),
     254             :                   ],
     255             : 
     256             :                   // Additional info section
     257           2 :                   Text(
     258             :                     'Información adicional',
     259           8 :                     style: Theme.of(context).textTheme.titleLarge?.copyWith(
     260             :                       fontWeight: FontWeight.bold,
     261             :                     ),
     262             :                   ),
     263           2 :                   const SizedBox(height: 8),
     264           2 :                   _buildInfoRow(
     265             :                     'Idioma original',
     266           6 :                     movie.originalLanguage.toUpperCase(),
     267             :                   ),
     268           2 :                   _buildInfoRow(
     269             :                     'Popularidad',
     270           6 :                     movie.popularity.toStringAsFixed(1),
     271             :                   ),
     272           4 :                   if (movie.video) _buildInfoRow('Video disponible', 'Sí'),
     273             :                 ],
     274             :               ),
     275             :             ),
     276             :           ),
     277             :         ],
     278             :       ),
     279             :     );
     280             :   }
     281             : 
     282           2 :   Widget _buildBackdropImage() {
     283           6 :     if (movie.backdropPath.isNotEmpty) {
     284           1 :       return Hero(
     285           3 :         tag: 'movie_poster_${movie.id}',
     286             :         flightShuttleBuilder:
     287           1 :             (
     288             :               BuildContext flightContext,
     289             :               Animation<double> animation,
     290             :               HeroFlightDirection flightDirection,
     291             :               BuildContext fromHeroContext,
     292             :               BuildContext toHeroContext,
     293             :             ) {
     294             :               // Animación de escala durante el vuelo
     295             :               final scaleAnimation =
     296           1 :                   Tween<double>(
     297           1 :                     begin: flightDirection == HeroFlightDirection.push
     298             :                         ? 1.0
     299             :                         : 1.2,
     300           1 :                     end: flightDirection == HeroFlightDirection.push
     301             :                         ? 1.2
     302             :                         : 1.0,
     303           1 :                   ).animate(
     304           1 :                     CurvedAnimation(
     305             :                       parent: animation,
     306           1 :                       curve: flightDirection == HeroFlightDirection.push
     307             :                           ? Curves.elasticOut
     308             :                           : Curves.easeInOut,
     309             :                     ),
     310             :                   );
     311             : 
     312           1 :               return AnimatedBuilder(
     313             :                 animation: scaleAnimation,
     314           1 :                 builder: (context, child) {
     315           1 :                   return Transform.scale(
     316           1 :                     scale: scaleAnimation.value,
     317           1 :                     child: Material(
     318             :                       color: Colors.transparent,
     319           1 :                       child: CachedNetworkImage(
     320             :                         imageUrl:
     321           3 :                             'https://image.tmdb.org/t/p/w1280${movie.backdropPath}',
     322             :                         fit: BoxFit.cover,
     323           1 :                         placeholder: (context, url) =>
     324           1 :                             _buildBackdropPlaceholder(),
     325             :                       ),
     326             :                     ),
     327             :                   );
     328             :                 },
     329             :               );
     330             :             },
     331           1 :         child: CachedNetworkImage(
     332           3 :           imageUrl: 'https://image.tmdb.org/t/p/w1280${movie.backdropPath}',
     333             :           fit: BoxFit.cover,
     334           2 :           placeholder: (context, url) => _buildBackdropPlaceholder(),
     335           2 :           errorWidget: (context, url, error) => _buildBackdropPlaceholder(),
     336             :         ),
     337             :       );
     338             :     } else {
     339           2 :       return Hero(
     340           6 :         tag: 'movie_poster_${movie.id}',
     341           2 :         child: Material(
     342             :           color: Colors.transparent,
     343           2 :           child: _buildBackdropPlaceholder(),
     344             :         ),
     345             :       );
     346             :     }
     347             :   }
     348             : 
     349           2 :   Widget _buildBackdropPlaceholder() {
     350           2 :     return Container(
     351           2 :       color: Colors.grey[300],
     352             :       child: const Center(
     353             :         child: Icon(Icons.movie, size: 80, color: Colors.grey),
     354             :       ),
     355             :     );
     356             :   }
     357             : 
     358           2 :   Widget _buildPosterImage() {
     359           6 :     if (movie.posterPath.isNotEmpty) {
     360           1 :       return CachedNetworkImage(
     361           3 :         imageUrl: 'https://image.tmdb.org/t/p/w500${movie.posterPath}',
     362             :         fit: BoxFit.cover,
     363           2 :         placeholder: (context, url) => _buildPosterPlaceholder(),
     364           2 :         errorWidget: (context, url, error) => _buildPosterPlaceholder(),
     365             :       );
     366             :     } else {
     367           2 :       return _buildPosterPlaceholder();
     368             :     }
     369             :   }
     370             : 
     371           2 :   Widget _buildPosterPlaceholder() {
     372           2 :     return Container(
     373           2 :       color: Colors.grey[300],
     374             :       child: const Center(
     375             :         child: Icon(Icons.movie, size: 40, color: Colors.grey),
     376             :       ),
     377             :     );
     378             :   }
     379             : 
     380           2 :   Widget _buildInfoRow(String label, String value) {
     381           2 :     return Padding(
     382             :       padding: const EdgeInsets.symmetric(vertical: 4),
     383           2 :       child: Row(
     384             :         crossAxisAlignment: CrossAxisAlignment.start,
     385           2 :         children: [
     386           2 :           SizedBox(
     387             :             width: 120,
     388           2 :             child: Text(
     389           2 :               '$label:',
     390             :               style: const TextStyle(fontWeight: FontWeight.w500),
     391             :             ),
     392             :           ),
     393           4 :           Expanded(child: Text(value)),
     394             :         ],
     395             :       ),
     396             :     );
     397             :   }
     398             : 
     399           2 :   String _formatReleaseDate(String releaseDate) {
     400             :     try {
     401           2 :       final date = DateTime.parse(releaseDate);
     402           2 :       final months = [
     403             :         'enero',
     404             :         'febrero',
     405             :         'marzo',
     406             :         'abril',
     407             :         'mayo',
     408             :         'junio',
     409             :         'julio',
     410             :         'agosto',
     411             :         'septiembre',
     412             :         'octubre',
     413             :         'noviembre',
     414             :         'diciembre',
     415             :       ];
     416          12 :       return '${date.day} de ${months[date.month - 1]} de ${date.year}';
     417             :     } catch (e) {
     418             :       return releaseDate;
     419             :     }
     420             :   }
     421             : 
     422           1 :   String _getGenreName(int genreId) {
     423             :     // TMDB genre mapping for movies
     424             :     const genreMap = {
     425             :       28: 'Acción',
     426             :       12: 'Aventura',
     427             :       16: 'Animación',
     428             :       35: 'Comedia',
     429             :       80: 'Crimen',
     430             :       99: 'Documental',
     431             :       18: 'Drama',
     432             :       10751: 'Familiar',
     433             :       14: 'Fantasía',
     434             :       36: 'Historia',
     435             :       27: 'Terror',
     436             :       10402: 'Música',
     437             :       9648: 'Misterio',
     438             :       10749: 'Romance',
     439             :       878: 'Ciencia ficción',
     440             :       10770: 'TV Movie',
     441             :       53: 'Thriller',
     442             :       10752: 'Guerra',
     443             :       37: 'Western',
     444             :     };
     445             : 
     446           1 :     return genreMap[genreId] ?? 'Desconocido';
     447             :   }
     448             : 
     449           1 :   void _toggleFavorite(BuildContext context) {
     450             :     // Check state BEFORE toggling
     451           2 :     final wasAlreadyFavorite = context.read<FavoritesProvider>().isFavorite(
     452           2 :       movie.id,
     453             :     );
     454             : 
     455           3 :     context.read<FavoritesProvider>().toggleFavorite(movie);
     456             : 
     457             :     // Show snackbar feedback with correct message
     458           2 :     ScaffoldMessenger.of(context).showSnackBar(
     459           1 :       SnackBar(
     460           1 :         content: Text(
     461             :           wasAlreadyFavorite
     462           2 :               ? '${movie.title} eliminado de favoritos'
     463           4 :               : '${movie.title} agregado a favoritos',
     464             :         ),
     465             :         duration: const Duration(seconds: 2),
     466             :         // action: SnackBarAction(
     467             :         //   label: 'Deshacer',
     468             :         //   onPressed: () {
     469             :         //     context.read<FavoritesProvider>().toggleFavorite(movie);
     470             :         //   },
     471             :         // ),
     472             :       ),
     473             :     );
     474             :   }
     475             : }

Generated by: LCOV version 1.15.alpha0w