first commit

This commit is contained in:
sartron_Northblue
2025-09-17 11:50:34 +04:00
commit 95afac750a
196 changed files with 6821 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
import 'package:jellyfin/views/home.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(title: 'Fultter demo', home: HomePage());
}
}
+67
View File
@@ -0,0 +1,67 @@
class Movie {
final int id;
final String title;
final String description;
final String imageUrl;
final String category;
final String director;
final String releaseDate;
final bool isFeatured;
final double rating;
final String originalTitle;
const Movie({
required this.id,
required this.title,
required this.description,
required this.imageUrl,
required this.category,
required this.director,
required this.releaseDate,
this.isFeatured = false,
this.rating = 0.0,
this.originalTitle = '',
});
factory Movie.fromJson(Map<String, dynamic> json) {
return Movie(
id: json['id'] ?? 0,
title: json['title'] ?? 'Titre inconnu',
description: json['overview'] ?? 'Description non disponible',
imageUrl: json['poster_path'] != null
? 'https://image.tmdb.org/t/p/w500${json['poster_path']}'
: '',
category: _getCategoryFromGenreIds(json['genre_ids'] ?? []),
director: 'Réalisateur inconnu',
releaseDate: json['release_date'] ?? 'Date inconnue',
rating: (json['vote_average'] ?? 0.0).toDouble(),
originalTitle: json['original_title'] ?? '',
);
}
static String _getCategoryFromGenreIds(List<dynamic> genreIds) {
if (genreIds.contains(28) || genreIds.contains(12)) return 'Action';
if (genreIds.contains(27)) return 'Horreur';
if (genreIds.contains(35)) return 'Comédie';
if (genreIds.contains(18)) return 'Drame';
if (genreIds.contains(37)) return 'Western';
return 'Autre';
}
}
class MovieCategory {
final String name;
final bool isSelected;
const MovieCategory({
required this.name,
this.isSelected = false,
});
MovieCategory copyWith({bool? isSelected}) {
return MovieCategory(
name: name,
isSelected: isSelected ?? this.isSelected,
);
}
}
+102
View File
@@ -0,0 +1,102 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/movie.dart';
class ApiService {
static const String apiKey = '70101da29bbd7fafb66f0ed06dd88794';
static const String accessToken = 'eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI3MDEwMWRhMjliYmQ3ZmFmYjY2ZjBlZDA2ZGQ4ODc5NCIsIm5iZiI6MTc0NzgxNDY5MS44NDA5OTk4LCJzdWIiOiI2ODJkODkyMzYyOTliNjFlNzM2NDk2ODkiLCJzY29wZXMiOlsiYXBpX3JlYWQiXSwidmVyc2lvbiI6MX0.nGl7SvvrI6FlcQ7uOGJzrejN-_s7G0kdvnVCzSGGwm8';
static const String baseUrl = 'https://api.themoviedb.org/3';
static const String imageBaseUrl = 'https://image.tmdb.org/t/p/w500';
static Future<List<Movie>> getPopularMovies() async {
try {
final response = await http.get(
Uri.parse('$baseUrl/movie/popular?api_key=$apiKey'),
headers: {
'Authorization': 'Bearer $accessToken',
'Content-Type': 'application/json',
},
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
final List<dynamic> movies = data['results'];
return movies.map((movie) => Movie.fromJson(movie)).toList();
} else {
throw Exception('Échec du chargement des films: ${response.statusCode}');
}
} catch (e) {
throw Exception('Erreur lors de la récupération des films: $e');
}
}
static Future<List<Movie>> getTopRatedMovies() async {
try {
final response = await http.get(
Uri.parse('$baseUrl/movie/top_rated?api_key=$apiKey'),
headers: {
'Authorization': 'Bearer $accessToken',
'Content-Type': 'application/json',
},
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
final List<dynamic> movies = data['results'];
return movies.map((movie) => Movie.fromJson(movie)).toList();
} else {
throw Exception('Échec du chargement des films: ${response.statusCode}');
}
} catch (e) {
throw Exception('Erreur lors de la récupération des films: $e');
}
}
static Future<List<Movie>> getNowPlayingMovies() async {
try {
final response = await http.get(
Uri.parse('$baseUrl/movie/now_playing?api_key=$apiKey'),
headers: {
'Authorization': 'Bearer $accessToken',
'Content-Type': 'application/json',
},
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
final List<dynamic> movies = data['results'];
return movies.map((movie) => Movie.fromJson(movie)).toList();
} else {
throw Exception('Échec du chargement des films: ${response.statusCode}');
}
} catch (e) {
throw Exception('Erreur lors de la récupération des films: $e');
}
}
static Future<List<Movie>> getUpcomingMovies() async {
try {
final response = await http.get(
Uri.parse('$baseUrl/movie/upcoming?api_key=$apiKey'),
headers: {
'Authorization': 'Bearer $accessToken',
'Content-Type': 'application/json',
},
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
final List<dynamic> movies = data['results'];
return movies.map((movie) => Movie.fromJson(movie)).toList();
} else {
throw Exception('Échec du chargement des films: ${response.statusCode}');
}
} catch (e) {
throw Exception('Erreur lors de la récupération des films: $e');
}
}
}
+168
View File
@@ -0,0 +1,168 @@
import 'package:postgres/postgres.dart';
import '../models/movie.dart';
import 'local_storage_service.dart';
class DatabaseService {
static const String host = 'localhost';
static const String database = 'flutterBDD';
static const String username = 'postgres';
static const String password = 'root';
static const int port = 5432;
static Connection? _connection;
static bool _useLocalStorage = false;
static Future<Connection> get connection async {
if (_connection == null && !_useLocalStorage) {
try {
_connection = await Connection.open(
Endpoint(
host: host,
port: port,
database: database,
username: username,
password: password,
),
);
await _createTables();
} catch (e) {
print('Erreur de connexion à PostgreSQL: $e');
print('Utilisation du stockage local à la place...');
_useLocalStorage = true;
throw Exception('PostgreSQL non disponible, utilisation du stockage local');
}
}
if (_useLocalStorage) {
throw Exception('Utilisation du stockage local');
}
return _connection!;
}
static Future<void> _createTables() async {
final conn = await connection;
await conn.execute('''
CREATE TABLE IF NOT EXISTS favorites (
id SERIAL PRIMARY KEY,
movie_id INTEGER NOT NULL,
title TEXT NOT NULL,
description TEXT,
image_url TEXT,
category TEXT,
director TEXT,
release_date TEXT,
rating REAL,
original_title TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''');
}
static Future<bool> addToFavorites(Movie movie) async {
try {
final conn = await connection;
final existing = await conn.execute(
Sql.named('SELECT id FROM favorites WHERE movie_id = @movieId'),
parameters: {'movieId': movie.id},
);
if (existing.isNotEmpty) {
return false;
}
await conn.execute(
Sql.named('''
INSERT INTO favorites (
movie_id, title, description, image_url, category,
director, release_date, rating, original_title
) VALUES (
@movieId, @title, @description, @imageUrl, @category,
@director, @releaseDate, @rating, @originalTitle
)
'''),
parameters: {
'movieId': movie.id,
'title': movie.title,
'description': movie.description,
'imageUrl': movie.imageUrl,
'category': movie.category,
'director': movie.director,
'releaseDate': movie.releaseDate,
'rating': movie.rating,
'originalTitle': movie.originalTitle,
},
);
return true;
} catch (e) {
print('Erreur PostgreSQL, utilisation du stockage local: $e');
return await LocalStorageService.addToFavorites(movie);
}
}
static Future<bool> removeFromFavorites(int movieId) async {
try {
final conn = await connection;
final result = await conn.execute(
Sql.named('DELETE FROM favorites WHERE movie_id = @movieId'),
parameters: {'movieId': movieId},
);
return result.length > 0;
} catch (e) {
print('Erreur PostgreSQL, utilisation du stockage local: $e');
return await LocalStorageService.removeFromFavorites(movieId);
}
}
static Future<bool> isFavorite(int movieId) async {
try {
final conn = await connection;
final result = await conn.execute(
Sql.named('SELECT id FROM favorites WHERE movie_id = @movieId'),
parameters: {'movieId': movieId},
);
return result.isNotEmpty;
} catch (e) {
print('Erreur PostgreSQL, utilisation du stockage local: $e');
return await LocalStorageService.isFavorite(movieId);
}
}
static Future<List<Movie>> getFavorites() async {
try {
final conn = await connection;
final results = await conn.execute(
Sql('SELECT * FROM favorites ORDER BY created_at DESC'),
);
return results.map((row) => Movie(
id: row[1] as int,
title: row[2] as String,
description: row[3] as String,
imageUrl: row[4] as String,
category: row[5] as String,
director: row[6] as String,
releaseDate: row[7] as String,
rating: (row[8] as double?) ?? 0.0,
originalTitle: row[9] as String,
isFeatured: false,
)).toList();
} catch (e) {
print('Erreur PostgreSQL, utilisation du stockage local: $e');
return await LocalStorageService.getFavorites();
}
}
static Future<void> close() async {
if (_connection != null) {
await _connection!.close();
_connection = null;
}
}
}
+108
View File
@@ -0,0 +1,108 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/movie.dart';
class LocalStorageService {
static const String _favoritesKey = 'favorite_movies';
static Future<bool> addToFavorites(Movie movie) async {
try {
final prefs = await SharedPreferences.getInstance();
final favoritesJson = prefs.getStringList(_favoritesKey) ?? [];
final existing = favoritesJson.any((json) {
final movieData = jsonDecode(json);
return movieData['id'] == movie.id;
});
if (existing) {
return false;
}
final movieJson = jsonEncode({
'id': movie.id,
'title': movie.title,
'description': movie.description,
'imageUrl': movie.imageUrl,
'category': movie.category,
'director': movie.director,
'releaseDate': movie.releaseDate,
'rating': movie.rating,
'originalTitle': movie.originalTitle,
'addedAt': DateTime.now().toIso8601String(),
});
favoritesJson.add(movieJson);
await prefs.setStringList(_favoritesKey, favoritesJson);
return true;
} catch (e) {
print('Erreur lors de l\'ajout aux favoris: $e');
return false;
}
}
static Future<bool> removeFromFavorites(int movieId) async {
try {
final prefs = await SharedPreferences.getInstance();
final favoritesJson = prefs.getStringList(_favoritesKey) ?? [];
final updatedFavorites = favoritesJson.where((json) {
final movieData = jsonDecode(json);
return movieData['id'] != movieId;
}).toList();
if (updatedFavorites.length != favoritesJson.length) {
await prefs.setStringList(_favoritesKey, updatedFavorites);
return true;
}
return false;
} catch (e) {
print('Erreur lors de la suppression des favoris: $e');
return false;
}
}
static Future<bool> isFavorite(int movieId) async {
try {
final prefs = await SharedPreferences.getInstance();
final favoritesJson = prefs.getStringList(_favoritesKey) ?? [];
return favoritesJson.any((json) {
final movieData = jsonDecode(json);
return movieData['id'] == movieId;
});
} catch (e) {
print('Erreur lors de la vérification des favoris: $e');
return false;
}
}
static Future<List<Movie>> getFavorites() async {
try {
final prefs = await SharedPreferences.getInstance();
final favoritesJson = prefs.getStringList(_favoritesKey) ?? [];
return favoritesJson.map((json) {
final movieData = jsonDecode(json);
return Movie(
id: movieData['id'],
title: movieData['title'],
description: movieData['description'],
imageUrl: movieData['imageUrl'],
category: movieData['category'],
director: movieData['director'],
releaseDate: movieData['releaseDate'],
rating: (movieData['rating'] ?? 0.0).toDouble(),
originalTitle: movieData['originalTitle'] ?? '',
isFeatured: false,
);
}).toList();
} catch (e) {
print('Erreur lors de la récupération des favoris: $e');
return [];
}
}
}
+326
View File
@@ -0,0 +1,326 @@
import 'package:flutter/material.dart';
import '../models/movie.dart';
import '../services/database_service.dart';
import 'movie_detail_page.dart';
class FavoritesPage extends StatefulWidget {
const FavoritesPage({super.key});
@override
State<FavoritesPage> createState() => _FavoritesPageState();
}
class _FavoritesPageState extends State<FavoritesPage> {
List<Movie> favoriteMovies = [];
bool isLoading = true;
String? errorMessage;
@override
void initState() {
super.initState();
_loadFavorites();
}
Future<void> _loadFavorites() async {
try {
setState(() {
isLoading = true;
errorMessage = null;
});
final favorites = await DatabaseService.getFavorites();
setState(() {
favoriteMovies = favorites;
isLoading = false;
});
} catch (e) {
setState(() {
errorMessage = e.toString();
isLoading = false;
});
}
}
Future<void> _removeFavorite(Movie movie) async {
final success = await DatabaseService.removeFromFavorites(movie.id);
if (success) {
setState(() {
favoriteMovies.removeWhere((m) => m.id == movie.id);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${movie.title} supprimé des favoris'),
backgroundColor: Colors.red,
),
);
}
}
void _navigateToMovieDetail(Movie movie) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MovieDetailPage(movie: movie),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.purple,
centerTitle: true,
title: const Text(
"MES FAVORIS",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
body: _buildBody(),
);
}
Widget _buildBody() {
if (isLoading) {
return const Center(
child: CircularProgressIndicator(
color: Colors.purple,
),
);
}
if (errorMessage != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
color: Colors.red,
size: 64,
),
const SizedBox(height: 16),
Text(
'Erreur de chargement',
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
errorMessage!,
style: TextStyle(
color: Colors.grey[400],
fontSize: 14,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadFavorites,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purple,
foregroundColor: Colors.white,
),
child: const Text('Réessayer'),
),
],
),
);
}
if (favoriteMovies.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.favorite_border,
color: Colors.grey,
size: 100,
),
const SizedBox(height: 16),
const Text(
'Aucun favori',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'Ajoutez des films à vos favoris\nen cliquant sur le cœur',
style: TextStyle(
color: Colors.grey[400],
fontSize: 16,
),
textAlign: TextAlign.center,
),
],
),
);
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: favoriteMovies.length,
itemBuilder: (context, index) {
final movie = favoriteMovies[index];
return _buildFavoriteItem(movie);
},
);
}
Widget _buildFavoriteItem(Movie movie) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[900],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[800]!, width: 1),
),
child: Row(
children: [
GestureDetector(
onTap: () => _navigateToMovieDetail(movie),
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.grey[700],
),
child: movie.imageUrl.isNotEmpty
? Image.network(
movie.imageUrl,
width: 80,
height: 80,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
gradient: LinearGradient(
colors: [Colors.grey[600]!, Colors.grey[800]!],
),
),
child: const Center(
child: Icon(
Icons.movie,
color: Colors.white,
size: 30,
),
),
);
},
)
: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
gradient: LinearGradient(
colors: [Colors.grey[600]!, Colors.grey[800]!],
),
),
child: const Center(
child: Icon(
Icons.movie,
color: Colors.white,
size: 30,
),
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
movie.title,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Row(
children: [
const Icon(Icons.star, color: Colors.amber, size: 14),
const SizedBox(width: 4),
Text(
movie.rating.toStringAsFixed(1),
style: TextStyle(
color: Colors.amber,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 8),
Text(
movie.category,
style: TextStyle(
color: Colors.purple[300],
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 4),
Text(
movie.description,
style: TextStyle(
color: Colors.grey[400],
fontSize: 12,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
movie.releaseDate,
style: TextStyle(
color: Colors.grey[500],
fontSize: 10,
),
),
],
),
),
Column(
children: [
IconButton(
onPressed: () => _removeFavorite(movie),
icon: const Icon(
Icons.favorite,
color: Colors.red,
size: 24,
),
),
IconButton(
onPressed: () => _navigateToMovieDetail(movie),
icon: const Icon(
Icons.play_arrow,
color: Colors.red,
size: 24,
),
),
],
),
],
),
);
}
}
+474
View File
@@ -0,0 +1,474 @@
import 'package:flutter/material.dart';
import '../models/movie.dart';
import '../services/api_service.dart';
import 'movie_detail_page.dart';
import 'favorites_page.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
List<MovieCategory> categories = [
const MovieCategory(name: "TOUS", isSelected: true),
const MovieCategory(name: "POPULAIRES"),
const MovieCategory(name: "MIEUX NOTÉS"),
const MovieCategory(name: "EN COURS"),
const MovieCategory(name: "PROCHAINEMENT"),
];
List<Movie> movies = [];
bool isLoading = true;
String? errorMessage;
int currentFeaturedIndex = 0;
int _currentIndex = 1;
@override
void initState() {
super.initState();
_loadMovies();
}
Future<void> _loadMovies() async {
try {
setState(() {
isLoading = true;
errorMessage = null;
});
final popularMovies = await ApiService.getPopularMovies();
setState(() {
movies = popularMovies;
if (movies.isNotEmpty) {
movies[0] = Movie(
id: movies[0].id,
title: movies[0].title,
description: movies[0].description,
imageUrl: movies[0].imageUrl,
category: movies[0].category,
director: movies[0].director,
releaseDate: movies[0].releaseDate,
isFeatured: true,
rating: movies[0].rating,
originalTitle: movies[0].originalTitle,
);
}
isLoading = false;
});
} catch (e) {
setState(() {
errorMessage = e.toString();
isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.purple,
centerTitle: true,
toolbarHeight: 80,
title: const Text(
"MOVIES",
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
letterSpacing: 3,
color: Colors.white,
),
),
),
body: _getBody(),
bottomNavigationBar: _buildBottomNav(),
),
);
}
Widget _buildBody() {
if (isLoading) {
return const Center(
child: CircularProgressIndicator(
color: Colors.purple,
),
);
}
if (errorMessage != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
color: Colors.red,
size: 64,
),
const SizedBox(height: 16),
Text(
'Erreur de chargement',
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
errorMessage!,
style: TextStyle(
color: Colors.grey[400],
fontSize: 14,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadMovies,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purple,
foregroundColor: Colors.white,
),
child: const Text('Réessayer'),
),
],
),
);
}
return Column(
children: [
_buildCategoryBar(),
Expanded(
child: ListView(
children: [
if (movies.isNotEmpty) ...[
_buildFeaturedMovie(),
_buildFeaturedIndicators(),
],
...movies.skip(5).map((movie) => _buildMovieItem(movie)),
],
),
),
],
);
}
Widget _buildCategoryBar() {
return Container(
height: 50,
color: Colors.black,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: categories.length,
itemBuilder: (context, index) {
final isSelected = index == 0;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: Text(
categories[index].name,
style: TextStyle(
color: isSelected ? Colors.white : Colors.grey,
fontSize: 16,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
);
},
),
);
}
Widget _getBody() {
switch (_currentIndex) {
case 0:
return const FavoritesPage();
case 1:
return _buildBody();
case 2:
return _buildProfilePage();
default:
return _buildBody();
}
}
Widget _buildProfilePage() {
return Container(
color: Colors.black,
child: const Center(
child: Text(
'Profil',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
);
}
Widget _buildBottomNav() {
return BottomNavigationBar(
backgroundColor: Colors.white,
selectedItemColor: Colors.purple,
unselectedItemColor: Colors.grey,
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
items: const [
BottomNavigationBarItem(icon: Icon(Icons.favorite), label: 'Favoris'),
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Accueil'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profil'),
],
);
}
Widget _buildFeaturedMovie() {
final featuredMovies = movies.take(5).toList();
return Container(
height: 300,
margin: const EdgeInsets.all(16),
child: PageView.builder(
onPageChanged: (index) {
setState(() {
currentFeaturedIndex = index;
});
},
itemCount: featuredMovies.length,
itemBuilder: (context, index) {
final movie = featuredMovies[index];
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.grey[800],
),
child: Stack(
children: [
_buildMovieImage(movie.imageUrl, 12, movie: movie),
_buildGradientOverlay(),
_buildMovieDetails(movie),
_buildPlayButton(50, 30),
],
),
);
},
),
);
}
Widget _buildMovieItem(Movie movie) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[900],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[800]!, width: 1),
),
child: Row(
children: [
_buildMoviePoster(movie),
const SizedBox(width: 16),
Expanded(child: _buildMovieInfo(movie)),
_buildSimplePlayButton(40, 24),
],
),
);
}
Widget _buildMoviePoster(Movie movie) {
return Container(
width: 80,
height: 80,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.grey[700],
),
child: _buildMovieImage(movie.imageUrl, 8, movie: movie),
);
}
Widget _buildMovieInfo(Movie movie) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(movie.title, style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Row(
children: [
Icon(Icons.star, color: Colors.amber, size: 14),
const SizedBox(width: 4),
Text(movie.rating.toStringAsFixed(1), style: TextStyle(color: Colors.amber, fontSize: 12, fontWeight: FontWeight.w500)),
const SizedBox(width: 8),
Text(movie.category, style: TextStyle(color: Colors.purple[300], fontSize: 12, fontWeight: FontWeight.w500)),
],
),
const SizedBox(height: 4),
Text(movie.description, style: TextStyle(color: Colors.grey[400], fontSize: 12), maxLines: 2, overflow: TextOverflow.ellipsis),
const SizedBox(height: 4),
Text(movie.releaseDate, style: TextStyle(color: Colors.grey[500], fontSize: 10)),
],
);
}
Widget _buildMovieImage(String imageUrl, double radius, {Movie? movie}) {
return GestureDetector(
onTap: movie != null ? () => _navigateToMovieDetail(movie) : null,
child: ClipRRect(
borderRadius: BorderRadius.circular(radius),
child: imageUrl.isNotEmpty
? Image.network(
imageUrl,
width: radius == 12 ? double.infinity : 80,
height: radius == 12 ? double.infinity : 80,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => _buildFallbackImage(radius),
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
width: radius == 12 ? double.infinity : 80,
height: radius == 12 ? double.infinity : 80,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(radius),
color: Colors.grey[800],
),
child: const Center(
child: CircularProgressIndicator(
color: Colors.purple,
strokeWidth: 2,
),
),
);
},
)
: _buildFallbackImage(radius),
),
);
}
Widget _buildFallbackImage(double radius) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(radius),
gradient: LinearGradient(
colors: radius == 12
? [Colors.blue[900]!, Colors.purple[900]!]
: [Colors.grey[600]!, Colors.grey[800]!],
),
),
child: Center(
child: Icon(Icons.movie, color: Colors.white, size: radius == 12 ? 50 : 30),
),
);
}
Widget _buildGradientOverlay() {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.transparent, Colors.black.withOpacity(0.7)],
),
),
);
}
Widget _buildMovieDetails(Movie movie) {
return Positioned(
left: 20,
bottom: 20,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("FILM POPULAIRE", style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 12)),
const SizedBox(height: 4),
Text(movie.category.toUpperCase(), style: TextStyle(color: Colors.purple[300], fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text(movie.title, style: const TextStyle(color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold)),
Row(
children: [
const Icon(Icons.star, color: Colors.amber, size: 16),
const SizedBox(width: 4),
Text("${movie.rating.toStringAsFixed(1)}/10", style: const TextStyle(color: Colors.white, fontSize: 16)),
],
),
const SizedBox(height: 8),
Text(movie.releaseDate, style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 12)),
],
),
);
}
Widget _buildPlayButton(double size, double iconSize) {
return Positioned(
right: 20,
bottom: 20,
child: Container(
width: size,
height: size,
decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle),
child: Icon(Icons.play_arrow, color: Colors.white, size: iconSize),
),
);
}
Widget _buildSimplePlayButton(double size, double iconSize) {
return Container(
width: size,
height: size,
decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle),
child: Icon(Icons.play_arrow, color: Colors.white, size: iconSize),
);
}
Widget _buildFeaturedIndicators() {
final featuredCount = movies.take(5).length;
return Container(
height: 20,
margin: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
featuredCount,
(index) => Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: index == currentFeaturedIndex
? Colors.purple
: Colors.grey.withOpacity(0.5),
),
),
),
),
);
}
void _navigateToMovieDetail(Movie movie) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MovieDetailPage(movie: movie),
),
);
}
}
+348
View File
@@ -0,0 +1,348 @@
import 'package:flutter/material.dart';
import '../models/movie.dart';
import '../services/database_service.dart';
class MovieDetailPage extends StatefulWidget {
final Movie movie;
const MovieDetailPage({super.key, required this.movie});
@override
State<MovieDetailPage> createState() => _MovieDetailPageState();
}
class _MovieDetailPageState extends State<MovieDetailPage> {
bool isFavorite = false;
bool isLoading = false;
@override
void initState() {
super.initState();
_checkFavoriteStatus();
}
Future<void> _checkFavoriteStatus() async {
final favorite = await DatabaseService.isFavorite(widget.movie.id);
setState(() {
isFavorite = favorite;
});
}
Future<void> _toggleFavorite() async {
setState(() {
isLoading = true;
});
try {
if (isFavorite) {
final success = await DatabaseService.removeFromFavorites(widget.movie.id);
if (success) {
setState(() {
isFavorite = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${widget.movie.title} supprimé des favoris'),
backgroundColor: Colors.red,
),
);
}
} else {
final success = await DatabaseService.addToFavorites(widget.movie);
if (success) {
setState(() {
isFavorite = true;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${widget.movie.title} ajouté aux favoris'),
backgroundColor: Colors.green,
),
);
}
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur: $e'),
backgroundColor: Colors.red,
),
);
} finally {
setState(() {
isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.purple,
centerTitle: true,
title: Text(
widget.movie.title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
actions: [
IconButton(
icon: isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: Icon(
isFavorite ? Icons.favorite : Icons.favorite_border,
color: isFavorite ? Colors.red : Colors.white,
),
onPressed: isLoading ? null : _toggleFavorite,
),
],
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: double.infinity,
height: 300,
child: widget.movie.imageUrl.isNotEmpty
? Image.network(
widget.movie.imageUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey[800],
child: const Center(
child: Icon(
Icons.movie,
color: Colors.white,
size: 100,
),
),
);
},
)
: Container(
color: Colors.grey[800],
child: const Center(
child: Icon(
Icons.movie,
color: Colors.white,
size: 100,
),
),
),
),
Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
widget.movie.title,
style: const TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.amber,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.star, color: Colors.white, size: 16),
const SizedBox(width: 4),
Text(
widget.movie.rating.toStringAsFixed(1),
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.purple,
borderRadius: BorderRadius.circular(20),
),
child: Text(
widget.movie.category,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 12),
Text(
widget.movie.releaseDate,
style: TextStyle(
color: Colors.grey[400],
fontSize: 16,
),
),
],
),
const SizedBox(height: 24),
const Text(
'Description',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Text(
widget.movie.description,
style: TextStyle(
color: Colors.grey[300],
fontSize: 16,
height: 1.5,
),
),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Lecture du film...'),
backgroundColor: Colors.purple,
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.play_arrow, size: 24),
SizedBox(width: 8),
Text(
'Regarder maintenant',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[900],
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Informations',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
_buildInfoRow('ID', widget.movie.id.toString()),
_buildInfoRow('Titre original', widget.movie.originalTitle.isNotEmpty ? widget.movie.originalTitle : widget.movie.title),
_buildInfoRow('Note', '${widget.movie.rating.toStringAsFixed(1)}/10'),
_buildInfoRow('Catégorie', widget.movie.category),
_buildInfoRow('Date de sortie', widget.movie.releaseDate),
],
),
),
],
),
),
],
),
),
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: Text(
'$label:',
style: TextStyle(
color: Colors.grey[400],
fontSize: 14,
),
),
),
Expanded(
child: Text(
value,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
),
),
),
],
),
);
}
}