Files
neomovies-mobile/lib/presentation/screens/movie_detail/movie_detail_screen.dart
2025-07-13 14:01:29 +03:00

357 lines
15 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:neomovies_mobile/presentation/providers/auth_provider.dart';
import 'package:neomovies_mobile/presentation/providers/favorites_provider.dart';
import 'package:neomovies_mobile/presentation/providers/reactions_provider.dart';
import 'package:neomovies_mobile/presentation/providers/movie_detail_provider.dart';
import 'package:neomovies_mobile/presentation/screens/player/video_player_screen.dart';
import 'package:provider/provider.dart';
class MovieDetailScreen extends StatefulWidget {
final String movieId;
final String mediaType;
const MovieDetailScreen({super.key, required this.movieId, this.mediaType = 'movie'});
@override
State<MovieDetailScreen> createState() => _MovieDetailScreenState();
}
class _MovieDetailScreenState extends State<MovieDetailScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
// Load movie details and reactions
Provider.of<MovieDetailProvider>(context, listen: false).loadMedia(int.parse(widget.movieId), widget.mediaType);
Provider.of<ReactionsProvider>(context, listen: false).loadReactionsForMedia(widget.mediaType, widget.movieId);
});
}
void _openPlayer(BuildContext context, String? imdbId, String title) {
if (imdbId == null || imdbId.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('IMDB ID not found. Cannot open player.'),
duration: Duration(seconds: 3),
),
);
return;
}
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => VideoPlayerScreen(
mediaId: imdbId,
mediaType: widget.mediaType,
title: title,
),
),
);
}
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
),
body: Consumer<MovieDetailProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (provider.error != null) {
return Center(child: Text('Error: ${provider.error}'));
}
if (provider.movie == null) {
return const Center(child: Text('Movie not found'));
}
final movie = provider.movie!;
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Poster
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 300),
child: AspectRatio(
aspectRatio: 2 / 3,
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: CachedNetworkImage(
imageUrl: movie.fullPosterUrl,
fit: BoxFit.cover,
placeholder: (context, url) => const Center(child: CircularProgressIndicator()),
errorWidget: (context, url, error) => const Icon(Icons.error),
),
),
),
),
),
const SizedBox(height: 24),
// Title
Text(
movie.title,
style: textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
// Tagline
if (movie.tagline != null && movie.tagline!.isNotEmpty)
Text(
movie.tagline!,
style: textTheme.titleMedium?.copyWith(color: textTheme.bodySmall?.color),
),
const SizedBox(height: 16),
// Meta Info
Wrap(
spacing: 8.0,
runSpacing: 4.0,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text('Рейтинг: ${movie.voteAverage?.toStringAsFixed(1) ?? 'N/A'}'),
const Text('|'),
if (movie.mediaType == 'tv')
Text('${movie.seasonsCount ?? '-'} сез., ${movie.episodesCount ?? '-'} сер.')
else if (movie.runtime != null)
Text('${movie.runtime} мин.'),
const Text('|'),
if (movie.releaseDate != null)
Text(DateFormat('d MMMM yyyy', 'ru').format(movie.releaseDate!)),
],
),
const SizedBox(height: 16),
// Genres
if (movie.genres != null && movie.genres!.isNotEmpty)
Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: movie.genres!
.map((genre) => Chip(
label: Text(genre),
backgroundColor: colorScheme.secondaryContainer,
labelStyle: textTheme.bodySmall?.copyWith(color: colorScheme.onSecondaryContainer),
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 2.0),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
))
.toList(),
),
const SizedBox(height: 24),
// Reactions Section
_buildReactionsSection(context),
const SizedBox(height: 24),
// Overview
Text(
'Описание',
style: textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
movie.overview ?? 'Описание недоступно.',
style: textTheme.bodyMedium,
),
const SizedBox(height: 24),
// Action Buttons
Row(
children: [
Expanded(
child: Consumer<MovieDetailProvider>(
builder: (context, provider, child) {
final imdbId = provider.imdbId;
final isImdbLoading = provider.isImdbLoading;
return ElevatedButton.icon(
onPressed: (isImdbLoading || imdbId == null)
? null // Делаем кнопку неактивной во время загрузки или если нет ID
: () {
_openPlayer(context, imdbId, provider.movie!.title);
},
icon: isImdbLoading
? Container(
width: 24,
height: 24,
padding: const EdgeInsets.all(2.0),
child: const CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3,
),
)
: const Icon(Icons.play_arrow),
label: const Text('Смотреть'),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
).copyWith(
// Устанавливаем цвет для неактивного состояния
backgroundColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.grey;
}
return Theme.of(context).colorScheme.primary;
},
),
),
);
},
),
),
const SizedBox(width: 16),
Consumer<FavoritesProvider>(
builder: (context, favoritesProvider, child) {
final isFavorite = favoritesProvider.isFavorite(widget.movieId);
return IconButton(
onPressed: () {
final authProvider = context.read<AuthProvider>();
if (!authProvider.isAuthenticated) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Войдите в аккаунт, чтобы добавлять в избранное.'),
duration: Duration(seconds: 2),
),
);
return;
}
if (isFavorite) {
favoritesProvider.removeFavorite(widget.movieId);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Удалено из избранного'),
duration: Duration(seconds: 2),
),
);
} else {
favoritesProvider.addFavorite(movie);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Добавлено в избранное'),
duration: Duration(seconds: 2),
),
);
}
},
icon: Icon(isFavorite ? Icons.favorite : Icons.favorite_border),
iconSize: 28,
style: IconButton.styleFrom(
backgroundColor: isFavorite ? Colors.red.withOpacity(0.1) : colorScheme.secondaryContainer,
foregroundColor: isFavorite ? Colors.red : colorScheme.onSecondaryContainer,
),
);
},
),
],
),
],
),
);
},
),
);
}
Widget _buildReactionsSection(BuildContext context) {
final authProvider = context.watch<AuthProvider>();
// Define the reactions with their icons and backend types
// Map of UI reaction types to backend types and icons
final List<Map<String, dynamic>> reactions = [
{'uiType': 'like', 'backendType': 'fire', 'icon': Icons.local_fire_department},
{'uiType': 'nice', 'backendType': 'nice', 'icon': Icons.thumb_up_alt},
{'uiType': 'think', 'backendType': 'think', 'icon': Icons.psychology},
{'uiType': 'bore', 'backendType': 'bore', 'icon': Icons.sentiment_dissatisfied},
{'uiType': 'shit', 'backendType': 'shit', 'icon': Icons.thumb_down_alt},
];
return Consumer<ReactionsProvider>(
builder: (context, provider, child) {
// Debug: Print current reaction data
// print('REACTIONS DEBUG:');
// print('- User reaction: ${provider.userReaction}');
// print('- Reaction counts: ${provider.reactionCounts}');
if (provider.isLoading && provider.reactionCounts.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
if (provider.error != null) {
return Center(child: Text('Error loading reactions: ${provider.error}'));
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Реакции',
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: reactions.map((reaction) {
final uiType = reaction['uiType'] as String;
final backendType = reaction['backendType'] as String;
final icon = reaction['icon'] as IconData;
final count = provider.reactionCounts[backendType] ?? 0;
final isSelected = provider.userReaction == backendType;
return Column(
children: [
IconButton(
icon: Icon(icon),
iconSize: 28,
color: isSelected ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
onPressed: () {
if (!authProvider.isAuthenticated) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Login to your account to leave a reaction.'),
duration: Duration(seconds: 2),
),
);
return;
}
provider.setReaction(widget.mediaType, widget.movieId, backendType);
},
),
const SizedBox(height: 4),
Text(
count.toString(),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: isSelected ? Theme.of(context).colorScheme.primary : null,
fontWeight: isSelected ? FontWeight.bold : null,
),
),
],
);
}).toList(),
),
],
);
},
);
}
}