mirror of
https://gitlab.com/foxixus/neomovies_mobile.git
synced 2025-10-28 01:18:50 +05:00
357 lines
15 KiB
Dart
357 lines
15 KiB
Dart
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(),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|