mirror of
https://gitlab.com/foxixus/neomovies_mobile.git
synced 2025-10-27 23:18:49 +05:00
Initial commit
This commit is contained in:
356
lib/presentation/screens/movie_detail/movie_detail_screen.dart
Normal file
356
lib/presentation/screens/movie_detail/movie_detail_screen.dart
Normal file
@@ -0,0 +1,356 @@
|
||||
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(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user