diff options
author | Iván Ávalos <avalos@disroot.org> | 2023-06-03 23:36:51 -0600 |
---|---|---|
committer | Iván Ávalos <avalos@disroot.org> | 2023-06-03 23:36:51 -0600 |
commit | a28fa912bc04f28125c3d62d2e288145233cd789 (patch) | |
tree | 0913b5387aeded6206fef98ccc834612debce778 | |
parent | 17bd9bc740e3be5509b81d42184819324881fe54 (diff) | |
download | pmsna1-a28fa912bc04f28125c3d62d2e288145233cd789.tar.gz pmsna1-a28fa912bc04f28125c3d62d2e288145233cd789.tar.bz2 pmsna1-a28fa912bc04f28125c3d62d2e288145233cd789.zip |
Finished practice 5
-rw-r--r-- | android/build.gradle | 2 | ||||
-rw-r--r-- | lib/database/helper.dart | 38 | ||||
-rw-r--r-- | lib/models/popular.dart | 22 | ||||
-rw-r--r-- | lib/models/popular_credit.dart | 19 | ||||
-rw-r--r-- | lib/models/popular_detail.dart | 83 | ||||
-rw-r--r-- | lib/models/popular_video.dart | 32 | ||||
-rw-r--r-- | lib/network/popular_api.dart | 29 | ||||
-rw-r--r-- | lib/routes.dart | 2 | ||||
-rw-r--r-- | lib/screens/favorites_screen.dart | 96 | ||||
-rw-r--r-- | lib/screens/popular_detail_screen.dart | 385 | ||||
-rw-r--r-- | lib/screens/popular_screen.dart | 44 | ||||
-rw-r--r-- | lib/screens/register_screen.dart | 2 | ||||
-rw-r--r-- | lib/widgets/popular_item.dart | 40 | ||||
-rw-r--r-- | lib/widgets/popular_list.dart | 38 | ||||
-rw-r--r-- | linux/flutter/generated_plugin_registrant.cc | 4 | ||||
-rw-r--r-- | linux/flutter/generated_plugins.cmake | 1 | ||||
-rw-r--r-- | macos/Flutter/GeneratedPluginRegistrant.swift | 2 | ||||
-rw-r--r-- | pubspec.lock | 202 | ||||
-rw-r--r-- | pubspec.yaml | 7 | ||||
-rw-r--r-- | windows/flutter/generated_plugin_registrant.cc | 6 | ||||
-rw-r--r-- | windows/flutter/generated_plugins.cmake | 2 |
21 files changed, 941 insertions, 115 deletions
diff --git a/android/build.gradle b/android/build.gradle index 144835c..f1fb363 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -27,6 +27,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/lib/database/helper.dart b/lib/database/helper.dart index da2d199..54e415c 100644 --- a/lib/database/helper.dart +++ b/lib/database/helper.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:pmsna1/models/event.dart'; +import 'package:pmsna1/models/popular.dart'; import 'package:pmsna1/models/post.dart'; import 'package:sqflite/sqflite.dart'; @@ -47,6 +48,14 @@ class DatabaseHelper { date DATE NOT NULL, completed INT DEFAULT 0 );"""); + + await db.execute("""CREATE TABLE IF NOT EXISTS favorites ( + id INT PRIMARY KEY, + title VARCHAR(128), + posterPath VARCHAR(128), + backdropPath VARCHAR(128), + overview TEXT +);"""); } Future<int> insert(String table, Map<String, dynamic> data) async { @@ -92,4 +101,33 @@ class DatabaseHelper { List<Map<String, Object?>> result = await conn.query("events"); return result.map((event) => Event.fromMap(event)).toList(); } + + /* + * Favorite specific functions + */ + + Future<List<Popular>> getAllFavorites() async { + Database conn = await database; + List<Map<String, Object?>> result = await conn.query("favorites"); + return result.map((p) { + Popular popular = Popular.fromDb(p); + popular.hasFavorite = true; + return popular; + }).toList(); + } + + Future<bool> hasFavorite(int id) async { + Database conn = await database; + List result = + await conn.query("favorites", where: "id = ?", whereArgs: [id]); + return result.isNotEmpty; + } + + Future<int> favoritePopular(Popular popular) async { + return insert('favorites', popular.toDb()); + } + + Future<int> unfavoritePopular(Popular popular) async { + return delete('favorites', 'id', popular.id); + } } diff --git a/lib/models/popular.dart b/lib/models/popular.dart index afe6858..a1c136b 100644 --- a/lib/models/popular.dart +++ b/lib/models/popular.dart @@ -11,6 +11,8 @@ class Popular { double? voteAverage; int? voteCount; + bool hasFavorite = false; + Popular({ this.backdropPath, this.id, @@ -25,6 +27,26 @@ class Popular { this.voteCount, }); + Map<String, dynamic> toDb() { + return { + "id": id, + "title": title, + "posterPath": posterPath, + "backdropPath": backdropPath, + "overview": overview, + }; + } + + factory Popular.fromDb(Map<String, dynamic> map) { + return Popular( + id: map['id'], + title: map['title'], + posterPath: map['posterPath'], + backdropPath: map['backdropPath'], + overview: map['overview'], + ); + } + factory Popular.fromMap(Map<String, dynamic> map) { return Popular( backdropPath: map['backdrop_path'], diff --git a/lib/models/popular_credit.dart b/lib/models/popular_credit.dart new file mode 100644 index 0000000..568beb0 --- /dev/null +++ b/lib/models/popular_credit.dart @@ -0,0 +1,19 @@ +class PopularCredit { + String? name; + String? character; + String? profilePath; + + PopularCredit({ + this.name, + this.character, + this.profilePath, + }); + + factory PopularCredit.fromMap(Map<String, dynamic> map) { + return PopularCredit( + name: map['name'], + character: map['character'], + profilePath: map['profile_path'], + ); + } +} diff --git a/lib/models/popular_detail.dart b/lib/models/popular_detail.dart new file mode 100644 index 0000000..8b158ac --- /dev/null +++ b/lib/models/popular_detail.dart @@ -0,0 +1,83 @@ +import 'package:pmsna1/models/popular_credit.dart'; +import 'package:pmsna1/models/popular_video.dart'; + +class Genre { + int? id; + String? name; +} + +class PopularDetail { + bool? adult; + String? backdropPath; + int? budget; + List<Genre>? genres; + String? homepage; + int? id; + String? originalLanguage; + String? overview; + double? popularity; + String? posterPath; + String? releaseDate; + int? revenue; + int? runtime; + String? tagline; + String? title; + bool? video; + double? voteAverage; + double? voteCount; + List<PopularVideo> videos; + List<PopularCredit> credits; + + bool hasFavorite = false; + + PopularDetail({ + this.adult, + this.backdropPath, + this.budget, + this.genres, + this.homepage, + this.id, + this.originalLanguage, + this.overview, + this.popularity, + this.posterPath, + this.releaseDate, + this.revenue, + this.runtime, + this.tagline, + this.title, + this.video, + this.voteAverage, + this.voteCount, + this.videos = const [], + this.credits = const [], + }); + + factory PopularDetail.fromMap(Map<String, dynamic> map) { + return PopularDetail( + adult: map['adult'], + backdropPath: map['backdrop_path'], + id: map['id'], + originalLanguage: map['original_language'], + overview: map['overview'], + popularity: map['popularity'], + posterPath: map['posterPath'], + releaseDate: map['releaseDate'], + revenue: map['revenue'], + runtime: map['runtime'], + tagline: map['tagline'], + title: map['title'], + video: map['video'], + voteAverage: (map['vote_average'] is int) + ? (map['vote_average'] as int).toDouble() + : map['vote_average'], + voteCount: map['voteCount'], + videos: (map['videos']['results'] as List<dynamic>) + .map((v) => PopularVideo.fromMap(v)) + .toList(), + credits: (map['credits']['cast'] as List<dynamic>) + .map((c) => PopularCredit.fromMap(c)) + .toList(), + ); + } +} diff --git a/lib/models/popular_video.dart b/lib/models/popular_video.dart new file mode 100644 index 0000000..b626431 --- /dev/null +++ b/lib/models/popular_video.dart @@ -0,0 +1,32 @@ +class PopularVideo { + String? name; + String? key; + String? site; + int? size; + String? type; + bool? official; + String? id; + + PopularVideo({ + this.name, + this.key, + this.site, + this.size, + this.type, + this.official, + this.id, + }); + + factory PopularVideo.fromMap(Map<String, dynamic> map) { + print(map.toString()); + return PopularVideo( + id: map['id'], + key: map['key'], + name: map['name'], + site: map['site'], + size: map['size'], + type: map['type'], + official: map['official'], + ); + } +} diff --git a/lib/network/popular_api.dart b/lib/network/popular_api.dart index 4d2c033..bf4098a 100644 --- a/lib/network/popular_api.dart +++ b/lib/network/popular_api.dart @@ -1,19 +1,42 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:pmsna1/database/helper.dart'; import 'package:pmsna1/models/popular.dart'; +import 'package:pmsna1/models/popular_detail.dart'; class PopularApi { String apiKey = '0cb894064f40656f3575e8ccae3d8d73'; - Uri get link => Uri.parse( + Uri get popularLink => Uri.parse( 'https://api.themoviedb.org/3/movie/popular?api_key=$apiKey&language=es-MX&page=1'); + Uri getDetailLink(int id) => Uri.parse( + 'https://api.themoviedb.org/3/movie/$id?api_key=$apiKey&language=es-MX&page=1&append_to_response=videos,credits', + ); Future<List<Popular>?> getAllPopular() async { - http.Response result = await http.get(link); + http.Response result = await http.get(popularLink); var list = jsonDecode(result.body)['results'] as List; if (result.statusCode != 200) { return null; } - return list.map((popular) => Popular.fromMap(popular)).toList(); + List<Popular> favorites = await DatabaseHelper().getAllFavorites(); + return list.map((p) { + Popular popular = Popular.fromMap(p); + if (favorites.where((f) => f.id == popular.id).isNotEmpty) { + popular.hasFavorite = true; + } + return popular; + }).toList(); + } + + Future<PopularDetail?> getPopularDetail(int id) async { + http.Response result = await http.get(getDetailLink(id)); + var map = jsonDecode(result.body) as Map<String, dynamic>; + if (result.statusCode != 200) { + return null; + } + PopularDetail detail = PopularDetail.fromMap(map); + detail.hasFavorite = await DatabaseHelper().hasFavorite(detail.id!); + return detail; } } diff --git a/lib/routes.dart b/lib/routes.dart index 7b64e9c..5f5d192 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -8,6 +8,7 @@ import 'package:pmsna1/screens/map_screen.dart'; import 'package:pmsna1/screens/new_event_screen.dart'; import 'package:pmsna1/screens/new_post_screen.dart'; import 'package:pmsna1/screens/onboarding_screen.dart'; +import 'package:pmsna1/screens/popular_detail_screen.dart'; import 'package:pmsna1/screens/popular_screen.dart'; import 'screens/login_screen.dart'; @@ -26,6 +27,7 @@ Map<String, WidgetBuilder> getApplicationRoutes() { '/albums': (BuildContext context) => const AlbumsScreen(), '/album': (BuildContext context) => const AlbumDetailScreen(), '/favorites': (BuildContext context) => const FavoritesScreen(), + '/popular_detail': (BuildContext context) => const PopularDetailScreen(), '/map': (BuildContext context) => const MapSample(), }; } diff --git a/lib/screens/favorites_screen.dart b/lib/screens/favorites_screen.dart index 74432a7..132b61f 100644 --- a/lib/screens/favorites_screen.dart +++ b/lib/screens/favorites_screen.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:pmsna1/firebase/favorites.dart'; +import 'package:pmsna1/database/helper.dart'; + +import '../widgets/loading_modal_widget.dart'; +import '../widgets/popular_list.dart'; class FavoritesScreen extends StatefulWidget { const FavoritesScreen({super.key}); @@ -9,73 +12,48 @@ class FavoritesScreen extends StatefulWidget { } class _FavoritesScreenState extends State<FavoritesScreen> { - final FavoritesFirebase _firebase = FavoritesFirebase(); + late DatabaseHelper _db; + + @override + void initState() { + super.initState(); + _db = DatabaseHelper(); + } @override Widget build(BuildContext context) { return Scaffold( - body: StreamBuilder( - stream: _firebase.getAllFavorites(), + appBar: AppBar(title: const Text('Favoritos')), + body: FutureBuilder( + future: _db.getAllFavorites(), builder: (context, snapshot) { if (snapshot.hasData) { - return ListView.builder( - itemCount: snapshot.data!.size, - itemBuilder: (context, index) => ListTile( - title: Text(snapshot.data!.docs[index].get('title')), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: () { - // _firebase.insertFavorite({ - // 'title': snapshot.data!.docs[index].get('title') - // }) - }, - icon: Icon( - Icons.favorite, - color: Theme.of(context).colorScheme.onBackground, - ), - ), - IconButton( - onPressed: () { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Confirmar borrado'), - content: const Text('¿Desea borrar el post?'), - actions: [ - TextButton( - child: const Text('Sí'), - onPressed: () { - // Delete post - _firebase.deleteFavorite( - snapshot.data!.docs[index].id); - Navigator.of(context).pop(); - }, - ), - TextButton( - child: const Text('No'), - onPressed: () { - Navigator.of(context).pop(); - }, - ) - ], - ), - ); - }, - icon: Icon( - Icons.delete, - color: Theme.of(context).colorScheme.onBackground, - ), - ), - ], - ), - ), + return PopularList( + snapshot.data!, + onFavorited: (popular, favorited) { + if (favorited) { + _db.favoritePopular(popular).whenComplete(() { + setState(() {}); + }); + } else { + _db.unfavoritePopular(popular).whenComplete(() { + setState(() {}); + }); + } + }, + onPressed: (popular) { + Navigator.of(context) + .pushNamed('/popular_detail', arguments: popular); + }, ); } else if (snapshot.hasError) { - return const Center(child: Text('Hubo un error')); + print(snapshot.error); + return const Center( + child: Text('Ocurrió un error'), + ); + } else { + return const LoadingModal(); } - return const Center(child: CircularProgressIndicator()); }, ), ); diff --git a/lib/screens/popular_detail_screen.dart b/lib/screens/popular_detail_screen.dart new file mode 100644 index 0000000..16f7811 --- /dev/null +++ b/lib/screens/popular_detail_screen.dart @@ -0,0 +1,385 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_rating_bar/flutter_rating_bar.dart'; +import 'package:infinite_carousel/infinite_carousel.dart'; +import 'package:pmsna1/database/helper.dart'; +import 'package:pmsna1/models/popular_detail.dart'; +import 'package:pmsna1/models/popular_video.dart'; +import 'package:pmsna1/network/popular_api.dart'; +import 'package:pmsna1/widgets/loading_modal_widget.dart'; +import 'package:youtube/youtube_thumbnail.dart'; +import 'package:youtube_player_iframe/youtube_player_iframe.dart'; + +import '../models/popular.dart'; +import '../models/popular_credit.dart'; + +class PopularDetailScreen extends StatefulWidget { + const PopularDetailScreen({super.key}); + + @override + State<PopularDetailScreen> createState() => _PopularDetailScreenState(); +} + +class _PopularDetailScreenState extends State<PopularDetailScreen> { + late Popular popular; + late PopularApi api; + late DatabaseHelper _db; + + @override + void initState() { + super.initState(); + api = PopularApi(); + _db = DatabaseHelper(); + } + + @override + Widget build(BuildContext context) { + popular = ModalRoute.of(context)?.settings.arguments as Popular; + + ImageFiltered backdrop = ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 9.0, sigmaY: 9.0), + child: Image.network( + 'https://image.tmdb.org/t/p/w500/${popular.backdropPath!}', + fit: BoxFit.cover, + alignment: Alignment.center, + ), + ); + + return Scaffold( + body: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => <Widget>[ + SliverAppBar( + expandedHeight: 200.0, + pinned: true, + actions: [ + FutureBuilder( + future: _db.hasFavorite(popular.id!), + builder: (context, snapshot) { + if (snapshot.hasData) { + return snapshot.data == true + ? IconButton( + icon: const Icon(Icons.favorite), + onPressed: () { + _db.unfavoritePopular(popular).whenComplete(() { + setState(() {}); + }); + }, + ) + : IconButton( + icon: const Icon(Icons.favorite_outline), + onPressed: () { + _db.favoritePopular(popular).whenComplete(() { + setState(() {}); + }); + }, + ); + } else if (snapshot.hasError) { + return const SizedBox.shrink(); + } else { + return const CircularProgressIndicator(value: null); + } + }, + ) + ], + flexibleSpace: FlexibleSpaceBar( + title: Text(popular.title!), + background: backdrop, + ), + ) + ], + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + children: [ + Synopsis(popular: popular), + FutureBuilder( + future: api.getPopularDetail(popular.id!), + builder: (context, snapshot) { + if (snapshot.hasData) { + PopularDetail detail = snapshot.data!; + return Column( + children: [ + Rating(detail.voteAverage), + Trailers(detail.videos), + Casting(detail.credits), + ], + ); + } else if (snapshot.hasError) { + print(snapshot.error); + return const Center(child: Text('Ocurrió un error')); + } else { + return const LoadingModal(); + } + }, + ) + ], + ), + ), + ), + ), + ); + } +} + +class Synopsis extends StatelessWidget { + const Synopsis({ + super.key, + required this.popular, + }); + + final Popular popular; + + @override + Widget build(BuildContext context) { + return Card( + clipBehavior: Clip.antiAliasWithSaveLayer, + semanticContainer: true, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Hero( + tag: popular.posterPath!, + child: Card( + clipBehavior: Clip.antiAliasWithSaveLayer, + semanticContainer: true, + child: FadeInImage( + height: 160.0, + placeholder: const AssetImage('assets/loading.gif'), + image: NetworkImage( + 'https://image.tmdb.org/t/p/w500/${popular.posterPath!}', + ), + ), + ), + ), + const SizedBox(width: 16.0), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Sinopsis', + style: Theme.of(context).typography.englishLike.titleLarge, + ), + const SizedBox(height: 10), + Text(popular.overview ?? ""), + ], + ), + ), + // Expanded(child: Text(popular.overview ?? "")), + ], + ), + ), + ); + } +} + +class Rating extends StatelessWidget { + final double? voteAverage; + const Rating(this.voteAverage, {super.key}); + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Rating', + style: Theme.of(context).typography.englishLike.headlineLarge, + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + RatingBar.builder( + initialRating: (voteAverage ?? 0.0) / 2, + minRating: 0.0, + direction: Axis.horizontal, + allowHalfRating: true, + itemCount: 5, + itemPadding: const EdgeInsets.symmetric(horizontal: 4.0), + itemBuilder: (context, _) => Icon( + Icons.star, + color: Theme.of(context).colorScheme.primary, + ), + ignoreGestures: true, + onRatingUpdate: (rating) {}, + ), + ], + ), + ], + ), + ), + ); + } +} + +class Trailers extends StatefulWidget { + final List<PopularVideo> videos; + const Trailers(this.videos, {super.key}); + + @override + State<Trailers> createState() => _TrailersState(); +} + +class _TrailersState extends State<Trailers> { + List<PopularVideo> get filteredVideos => widget.videos + .where((v) => + v.site == 'YouTube' && v.official == true && v.type == 'Trailer') + .toList(); + + final _controller = YoutubePlayerController( + params: const YoutubePlayerParams( + mute: false, + showControls: true, + showFullscreenButton: false, + ), + ); + + bool isLoaded = false; + + @override + Widget build(BuildContext context) { + return Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + child: Text( + 'Trailers', + style: Theme.of(context).typography.englishLike.headlineLarge, + ), + ), + isLoaded + ? Padding( + padding: const EdgeInsets.only(top: 16), + child: YoutubePlayer( + controller: _controller, + aspectRatio: 16 / 9, + ), + ) + : const SizedBox.shrink(), + ListView.builder( + padding: const EdgeInsets.all(16), + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: filteredVideos.length, + itemBuilder: (context, index) { + PopularVideo video = widget.videos[index]; + return Card( + clipBehavior: Clip.antiAliasWithSaveLayer, + semanticContainer: true, + child: InkWell( + onTap: () { + _controller.loadVideoById(videoId: video.key!); + setState(() { + isLoaded = true; + }); + }, + child: Row( + children: [ + Image.network( + YoutubeThumbnail(youtubeId: video.key!).standard(), + height: 100.0, + errorBuilder: (context, error, stackTrace) => Container( + color: Theme.of(context).colorScheme.primary, + height: 100.0, + width: (4 * 100) / 3, + child: const Icon(Icons.movie), + ), + ), + const SizedBox(width: 16.0), + Expanded( + child: Text( + video.name ?? "", + style: Theme.of(context) + .typography + .englishLike + .titleMedium, + ), + ), + ], + ), + ), + ); + }, + ) + ], + ), + ); + } +} + +class Casting extends StatelessWidget { + final List<PopularCredit> credits; + const Casting(this.credits, {super.key}); + + @override + Widget build(BuildContext context) { + return Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + child: Text( + 'Reparto', + style: Theme.of(context).typography.englishLike.headlineLarge, + ), + ), + SizedBox( + height: 350.0, + child: InfiniteCarousel.builder( + axisDirection: Axis.horizontal, + velocityFactor: 0.5, + itemCount: credits.length, + itemExtent: 160, + itemBuilder: (context, itemIndex, realIndex) { + PopularCredit credit = credits[itemIndex]; + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CircleAvatar( + radius: 50.0, + backgroundImage: credit.profilePath != null + ? NetworkImage( + 'https://image.tmdb.org/t/p/w500/${credit.profilePath!}', + ) + : null, + child: credit.profilePath == null + ? const Icon(Icons.person) + : null, + ), + const SizedBox(height: 16.0), + Text( + credit.name ?? "", + style: Theme.of(context) + .typography + .englishLike + .titleMedium, + ), + Text('como «${credit.character ?? ""}»'), + ], + ), + ), + ), + ); + }, + ), + ) + ], + ), + ); + } +} diff --git a/lib/screens/popular_screen.dart b/lib/screens/popular_screen.dart index 07ca70e..b961ce9 100644 --- a/lib/screens/popular_screen.dart +++ b/lib/screens/popular_screen.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:pmsna1/database/helper.dart'; import 'package:pmsna1/models/popular.dart'; import 'package:pmsna1/network/popular_api.dart'; import 'package:pmsna1/widgets/loading_modal_widget.dart'; -import 'package:pmsna1/widgets/popular_item.dart'; + +import '../widgets/popular_list.dart'; class PopularScreen extends StatefulWidget { const PopularScreen({super.key}); @@ -13,31 +15,49 @@ class PopularScreen extends StatefulWidget { class _PopularScreenState extends State<PopularScreen> { PopularApi? popularApi; + late DatabaseHelper _db; @override void initState() { super.initState(); popularApi = PopularApi(); + _db = DatabaseHelper(); } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Películas populares')), + appBar: AppBar( + title: const Text('Películas populares'), + actions: [ + IconButton( + icon: const Icon(Icons.favorite_outline), + onPressed: () { + Navigator.of(context).pushNamed('/favorites'); + }, + ), + ], + ), body: FutureBuilder( future: popularApi!.getAllPopular(), builder: (context, AsyncSnapshot<List<Popular>?> snapshot) { if (snapshot.hasData) { - return GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - childAspectRatio: .9, - mainAxisSpacing: 10, - crossAxisSpacing: 10, - ), - itemCount: snapshot.data != null ? snapshot.data!.length : 0, - itemBuilder: (context, index) { - return PopularItem(snapshot.data![index]); + return PopularList( + snapshot.data!, + onFavorited: (popular, favorited) { + if (favorited) { + _db.favoritePopular(popular).whenComplete(() { + setState(() {}); + }); + } else { + _db.unfavoritePopular(popular).whenComplete(() { + setState(() {}); + }); + } + }, + onPressed: (popular) { + Navigator.of(context) + .pushNamed('/popular_detail', arguments: popular); }, ); } else if (snapshot.hasError) { diff --git a/lib/screens/register_screen.dart b/lib/screens/register_screen.dart index 16875f2..ec9eb25 100644 --- a/lib/screens/register_screen.dart +++ b/lib/screens/register_screen.dart @@ -165,7 +165,7 @@ class _RegisterScreenState extends State<RegisterScreen> { ) .then((success) { if (success) { - Navigator.of(context).pushNamed('/dash'); + Navigator.of(context).pushNamed('/onboard'); } }); } diff --git a/lib/widgets/popular_item.dart b/lib/widgets/popular_item.dart index 7e8cc2c..3ef32cd 100644 --- a/lib/widgets/popular_item.dart +++ b/lib/widgets/popular_item.dart @@ -3,15 +3,45 @@ import 'package:pmsna1/models/popular.dart'; class PopularItem extends StatelessWidget { final Popular popular; + final void Function(bool favorited) onFavorited; + final void Function() onPressed; - const PopularItem(this.popular, {super.key}); + const PopularItem( + this.popular, { + super.key, + required this.onFavorited, + required this.onPressed, + }); @override Widget build(BuildContext context) { - return FadeInImage( - placeholder: const AssetImage('assets/loading.gif'), - image: NetworkImage( - 'https://image.tmdb.org/t/p/w500/${popular.posterPath!}', + return Card( + semanticContainer: true, + clipBehavior: Clip.antiAliasWithSaveLayer, + child: InkWell( + onTap: onPressed, + child: Stack( + children: [ + Hero( + tag: popular.posterPath!, + child: FadeInImage( + placeholder: const AssetImage('assets/loading.gif'), + image: NetworkImage( + 'https://image.tmdb.org/t/p/w500/${popular.posterPath!}', + ), + ), + ), + popular.hasFavorite + ? IconButton( + icon: const Icon(Icons.favorite), + onPressed: () => onFavorited(!popular.hasFavorite), + ) + : IconButton( + icon: const Icon(Icons.favorite_outline), + onPressed: () => onFavorited(!popular.hasFavorite), + ), + ], + ), ), ); } diff --git a/lib/widgets/popular_list.dart b/lib/widgets/popular_list.dart new file mode 100644 index 0000000..2422a38 --- /dev/null +++ b/lib/widgets/popular_list.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +import '../models/popular.dart'; +import 'popular_item.dart'; + +class PopularList extends StatelessWidget { + final List<Popular> popularList; + final void Function(Popular popular, bool favorited) onFavorited; + final void Function(Popular popular) onPressed; + + const PopularList( + this.popularList, { + super.key, + required this.onFavorited, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 2 / 3, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + ), + itemCount: popularList.length, + itemBuilder: (context, index) { + return PopularItem( + popularList[index], + onFavorited: (favorited) => + onFavorited(popularList[index], favorited), + onPressed: () => onPressed(popularList[index]), + ); + }, + ); + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..f6f23bf 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include <url_launcher_linux/url_launcher_plugin.h> void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..f16b4c3 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 9d1472f..d201a30 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -11,6 +11,7 @@ import firebase_core import path_provider_foundation import shared_preferences_foundation import sqflite +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) @@ -19,4 +20,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index a5e6d10..8115999 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "6a0ad72b2bcdb461749e40c01c478212a78db848dfcb2f10f2a461988bc5fb29" + sha256: "9ebe81588e666f7e2b21309f2b5653bd9642d7f27fd0a6894278d2ff40cb9481" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.3.2" archive: dependency: transitive description: @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" boolean_selector: dependency: transitive description: @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" circular_menu: dependency: "direct main" description: @@ -93,10 +93,10 @@ packages: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" concentric_transition: dependency: "direct main" description: @@ -181,42 +181,42 @@ packages: dependency: transitive description: name: firebase_auth_platform_interface - sha256: "1217d8aa313b49d58b489aa8879544563abc8793d9612ff20d8df193f202aedc" + sha256: e46e136a6f6eec88b30f12445ff7f5b19b23b7ede694921ced4f8eba8eb634f6 url: "https://pub.dev" source: hosted - version: "6.12.0" + version: "6.15.2" firebase_auth_web: dependency: transitive description: name: firebase_auth_web - sha256: bf7f1a87995a58b0f07dc617806dabd7ff25c64be7fa47b41ab1bb9a485b0062 + sha256: "553bd576d793d05b920971a2c7ab02bd049d4971153702074ea2555877efd392" url: "https://pub.dev" source: hosted - version: "5.2.10" + version: "5.5.2" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "239e4ac688674a7e7b5476fd16b0d8e2b5a453d464f32091af3ce1df4ebb7316" + sha256: e9b36b391690cf329c6fb1de220045e97c13784c303820cd33962319580a56c6 url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.13.1" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: "0df0a064ab0cad7f8836291ca6f3272edd7b83ad5b3540478ee46a0849d8022b" + sha256: b63e3be6c96ef5c33bdec1aab23c91eb00696f6452f0519401d640938c94cba2 url: "https://pub.dev" source: hosted - version: "4.6.0" + version: "4.8.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: "347351a8f0518f3343d79a9a0690fa67ad232fc32e2ea270677791949eac792b" + sha256: "8c0f4c87d20e2d001a5915df238c1f9c88704231f591324205f5a5d2a7740a45" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.5.0" flutter: dependency: "direct main" description: flutter @@ -238,6 +238,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.8" + flutter_rating_bar: + dependency: "direct main" + description: + name: flutter_rating_bar + sha256: d2af03469eac832c591a1eba47c91ecc871fe5708e69967073c043b2d775ed93 + url: "https://pub.dev" + source: hosted + version: "4.0.1" flutter_staggered_grid_view: dependency: "direct main" description: @@ -352,22 +360,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.3" + infinite_carousel: + dependency: "direct main" + description: + name: infinite_carousel + sha256: fe04c3b08adad2ee00c9bf40b46e0ff7944d206081392c4ae0f6b82c89c6e70d + url: "https://pub.dev" + source: hosted + version: "1.0.3" intl: dependency: transitive description: name: intl - sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.18.1" js: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" lints: dependency: transitive description: @@ -388,10 +404,10 @@ packages: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" url: "https://pub.dev" source: hosted - version: "0.12.13" + version: "0.12.15" material_color_utilities: dependency: transitive description: @@ -404,10 +420,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" nested: dependency: transitive description: @@ -420,10 +436,10 @@ packages: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.8.3" path_parsing: dependency: transitive description: @@ -689,10 +705,10 @@ packages: dependency: "direct main" description: name: table_calendar - sha256: "526854609e1a3df31a841f4dd307b9be7a6a21c945b88a8867dcb66fe5d84d70" + sha256: "1e3521a3e6d3fc7f645a58b135ab663d458ab12504f1ea7f9b4b81d47086c478" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.9" term_glyph: dependency: transitive description: @@ -705,10 +721,10 @@ packages: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.5.1" typed_data: dependency: transitive description: @@ -717,6 +733,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 + url: "https://pub.dev" + source: hosted + version: "6.1.11" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: eed4e6a1164aa9794409325c3b707ff424d4d1c2a785e7db67f8bbda00e36e51 + url: "https://pub.dev" + source: hosted + version: "6.0.35" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "6bb1e5d7fe53daf02a8fee85352432a40b1f868a81880e99ec7440113d5cfcab" + url: "https://pub.dev" + source: hosted + version: "2.0.17" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "254708f17f7c20a9c8c471f67d86d76d4a3f9c1591aad1e15292008aceb82771" + url: "https://pub.dev" + source: hosted + version: "3.0.6" vector_graphics: dependency: transitive description: @@ -749,6 +829,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + webview_flutter: + dependency: transitive + description: + name: webview_flutter + sha256: "5604dac1178680a34fbe4a08c7b69ec42cca6601dc300009ec9ff69bef284cc2" + url: "https://pub.dev" + source: hosted + version: "4.2.1" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "57a22c86065375c1598b57224f92d6008141be0c877c64100de8bfb6f71083d8" + url: "https://pub.dev" + source: hosted + version: "3.7.1" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "656e2aeaef318900fffd21468b6ddc7958c7092a642f0e7220bac328b70d4a81" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: "6bbc6ade302b842999b27cbaa7171241c273deea8a9c73f92ceb3d811c767de2" + url: "https://pub.dev" + source: hosted + version: "3.4.4" win32: dependency: transitive description: @@ -773,6 +885,30 @@ packages: url: "https://pub.dev" source: hosted version: "6.2.2" + youtube: + dependency: "direct main" + description: + name: youtube + sha256: "3d82e77fe8918a891c3aaba68adc14d5f4bbc6c2e368cfa100a9d620f238f9b0" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + youtube_player_iframe: + dependency: "direct main" + description: + name: youtube_player_iframe + sha256: d7aec9083430db4e5da83a3b5d7b7fcbb93cfa027d9f680ce3c7e7cd20724305 + url: "https://pub.dev" + source: hosted + version: "4.0.4" + youtube_player_iframe_web: + dependency: transitive + description: + name: youtube_player_iframe_web + sha256: c7020816031600349b56d2729d4e8be011fcb723ff7dc2dd0cdf72096a0e5ff4 + url: "https://pub.dev" + source: hosted + version: "2.0.2" sdks: - dart: ">=2.19.1 <3.0.0" + dart: ">=3.0.2 <4.0.0" flutter: ">=3.7.0-0" diff --git a/pubspec.yaml b/pubspec.yaml index e19aaa7..cf88624 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,11 +23,16 @@ dependencies: http: ^0.13.5 firebase_core: ^2.8.0 firebase_auth: ^4.3.0 - table_calendar: <=3.0.9 + table_calendar: ^3.0.9 flutter_staggered_grid_view: ^0.6.2 cloud_firestore: ^4.5.2 google_maps_flutter: ^2.2.6 circular_menu: ^2.0.1 + url_launcher: ^6.1.11 + youtube: ^1.0.1 + youtube_player_iframe: ^4.0.4 + infinite_carousel: ^1.0.3 + flutter_rating_bar: ^4.0.1 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..ec8e8d4 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,12 @@ #include "generated_plugin_registrant.h" +#include <firebase_core/firebase_core_plugin_c_api.h> +#include <url_launcher_windows/url_launcher_windows.h> void RegisterPlugins(flutter::PluginRegistry* registry) { + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..02d26c3 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,8 @@ # list(APPEND FLUTTER_PLUGIN_LIST + firebase_core + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST |