From 2ec2e2f9f2ae851c73794d2a56a5c9d82a167cb9 Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Wed, 13 Nov 2024 17:05:37 -0800 Subject: [PATCH] Added detail and signin pages --- .../android/app/src/main/AndroidManifest.xml | 1 + .../.dataconnect/schema/main/input.gql | 56 +++ .../.dataconnect/schema/main/query.gql | 24 + .../dataconnect/connector/connector.yaml | 2 +- dataconnect/dataconnect/connector/queries.gql | 26 ++ .../lib/movies_connector/movies.dart | 7 + dataconnect/dataconnect/schema/schema.gql | 7 + dataconnect/firebase.json | 8 + dataconnect/lib/actor_detail.dart | 93 ++++ dataconnect/lib/destination.dart | 22 + dataconnect/lib/genre_list_movies.dart | 133 ++++++ dataconnect/lib/genre_page.dart | 63 +++ dataconnect/lib/login.dart | 100 ++++ dataconnect/lib/main.dart | 32 +- dataconnect/lib/movie_detail.dart | 141 +++--- .../lib/movies_connector/list_genres.dart | 126 ++++++ .../list_movies_by_genre.dart | 427 ++++++++++++++++++ dataconnect/lib/movies_connector/movies.dart | 14 + dataconnect/lib/navigation_shell.dart | 34 ++ dataconnect/lib/profile.dart | 57 +++ dataconnect/lib/router.dart | 78 ++++ dataconnect/lib/sign_up.dart | 123 +++++ dataconnect/lib/util/auth.dart | 12 + dataconnect/pubspec.lock | 22 +- dataconnect/pubspec.yaml | 1 + 25 files changed, 1510 insertions(+), 99 deletions(-) create mode 100644 dataconnect/lib/actor_detail.dart create mode 100644 dataconnect/lib/destination.dart create mode 100644 dataconnect/lib/genre_list_movies.dart create mode 100644 dataconnect/lib/genre_page.dart create mode 100644 dataconnect/lib/login.dart create mode 100644 dataconnect/lib/movies_connector/list_genres.dart create mode 100644 dataconnect/lib/movies_connector/list_movies_by_genre.dart create mode 100644 dataconnect/lib/navigation_shell.dart create mode 100644 dataconnect/lib/profile.dart create mode 100644 dataconnect/lib/router.dart create mode 100644 dataconnect/lib/sign_up.dart create mode 100644 dataconnect/lib/util/auth.dart diff --git a/dataconnect/android/app/src/main/AndroidManifest.xml b/dataconnect/android/app/src/main/AndroidManifest.xml index 8ab02cb..136e48a 100644 --- a/dataconnect/android/app/src/main/AndroidManifest.xml +++ b/dataconnect/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ createState() => _ActorDetailState(); +} + +class _ActorDetailState extends State { + bool loading = true; + GetActorByIdActor? actor; + @override + void initState() { + super.initState(); + MoviesConnector.instance + .getActorById(id: widget.actorId) + .execute() + .then((value) { + setState(() { + loading = false; + actor = value.data.actor; + }); + }); + } + + _buildActorInfo() { + return [ + Align( + alignment: Alignment.centerLeft, + child: Container( + child: Text( + actor!.name, + style: const TextStyle(fontSize: 30), + ), + )), + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: AspectRatio( + aspectRatio: 9 / 16, // 9:16 aspect ratio for the image + child: Image.network( + actor!.imageUrl, + fit: BoxFit.cover, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(15, 0, 0, 0), + // child: Column(children: [Text(actor!.info!)]), + )) + ]), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // TODO(mtewani): Check if the movie has been watched by the user + OutlinedButton.icon( + onPressed: () { + // TODO(mtewani): Check if user is logged in. + }, + icon: const Icon(Icons.favorite), + label: const Text('Add To Favorites'), + ) + ], + ) + ]; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: actor == null + ? const Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [CircularProgressIndicator()], + ) + : Padding( + padding: const EdgeInsets.all(30), + child: SingleChildScrollView( + child: Column( + children: [..._buildActorInfo()], + )))), + ); + } +} diff --git a/dataconnect/lib/destination.dart b/dataconnect/lib/destination.dart new file mode 100644 index 0000000..f3d04d4 --- /dev/null +++ b/dataconnect/lib/destination.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class Destination { + const Destination({required this.label, required this.icon}); + final String label; + final IconData icon; +} + +class Route { + Route({required this.path, required this.label, required this.iconData}); + final String path; + final String label; + final IconData iconData; +} + +var homePath = Route(path: '/home', label: 'Home', iconData: Icons.home); +var genrePath = Route(path: '/genres', label: 'Genres', iconData: Icons.list); +var searchPath = + Route(path: '/search', label: 'Search', iconData: Icons.search); +var profilePath = + Route(path: '/profile', label: 'Profile', iconData: Icons.person); +var paths = [homePath, genrePath, searchPath, profilePath]; diff --git a/dataconnect/lib/genre_list_movies.dart b/dataconnect/lib/genre_list_movies.dart new file mode 100644 index 0000000..db43fb0 --- /dev/null +++ b/dataconnect/lib/genre_list_movies.dart @@ -0,0 +1,133 @@ +import 'package:dataconnect/movies_connector/movies.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class GenreListMovies extends StatefulWidget { + const GenreListMovies({super.key, required this.genre}); + final String genre; + + @override + State createState() => _GenreListMoviesState(); +} + +class _GenreListMoviesState extends State { + bool loading = true; + List _mostPopular = []; + List _mostRecent = []; + + void _visitDetail(String id) { + context.push("/movies/$id"); + } + + Widget _buildMovieList(String title, List movies) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + title, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ), + SizedBox( + height: 300, // Adjust the height as needed + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: movies.length, + itemBuilder: (context, index) { + return _buildMovieItem(movies[index]); + }, + ), + ), + ], + ); + } + + Widget _buildMovieItem(ListMoviesByGenreMovies movie) { + return Container( + width: 150, // Adjust the width as needed + padding: const EdgeInsets.all(4.0), + child: Card( + child: InkWell( + onTap: () { + _visitDetail(movie.id); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: 9 / 16, // 9:16 aspect ratio for the image + child: Image.network( + movie.imageUrl, + fit: BoxFit.cover, + ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + movie.title, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + ], + )), + ), + ); + } + + @override + void initState() { + super.initState(); + String genre = widget.genre; + MoviesConnector.instance + .listMoviesByGenre(genre: genre) + .orderByRating(OrderDirection.DESC) + .limit(10) + .execute() + .then((res) { + setState(() { + _mostPopular = res.data.movies; + }); + }); + + MoviesConnector.instance + .listMoviesByGenre(genre: genre) + .orderByReleaseYear(OrderDirection.DESC) + .execute() + .then((res) { + setState(() { + _mostRecent = res.data.movies; + }); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: _mostPopular.isEmpty && _mostRecent.isEmpty + ? const Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [CircularProgressIndicator()], + ) + : SingleChildScrollView( + padding: const EdgeInsets.all(30), + child: Column( + children: [ + Text( + "${widget.genre} Movies", + style: const TextStyle( + fontSize: 20, fontWeight: FontWeight.bold), + ), + _buildMovieList("Most Popular", _mostPopular), + _buildMovieList("Most Recent", _mostRecent) + ], + ), + )), + ); + } +} diff --git a/dataconnect/lib/genre_page.dart b/dataconnect/lib/genre_page.dart new file mode 100644 index 0000000..7838e40 --- /dev/null +++ b/dataconnect/lib/genre_page.dart @@ -0,0 +1,63 @@ +import 'package:dataconnect/movies_connector/movies.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class GenrePage extends StatefulWidget { + const GenrePage({super.key}); + + @override + State createState() => _GenrePageState(); +} + +class _GenrePageState extends State { + List genres = []; + bool loading = true; + @override + void initState() { + super.initState(); + MoviesConnector.instance.listGenres().execute().then((genres) { + setState(() { + loading = false; + this.genres = + genres.data.genres.map((genreData) => genreData.genre!).toList(); + }); + }); + } + + Widget _buildGenreList() { + return ListView.builder( + itemBuilder: (context, index) { + String genre = genres[index]; + return ListTile( + title: TextButton( + child: Text(genre, style: TextStyle(fontSize: 30)), + onPressed: () { + context.push('/genres/${genre}'); + }, + ), + ); + }, + itemCount: genres.length, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: loading + ? const Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [CircularProgressIndicator()], + ) + : Padding( + padding: const EdgeInsets.all(30), child: _buildGenreList() + // ..._buildMainDescription(), + // _buildMainActorsList(), + // _buildSupportingActorsList(), + // ..._buildRatings() + )), + ); + } +} diff --git a/dataconnect/lib/login.dart b/dataconnect/lib/login.dart new file mode 100644 index 0000000..3817a85 --- /dev/null +++ b/dataconnect/lib/login.dart @@ -0,0 +1,100 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class Login extends StatefulWidget { + const Login({super.key}); + + @override + State createState() => _LoginState(); +} + +class _LoginState extends State { + final _formKey = GlobalKey(); + String _username = ''; + String _password = ''; + Widget _buildForm() { + return Form( + key: _formKey, + child: Column( + children: [ + TextFormField( + decoration: const InputDecoration( + hintText: "Username", border: OutlineInputBorder()), + onChanged: (value) { + setState(() { + _username = value; + }); + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter some text'; + } + return null; + }, + ), + const SizedBox(height: 30), + TextFormField( + decoration: const InputDecoration( + hintText: "Password", border: OutlineInputBorder()), + onChanged: (value) { + setState(() { + _password = value; + }); + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a password'; + } + + return null; + }, + ), + ElevatedButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + logIn(); + } + }, + child: const Text('Sign In')), + Text('Don\'t have an account?'), + ElevatedButton( + onPressed: () { + context.go('/signup'); + }, + child: const Text('Sign Up')), + ], + )); + } + + logIn() async { + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text('Signing In'))); + try { + await FirebaseAuth.instance + .signInWithEmailAndPassword(email: _username, password: _password); + if (mounted) { + context.go('/home'); + } + } catch (_) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('There was an error when creating a user.'))); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Padding( + padding: EdgeInsets.all(30), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [_buildForm()], + ))), + ); + } +} diff --git a/dataconnect/lib/main.dart b/dataconnect/lib/main.dart index 4709ac9..eb3e599 100644 --- a/dataconnect/lib/main.dart +++ b/dataconnect/lib/main.dart @@ -1,36 +1,19 @@ -import 'dart:async'; - -import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:dataconnect/router.dart'; +import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:go_router/go_router.dart'; import 'firebase_options.dart'; -import 'movie_detail.dart'; import 'movies_connector/movies.dart'; -final _router = GoRouter( - routes: [ - GoRoute( - path: '/', - builder: (context, state) => MyHomePage(), - ), - GoRoute( - path: '/movies/:movieId', - builder: (context, state) => - MovieDetail(id: state.pathParameters['movieId']!), - ) - ], - redirect: (context, state) { - print(state.path); - return null; - }, -); - void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); + MoviesConnector.instance.dataConnect + .useDataConnectEmulator('localhost', 9399); + FirebaseAuth.instance.useAuthEmulator('localhost', 9400); runApp(const MyApp()); } @@ -42,7 +25,7 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp.router( theme: ThemeData.dark(), - routerConfig: _router, + routerConfig: router, ); } } @@ -73,8 +56,7 @@ class _MyHomePageState extends State { /// TODO: Uncomment the following lines to update the movies state when data /// comes back from the server. - MoviesConnector.instance.dataConnect - .useDataConnectEmulator('localhost', 9399); + MoviesConnector.instance .listMovies() .orderByRating(OrderDirection.DESC) diff --git a/dataconnect/lib/movie_detail.dart b/dataconnect/lib/movie_detail.dart index 8adf782..90810f2 100644 --- a/dataconnect/lib/movie_detail.dart +++ b/dataconnect/lib/movie_detail.dart @@ -1,5 +1,6 @@ import 'package:dataconnect/movies_connector/movies.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; class MovieDetail extends StatefulWidget { @@ -8,19 +9,20 @@ class MovieDetail extends StatefulWidget { final String id; @override - State createState() => _MovieDetailState(id: this.id); + State createState() => _MovieDetailState(); } class _MovieDetailState extends State { - _MovieDetailState({required this.id}); - String id; double _ratingValue = 0; bool loading = true; GetMovieByIdData? data; @override void initState() { super.initState(); - MoviesConnector.instance.getMovieById(id: id).execute().then((value) { + MoviesConnector.instance + .getMovieById(id: widget.id) + .execute() + .then((value) { setState(() { loading = false; data = value.data; @@ -97,69 +99,84 @@ class _MovieDetailState extends State { ]; } + void _visitActorDetail(String id) { + context.push("/actors/$id"); + } + Widget _buildMainActorsList() { return Container( + height: 125, child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Main Actors", - style: TextStyle(fontSize: 30), - ), - ListView.builder( - scrollDirection: Axis.vertical, - shrinkWrap: true, - itemBuilder: (context, index) { - GetMovieByIdMovieMainActors actor = data!.movie!.mainActors[index]; - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CircleAvatar( - radius: 30, - child: ClipOval(child: Image.network(actor.imageUrl))), - Text(actor - .name) // TODO(mtewani): Clip if there's not enough width - ]); - }, - itemCount: data!.movie!.mainActors.length, - ) - ], - )); + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Main Actors", + style: TextStyle(fontSize: 30), + ), + Expanded( + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + GetMovieByIdMovieMainActors actor = + data!.movie!.mainActors[index]; + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CircleAvatar( + radius: 30, + child: + ClipOval(child: Image.network(actor.imageUrl))), + Text(actor + .name) // TODO(mtewani): Clip if there's not enough width + ]); + }, + itemCount: data!.movie!.mainActors.length, + )) + ], + )); } Widget _buildSupportingActorsList() { return Container( + height: 125, child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Supporting Actors", - style: TextStyle(fontSize: 30), - ), - ListView.builder( - scrollDirection: Axis.vertical, - shrinkWrap: true, - itemBuilder: (context, index) { - GetMovieByIdMovieSupportingActors actor = - data!.movie!.supportingActors[index]; - return Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CircleAvatar( - radius: 30, - child: ClipOval(child: Image.network(actor.imageUrl))), - Text(actor - .name) // TODO(mtewani): Clip if there's not enough width - ]); - }, - itemCount: data!.movie!.mainActors.length, - ) - ], - )); + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Supporting Actors", + style: TextStyle(fontSize: 30), + ), + Expanded( + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + GetMovieByIdMovieSupportingActors actor = + data!.movie!.supportingActors[index]; + return InkWell( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CircleAvatar( + radius: 30, + child: ClipOval( + child: Image.network(actor.imageUrl))), + Text(actor + .name) // TODO(mtewani): Clip if there's not enough width + ]), + onTap: () { + _visitActorDetail(actor.id); + }, + ); + }, + itemCount: data!.movie!.mainActors.length, + ), + ) + ], + )); } List _buildRatings() { @@ -176,7 +193,7 @@ class _MovieDetailState extends State { }); }, ), - TextField( + const TextField( decoration: InputDecoration( hintText: "Write your review", border: OutlineInputBorder(), @@ -191,7 +208,7 @@ class _MovieDetailState extends State { ...data!.movie!.reviews.map((review) { return Card( child: Padding( - padding: EdgeInsets.all(20.0), + padding: const EdgeInsets.all(20.0), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, @@ -200,7 +217,7 @@ class _MovieDetailState extends State { Row( children: [ Text(DateFormat.yMMMd().format(review.reviewDate)), - SizedBox( + const SizedBox( width: 10, ), Text("Rating ${review.rating}") diff --git a/dataconnect/lib/movies_connector/list_genres.dart b/dataconnect/lib/movies_connector/list_genres.dart new file mode 100644 index 0000000..dd83467 --- /dev/null +++ b/dataconnect/lib/movies_connector/list_genres.dart @@ -0,0 +1,126 @@ +part of movies_connector; + +class ListGenresVariablesBuilder { + + + FirebaseDataConnect _dataConnect; + + ListGenresVariablesBuilder(this._dataConnect, ); + Deserializer dataDeserializer = (dynamic json) => ListGenresData.fromJson(jsonDecode(json)); + + Future> execute() { + return this.ref().execute(); + } + QueryRef ref() { + + return _dataConnect.query("ListGenres", dataDeserializer, emptySerializer, null); + } +} + + + class ListGenresGenres { + + String? genre; + + + + + + + ListGenresGenres.fromJson(dynamic json): + genre = json['genre'] == null ? null : + + nativeFromJson(json['genre']) + + + + + { + + + + } + + + Map toJson() { + Map json = {}; + + + if (genre != null) { + json['genre'] = + + nativeToJson(genre) + +; + } + + + return json; + } + + ListGenresGenres({ + + this.genre, + + }); +} + + + + class ListGenresData { + + List genres; + + + + + + + ListGenresData.fromJson(dynamic json): + genres = + + + (json['genres'] as List) + .map((e) => ListGenresGenres.fromJson(e)) + .toList() + + + + + + { + + + + } + + + Map toJson() { + Map json = {}; + + + json['genres'] = + + + genres.map((e) => e.toJson()).toList() + + +; + + + return json; + } + + ListGenresData({ + + required this.genres, + + }); +} + + + + + + + diff --git a/dataconnect/lib/movies_connector/list_movies_by_genre.dart b/dataconnect/lib/movies_connector/list_movies_by_genre.dart new file mode 100644 index 0000000..a0bfb78 --- /dev/null +++ b/dataconnect/lib/movies_connector/list_movies_by_genre.dart @@ -0,0 +1,427 @@ +part of movies_connector; + +class ListMoviesByGenreVariablesBuilder { + String genre; +Optional _orderByRating = Optional.optional(orderDirectionDeserializer, enumSerializer); +Optional _orderByReleaseYear = Optional.optional(orderDirectionDeserializer, enumSerializer); +Optional _limit = Optional.optional(nativeFromJson, nativeToJson); + + + FirebaseDataConnect _dataConnect; + ListMoviesByGenreVariablesBuilder orderByRating(OrderDirection? t) { +this._orderByRating.value = t; +return this; +} +ListMoviesByGenreVariablesBuilder orderByReleaseYear(OrderDirection? t) { +this._orderByReleaseYear.value = t; +return this; +} +ListMoviesByGenreVariablesBuilder limit(int? t) { +this._limit.value = t; +return this; +} + + ListMoviesByGenreVariablesBuilder(this._dataConnect, {required String this.genre,}); + Deserializer dataDeserializer = (dynamic json) => ListMoviesByGenreData.fromJson(jsonDecode(json)); + Serializer varsSerializer = (ListMoviesByGenreVariables vars) => jsonEncode(vars.toJson()); + Future> execute() { + return this.ref().execute(); + } + QueryRef ref() { + ListMoviesByGenreVariables vars=ListMoviesByGenreVariables(genre: genre,orderByRating: _orderByRating,orderByReleaseYear: _orderByReleaseYear,limit: _limit,); + + return _dataConnect.query("ListMoviesByGenre", dataDeserializer, varsSerializer, vars); + } +} + + + class ListMoviesByGenreMovies { + + String id; + + + String title; + + + String imageUrl; + + + int? releaseYear; + + + String? genre; + + + double? rating; + + + List? tags; + + + String? description; + + + + + + + ListMoviesByGenreMovies.fromJson(dynamic json): + id = + + nativeFromJson(json['id']) + + + + , + + title = + + nativeFromJson(json['title']) + + + + , + + imageUrl = + + nativeFromJson(json['imageUrl']) + + + + , + + releaseYear = json['releaseYear'] == null ? null : + + nativeFromJson(json['releaseYear']) + + + + , + + genre = json['genre'] == null ? null : + + nativeFromJson(json['genre']) + + + + , + + rating = json['rating'] == null ? null : + + nativeFromJson(json['rating']) + + + + , + + tags = json['tags'] == null ? null : + + + (json['tags'] as List) + .map((e) => nativeFromJson(e)) + .toList() + + + + + , + + description = json['description'] == null ? null : + + nativeFromJson(json['description']) + + + + + { + + + + + + + + + + + + + + + + + + } + + + Map toJson() { + Map json = {}; + + + json['id'] = + + nativeToJson(id) + +; + + + + json['title'] = + + nativeToJson(title) + +; + + + + json['imageUrl'] = + + nativeToJson(imageUrl) + +; + + + + if (releaseYear != null) { + json['releaseYear'] = + + nativeToJson(releaseYear) + +; + } + + + + if (genre != null) { + json['genre'] = + + nativeToJson(genre) + +; + } + + + + if (rating != null) { + json['rating'] = + + nativeToJson(rating) + +; + } + + + + if (tags != null) { + json['tags'] = + + + tags?.map((e) => nativeToJson(e)).toList() + + +; + } + + + + if (description != null) { + json['description'] = + + nativeToJson(description) + +; + } + + + return json; + } + + ListMoviesByGenreMovies({ + + required this.id, + + required this.title, + + required this.imageUrl, + + this.releaseYear, + + this.genre, + + this.rating, + + this.tags, + + this.description, + + }); +} + + + + class ListMoviesByGenreData { + + List movies; + + + + + + + ListMoviesByGenreData.fromJson(dynamic json): + movies = + + + (json['movies'] as List) + .map((e) => ListMoviesByGenreMovies.fromJson(e)) + .toList() + + + + + + { + + + + } + + + Map toJson() { + Map json = {}; + + + json['movies'] = + + + movies.map((e) => e.toJson()).toList() + + +; + + + return json; + } + + ListMoviesByGenreData({ + + required this.movies, + + }); +} + + + + class ListMoviesByGenreVariables { + + String genre; + + + late OptionalorderByRating; + + + late OptionalorderByReleaseYear; + + + late Optionallimit; + + + + + + @Deprecated('fromJson is deprecated for Variable classes as they are no longer required for deserialization.') + + + ListMoviesByGenreVariables.fromJson(Map json): + genre = + + nativeFromJson(json['genre']) + + + + + { + + + + + orderByRating = Optional.optional(orderDirectionDeserializer, enumSerializer); + orderByRating.value = json['orderByRating'] == null ? null : + + OrderDirection.values.byName(json['orderByRating']) + +; + + + + orderByReleaseYear = Optional.optional(orderDirectionDeserializer, enumSerializer); + orderByReleaseYear.value = json['orderByReleaseYear'] == null ? null : + + OrderDirection.values.byName(json['orderByReleaseYear']) + +; + + + + limit = Optional.optional(nativeFromJson, nativeToJson); + limit.value = json['limit'] == null ? null : + + nativeFromJson(json['limit']) + +; + + + } + + + Map toJson() { + Map json = {}; + + + json['genre'] = + + nativeToJson(genre) + +; + + + + if(orderByRating.state == OptionalState.set) { + json['orderByRating'] = orderByRating.toJson(); + } + + + + if(orderByReleaseYear.state == OptionalState.set) { + json['orderByReleaseYear'] = orderByReleaseYear.toJson(); + } + + + + if(limit.state == OptionalState.set) { + json['limit'] = limit.toJson(); + } + + + return json; + } + + ListMoviesByGenreVariables({ + + required this.genre, + + required this.orderByRating, + + required this.orderByReleaseYear, + + required this.limit, + + }); +} + + + + + + + diff --git a/dataconnect/lib/movies_connector/movies.dart b/dataconnect/lib/movies_connector/movies.dart index b7fbdaa..cf9114d 100644 --- a/dataconnect/lib/movies_connector/movies.dart +++ b/dataconnect/lib/movies_connector/movies.dart @@ -16,6 +16,10 @@ part 'delete_review.dart'; part 'list_movies.dart'; +part 'list_movies_by_genre.dart'; + +part 'list_genres.dart'; + part 'get_movie_by_id.dart'; part 'get_actor_by_id.dart'; @@ -85,6 +89,16 @@ class MoviesConnector { } + ListMoviesByGenreVariablesBuilder listMoviesByGenre ({required String genre,}) { + return ListMoviesByGenreVariablesBuilder(dataConnect, genre: genre,); + } + + + ListGenresVariablesBuilder listGenres () { + return ListGenresVariablesBuilder(dataConnect, ); + } + + GetMovieByIdVariablesBuilder getMovieById ({required String id,}) { return GetMovieByIdVariablesBuilder(dataConnect, id: id,); } diff --git a/dataconnect/lib/navigation_shell.dart b/dataconnect/lib/navigation_shell.dart new file mode 100644 index 0000000..f73b02e --- /dev/null +++ b/dataconnect/lib/navigation_shell.dart @@ -0,0 +1,34 @@ +import 'package:dataconnect/destination.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class NavigationShell extends StatelessWidget { + const NavigationShell({required this.navigationShell, super.key}); + final StatefulNavigationShell navigationShell; + + void _goBranch(int index) { + navigationShell.goBranch( + index, + // A common pattern when using bottom navigation bars is to support + // navigating to the initial location when tapping the item that is + // already active. This example demonstrates how to support this behavior, + // using the initialLocation parameter of goBranch. + initialLocation: index == navigationShell.currentIndex, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: navigationShell, + bottomNavigationBar: NavigationBar( + selectedIndex: navigationShell.currentIndex, + onDestinationSelected: _goBranch, + destinations: paths + .map((destination) => NavigationDestination( + icon: Icon(destination.iconData), label: destination.label)) + .toList(), + ), + ); + } +} diff --git a/dataconnect/lib/profile.dart b/dataconnect/lib/profile.dart new file mode 100644 index 0000000..1a7768f --- /dev/null +++ b/dataconnect/lib/profile.dart @@ -0,0 +1,57 @@ +import 'package:dataconnect/main.dart'; +import 'package:dataconnect/util/auth.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class Profile extends StatefulWidget { + const Profile({super.key}); + + @override + State createState() => _ProfileState(); +} + +class _ProfileState extends State { + User? _currentUser; + @override + void initState() { + super.initState(); + Auth.getCurrentUser().then((user) { + setState(() { + _currentUser = user; + }); + }); + } + + Widget _UserInfo() { + return Container( + child: Column( + children: [ + Text('Welcome back ${_currentUser!.displayName ?? ''}!'), + TextButton( + onPressed: () async { + FirebaseAuth.instance.signOut(); + context.go('/login'); + }, + child: Text('Sign out')) + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return _currentUser == null + ? const Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [CircularProgressIndicator()], + ) + : Container( + padding: EdgeInsets.all(30), + child: Column( + children: [_UserInfo()], + ), + ); + } +} diff --git a/dataconnect/lib/router.dart b/dataconnect/lib/router.dart new file mode 100644 index 0000000..f639a8b --- /dev/null +++ b/dataconnect/lib/router.dart @@ -0,0 +1,78 @@ +import 'package:dataconnect/destination.dart'; +import 'package:dataconnect/login.dart'; +import 'package:dataconnect/profile.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import 'actor_detail.dart'; +import 'genre_list_movies.dart'; +import 'genre_page.dart'; +import 'main.dart'; +import 'movie_detail.dart'; +import 'navigation_shell.dart'; +import 'sign_up.dart'; +import 'util/auth.dart'; + +var router = GoRouter(initialLocation: homePath.path, routes: [ + StatefulShellRoute.indexedStack( + builder: (context, state, navigationShell) { + return NavigationShell(navigationShell: navigationShell); + }, + branches: [ + StatefulShellBranch(routes: [ + GoRoute( + path: homePath.path, + builder: (context, state) => const MyHomePage(), + ), + GoRoute( + path: '/movies/:movieId', + builder: (context, state) => + MovieDetail(id: state.pathParameters['movieId']!)) + ]), + StatefulShellBranch( + routes: [ + GoRoute( + path: genrePath.path, + builder: (context, state) => const GenrePage(), + ), + GoRoute( + path: "/genres/:genre", + builder: (context, state) => + GenreListMovies(genre: state.pathParameters['genre']!), + ), + GoRoute( + path: "/actors", + redirect: (context, state) => + '/actors/${state.pathParameters['actorId']}', + routes: [ + GoRoute( + path: ":actorId", + builder: (context, state) => ActorDetail( + actorId: state.pathParameters['actorId']!)) + ]) + ], + ), + StatefulShellBranch(routes: [ + GoRoute( + path: "/search", + builder: (context, state) => const Text('abc'), + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: "/profile", + redirect: (context, state) async { + return (await Auth.isLoggedIn()) ? null : '/login'; + }, + builder: (context, state) => const Profile()), + GoRoute( + path: "/login", + builder: (context, state) => const Login(), + ), + GoRoute( + path: "/signup", + builder: (context, state) => const SignUp(), + ) + ]) + ]) +]); diff --git a/dataconnect/lib/sign_up.dart b/dataconnect/lib/sign_up.dart new file mode 100644 index 0000000..676f138 --- /dev/null +++ b/dataconnect/lib/sign_up.dart @@ -0,0 +1,123 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class SignUp extends StatefulWidget { + const SignUp({super.key}); + + @override + State createState() => _SignUpState(); +} + +class _SignUpState extends State { + final _formKey = GlobalKey(); + String _username = ''; + String _password = ''; + String _confirmPassword = ''; + Widget _buildForm() { + return Form( + key: _formKey, + child: Column( + children: [ + TextFormField( + decoration: const InputDecoration( + hintText: "Email", border: OutlineInputBorder()), + onChanged: (value) { + setState(() { + _username = value; + }); + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter some text'; + } + return null; + }, + ), + const SizedBox(height: 30), + TextFormField( + obscureText: true, + enableSuggestions: false, + autocorrect: false, + decoration: const InputDecoration( + hintText: "Password", border: OutlineInputBorder()), + onChanged: (value) { + setState(() { + _password = value; + }); + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a password'; + } + if (value != _confirmPassword) { + return 'Passwords do not match'; + } + return null; + }, + ), + const SizedBox(height: 30), + TextFormField( + obscureText: true, + enableSuggestions: false, + autocorrect: false, + decoration: const InputDecoration( + hintText: "Confirm Password", border: OutlineInputBorder()), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter some text'; + } + if (value != _password) { + return 'Passwords do not match'; + } + return null; + }, + onChanged: (value) { + setState(() { + _confirmPassword = value; + }); + }, + ), + ElevatedButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + signUp(); + } + }, + child: Text('Submit')) + ], + )); + } + + signUp() async { + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text('Signing Up'))); + try { + await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: _username, password: _password); + if (mounted) { + context.go('/home'); + } + } catch (e) { + print(e); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('There was an error when creating a user.'))); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Padding( + padding: EdgeInsets.all(30), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [_buildForm()], + ))), + ); + } +} diff --git a/dataconnect/lib/util/auth.dart b/dataconnect/lib/util/auth.dart new file mode 100644 index 0000000..b31c140 --- /dev/null +++ b/dataconnect/lib/util/auth.dart @@ -0,0 +1,12 @@ +import 'package:firebase_auth/firebase_auth.dart'; + +class Auth { + static isLoggedIn() async { + User? user = await FirebaseAuth.instance.authStateChanges().first; + return user != null; + } + + static getCurrentUser() { + return FirebaseAuth.instance.authStateChanges().first; + } +} diff --git a/dataconnect/pubspec.lock b/dataconnect/pubspec.lock index b774782..5493882 100644 --- a/dataconnect/pubspec.lock +++ b/dataconnect/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "5534e701a2c505fed1f0799e652dd6ae23bd4d2c4cf797220e5ced5764a7c1c2" + sha256: "71c01c1998c40b3af1944ad0a5f374b4e6fef7f3d2df487f3970dbeadaeb25a1" url: "https://pub.dev" source: hosted - version: "1.3.44" + version: "1.3.46" archive: dependency: transitive description: @@ -114,37 +114,37 @@ packages: source: hosted version: "0.2.0" firebase_auth: - dependency: transitive + dependency: "direct main" description: name: firebase_auth - sha256: d453acec0d958ba0e25d41a9901b32cb77d1535766903dea7a61b2788c304596 + sha256: "49c356bac95ed234805e3bb928a86d5b21a4d3745d77be53ecf2d61409ddb802" url: "https://pub.dev" source: hosted - version: "5.3.1" + version: "5.3.3" firebase_auth_platform_interface: dependency: transitive description: name: firebase_auth_platform_interface - sha256: "78966c2ef774f5bf2a8381a307222867e9ece3509110500f7a138c115926aa65" + sha256: "9bc336ce673ea90a9dbdb04f0e9a3e52a32321898dc869cdefe6cc0f0db369ed" url: "https://pub.dev" source: hosted - version: "7.4.7" + version: "7.4.9" firebase_auth_web: dependency: transitive description: name: firebase_auth_web - sha256: "77ad3b252badedd3f08dfa21a4c7fe244be96c6da3a4067f253b13ea5d32424c" + sha256: "56dcce4293e2a2c648c33ab72c09e888bd0e64cbb1681a32575ec9dc9c2f67f3" url: "https://pub.dev" source: hosted - version: "5.13.2" + version: "5.13.4" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "51dfe2fbf3a984787a2e7b8592f2f05c986bfedd6fdacea3f9e0a7beb334de96" + sha256: "2438a75ad803e818ad3bd5df49137ee619c46b6fc7101f4dbc23da07305ce553" url: "https://pub.dev" source: hosted - version: "3.6.0" + version: "3.8.0" firebase_core_platform_interface: dependency: transitive description: diff --git a/dataconnect/pubspec.yaml b/dataconnect/pubspec.yaml index 5ff5570..04c8f25 100644 --- a/dataconnect/pubspec.yaml +++ b/dataconnect/pubspec.yaml @@ -39,6 +39,7 @@ dependencies: firebase_core: ^3.6.0 go_router: ^14.5.0 intl: ^0.19.0 + firebase_auth: ^5.3.3 dev_dependencies: flutter_test: