LCOV - code coverage report
Current view: top level - app\presentation\widgets\movie_card.dart - movie_card.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 81 81 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             : import '../pages/details/movie_details_page.dart';
       7             : 
       8             : import '../../domain/entities/models/mdl_the_movie.dart';
       9             : 
      10             : /// Displays a card for a single movie, including poster, title, release date, rating, and favorite button.
      11             : ///
      12             : /// The `MovieCard` widget presents movie information in a visually rich card format.
      13             : /// It supports navigation to the details page, toggling favorite status, and provides feedback via SnackBar.
      14             : ///
      15             : /// ### Parameters
      16             : /// - [movie]: The movie to display (required).
      17             : /// - [onTap]: Optional callback for custom tap behavior. Defaults to navigation to details page.
      18             : ///
      19             : /// ### Visual States
      20             : /// - Shows poster image or a placeholder if not available.
      21             : /// - Displays a favorite button, reflecting current favorite status.
      22             : /// - Shows movie rating as a badge if available.
      23             : /// - Presents title and release year.
      24             : ///
      25             : /// ### Interactions
      26             : /// - Tapping the card navigates to the movie details page (unless [onTap] is provided).
      27             : /// - Tapping the favorite button toggles favorite status and shows a SnackBar.
      28             : ///
      29             : /// ### Usage
      30             : /// Use this widget in grids or lists to display movie items with interactive features.
      31             : class MovieCard extends StatelessWidget {
      32             :   final TheMovie movie;
      33             :   final VoidCallback? onTap;
      34             : 
      35           3 :   const MovieCard({super.key, required this.movie, this.onTap});
      36             : 
      37           3 :   @override
      38             :   Widget build(BuildContext context) {
      39           3 :     return Card(
      40             :       elevation: 4,
      41           6 :       shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      42           3 :       child: InkWell(
      43           3 :         borderRadius: BorderRadius.circular(12),
      44           5 :         onTap: onTap ?? () => _navigateToDetails(context),
      45           3 :         child: Column(
      46             :           crossAxisAlignment: CrossAxisAlignment.start,
      47           3 :           children: [
      48             :             // Movie Poster
      49           3 :             Expanded(
      50             :               flex: 3,
      51           3 :               child: Stack(
      52           3 :                 children: [
      53             :                   // Poster Image
      54           3 :                   Container(
      55             :                     width: double.infinity,
      56             :                     decoration: const BoxDecoration(
      57             :                       borderRadius: BorderRadius.vertical(
      58             :                         top: Radius.circular(12),
      59             :                       ),
      60             :                     ),
      61           3 :                     child: ClipRRect(
      62             :                       borderRadius: const BorderRadius.vertical(
      63             :                         top: Radius.circular(12),
      64             :                       ),
      65           3 :                       child: Hero(
      66           9 :                         tag: 'movie_poster_${movie.id}',
      67           3 :                         child: _buildPosterImage(),
      68             :                       ),
      69             :                     ),
      70             :                   ),
      71             : 
      72             :                   // Favorite Button
      73           3 :                   Positioned(
      74             :                     top: 8,
      75             :                     right: 8,
      76           3 :                     child: Consumer<FavoritesProvider>(
      77           3 :                       builder: (context, favoritesProvider, child) {
      78           3 :                         final isFavorite = favoritesProvider.isFavorite(
      79           6 :                           movie.id,
      80             :                         );
      81           3 :                         return Container(
      82           3 :                           decoration: BoxDecoration(
      83           3 :                             color: Colors.black.withValues(alpha: 0.6),
      84             :                             shape: BoxShape.circle,
      85             :                           ),
      86           3 :                           child: IconButton(
      87           3 :                             icon: Icon(
      88             :                               isFavorite
      89             :                                   ? Icons.favorite
      90             :                                   : Icons.favorite_border,
      91             :                               color: isFavorite ? Colors.red : Colors.white,
      92             :                               size: 20,
      93             :                             ),
      94           2 :                             onPressed: () => _toggleFavorite(context),
      95             :                             padding: const EdgeInsets.all(4),
      96             :                             constraints: const BoxConstraints(
      97             :                               minWidth: 32,
      98             :                               minHeight: 32,
      99             :                             ),
     100             :                           ),
     101             :                         );
     102             :                       },
     103             :                     ),
     104             :                   ),
     105             : 
     106             :                   // Rating Badge
     107           9 :                   if (movie.voteAverage > 0)
     108           1 :                     Positioned(
     109             :                       top: 8,
     110             :                       left: 8,
     111           1 :                       child: Container(
     112             :                         padding: const EdgeInsets.symmetric(
     113             :                           horizontal: 6,
     114             :                           vertical: 2,
     115             :                         ),
     116           1 :                         decoration: BoxDecoration(
     117           1 :                           color: Colors.black.withValues(alpha: 0.7),
     118           1 :                           borderRadius: BorderRadius.circular(8),
     119             :                         ),
     120           1 :                         child: Row(
     121             :                           mainAxisSize: MainAxisSize.min,
     122           1 :                           children: [
     123             :                             const Icon(
     124             :                               Icons.star,
     125             :                               color: Colors.amber,
     126             :                               size: 12,
     127             :                             ),
     128             :                             const SizedBox(width: 2),
     129           1 :                             Text(
     130           3 :                               movie.voteAverage.toStringAsFixed(1),
     131             :                               style: const TextStyle(
     132             :                                 color: Colors.white,
     133             :                                 fontSize: 10,
     134             :                                 fontWeight: FontWeight.bold,
     135             :                               ),
     136             :                             ),
     137             :                           ],
     138             :                         ),
     139             :                       ),
     140             :                     ),
     141             :                 ],
     142             :               ),
     143             :             ),
     144             : 
     145             :             // Movie Info
     146           3 :             Expanded(
     147             :               flex: 1,
     148           3 :               child: Padding(
     149             :                 padding: const EdgeInsets.all(8.0),
     150           3 :                 child: Column(
     151             :                   crossAxisAlignment: CrossAxisAlignment.start,
     152           3 :                   children: [
     153             :                     // Title - Flexible para adaptarse al espacio
     154           3 :                     Flexible(
     155           3 :                       child: Text(
     156           6 :                         movie.title,
     157          12 :                         style: Theme.of(context).textTheme.titleSmall?.copyWith(
     158             :                           fontWeight: FontWeight.w600,
     159             :                         ),
     160             :                         maxLines: 2,
     161             :                         overflow: TextOverflow.ellipsis,
     162             :                       ),
     163             :                     ),
     164             : 
     165             :                     // Spacing between title and date
     166             :                     const SizedBox(height: 4),
     167             : 
     168             :                     // Release Date - Siempre visible en la parte inferior
     169           9 :                     if (movie.releaseDate.isNotEmpty)
     170           1 :                       Text(
     171           3 :                         _formatReleaseDate(movie.releaseDate),
     172           4 :                         style: Theme.of(context).textTheme.bodySmall?.copyWith(
     173           1 :                           color: Colors.grey[600],
     174             :                         ),
     175             :                         maxLines: 1,
     176             :                         overflow: TextOverflow.ellipsis,
     177             :                       ),
     178             :                   ],
     179             :                 ),
     180             :               ),
     181             :             ),
     182             :           ],
     183             :         ),
     184             :       ),
     185             :     );
     186             :   }
     187             : 
     188           3 :   Widget _buildPosterImage() {
     189           9 :     if (movie.posterPath.isNotEmpty) {
     190           1 :       return CachedNetworkImage(
     191           3 :         imageUrl: 'https://image.tmdb.org/t/p/w500${movie.posterPath}',
     192             :         fit: BoxFit.cover,
     193           2 :         placeholder: (context, url) => _buildPlaceholder(),
     194           2 :         errorWidget: (context, url, error) => _buildPlaceholder(),
     195             :       );
     196             :     } else {
     197           3 :       return _buildPlaceholder();
     198             :     }
     199             :   }
     200             : 
     201           3 :   Widget _buildPlaceholder() {
     202           3 :     return Container(
     203             :       width: double.infinity,
     204           3 :       color: Colors.grey[300],
     205             :       child: const Column(
     206             :         mainAxisAlignment: MainAxisAlignment.center,
     207             :         children: [
     208             :           Icon(Icons.movie, size: 40, color: Colors.grey),
     209             :           SizedBox(height: 4),
     210             :           Text(
     211             :             'Sin imagen',
     212             :             style: TextStyle(color: Colors.grey, fontSize: 12),
     213             :           ),
     214             :         ],
     215             :       ),
     216             :     );
     217             :   }
     218             : 
     219           1 :   String _formatReleaseDate(String releaseDate) {
     220             :     try {
     221           1 :       final date = DateTime.parse(releaseDate);
     222           2 :       return '${date.year}';
     223             :     } catch (e) {
     224             :       return releaseDate;
     225             :     }
     226             :   }
     227             : 
     228           1 :   void _navigateToDetails(BuildContext context) {
     229           1 :     Navigator.push(
     230             :       context,
     231           1 :       PageRouteBuilder(
     232           1 :         pageBuilder: (context, animation, secondaryAnimation) =>
     233           2 :             MovieDetailsPage(movie: movie),
     234             :         transitionDuration: const Duration(milliseconds: 800),
     235             :         reverseTransitionDuration: const Duration(milliseconds: 600),
     236           1 :         transitionsBuilder: (context, animation, secondaryAnimation, child) {
     237             :           // Curva elástica para efecto de rebote
     238           1 :           final curvedAnimation = CurvedAnimation(
     239             :             parent: animation,
     240             :             curve: Curves.elasticOut,
     241             :             reverseCurve: Curves.easeInOut,
     242             :           );
     243             : 
     244           1 :           return FadeTransition(opacity: curvedAnimation, child: child);
     245             :         },
     246             :       ),
     247             :     );
     248             :   }
     249             : 
     250           1 :   void _toggleFavorite(BuildContext context) {
     251             :     // Check state BEFORE toggling
     252           2 :     final wasAlreadyFavorite = context.read<FavoritesProvider>().isFavorite(
     253           2 :       movie.id,
     254             :     );
     255             : 
     256           3 :     context.read<FavoritesProvider>().toggleFavorite(movie);
     257             : 
     258             :     // Show snackbar feedback with correct message
     259           2 :     ScaffoldMessenger.of(context).showSnackBar(
     260           1 :       SnackBar(
     261           1 :         content: Text(
     262             :           wasAlreadyFavorite
     263           2 :               ? '${movie.title} eliminado de favoritos'
     264           4 :               : '${movie.title} agregado a favoritos',
     265             :         ),
     266             :         duration: const Duration(seconds: 2),
     267             :       ),
     268             :     );
     269             :   }
     270             : }

Generated by: LCOV version 1.15.alpha0w