diff --git a/lib/application/bindings/application_bindings.dart b/lib/application/bindings/application_bindings.dart index be4b49f..e42e8e8 100644 --- a/lib/application/bindings/application_bindings.dart +++ b/lib/application/bindings/application_bindings.dart @@ -1,5 +1,6 @@ import 'package:get/get.dart'; +import 'package:app_filmes/application/rest_client/rest_client.dart'; import 'package:app_filmes/application/auth/auth_service.dart'; import 'package:app_filmes/repositories/login/login_repository_impl.dart'; import 'package:app_filmes/repositories/login/login_repository.dart'; @@ -9,6 +10,9 @@ import 'package:app_filmes/services/login/login_service_impl.dart'; class ApplicationBindings implements Bindings { @override void dependencies() { + Get.lazyPut( + () => RestClient(), + ); Get.lazyPut( () => LoginRepositoryImpl(), fenix: true, diff --git a/lib/application/rest_client/rest_client.dart b/lib/application/rest_client/rest_client.dart new file mode 100644 index 0000000..66dcead --- /dev/null +++ b/lib/application/rest_client/rest_client.dart @@ -0,0 +1,7 @@ +import 'package:get/get_connect.dart'; + +class RestClient extends GetConnect { + RestClient() { + httpClient.baseUrl = 'https://api.themoviedb.org/3'; + } +} diff --git a/lib/application/ui/widgets/movie_card.dart b/lib/application/ui/widgets/movie_card.dart new file mode 100644 index 0000000..f340b0c --- /dev/null +++ b/lib/application/ui/widgets/movie_card.dart @@ -0,0 +1,81 @@ +import 'package:app_filmes/application/ui/filmes_app_icons_icons.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +class MovieCard extends StatelessWidget { + const MovieCard({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 280, + width: 158, + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Material( + elevation: 2, + borderRadius: BorderRadius.circular(20), + child: ClipRRect( + clipBehavior: Clip.antiAlias, + borderRadius: BorderRadius.circular(20), + child: Image.network( + 'https://br.web.img3.acsta.net/pictures/15/09/29/12/57/543717.jpg', + width: 148, + height: 184, + fit: BoxFit.cover, + ), + ), + ), + const SizedBox(height: 20), + const Text( + 'Cristiano Ronaldo', + overflow: TextOverflow.ellipsis, + maxLines: 2, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + const Text( + '2015', + overflow: TextOverflow.ellipsis, + maxLines: 2, + style: TextStyle( + fontSize: 11, + color: Colors.grey, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + Positioned( + bottom: 80, + right: -8, + child: Material( + elevation: 5, + shape: const CircleBorder(), + clipBehavior: Clip.antiAlias, + child: SizedBox( + height: 30, + child: IconButton( + iconSize: 13, + icon: const Icon( + FilmesAppIcons.heart, + color: Colors.grey, + ), + onPressed: () {}, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/models/genre_model.dart b/lib/models/genre_model.dart new file mode 100644 index 0000000..ef33b06 --- /dev/null +++ b/lib/models/genre_model.dart @@ -0,0 +1,30 @@ +import 'dart:convert'; + +class GenreModel { + GenreModel({ + required this.id, + required this.name, + }); + + final int id; + final String name; + + Map toMap() { + return { + 'id': id, + 'name': name, + }; + } + + factory GenreModel.fromMap(Map map) { + return GenreModel( + id: map['id'] ?? 0, + name: map['name'] ?? '', + ); + } + + String toJson() => json.encode(toMap()); + + factory GenreModel.fromJson(String source) => + GenreModel.fromMap(json.decode(source)); +} diff --git a/lib/modules/home/home_page.dart b/lib/modules/home/home_page.dart index e35b917..ef21976 100644 --- a/lib/modules/home/home_page.dart +++ b/lib/modules/home/home_page.dart @@ -1,3 +1,4 @@ +import 'package:app_filmes/modules/movies/movies_bindings.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -44,6 +45,7 @@ class HomePage extends GetView { return GetPageRoute( settings: settings, page: () => const MoviesPage(), + binding: MoviesBindings(), ); } if (settings.name == '/favorites') { diff --git a/lib/modules/movies/movies_bindings.dart b/lib/modules/movies/movies_bindings.dart new file mode 100644 index 0000000..19b0db9 --- /dev/null +++ b/lib/modules/movies/movies_bindings.dart @@ -0,0 +1,28 @@ +import 'package:app_filmes/modules/movies/movies_controller.dart'; +import 'package:get/get.dart'; + +import 'package:app_filmes/repositories/genres/genres_repository.dart'; +import 'package:app_filmes/repositories/genres/genres_repository_impl.dart'; +import 'package:app_filmes/services/genres/genres_service.dart'; +import 'package:app_filmes/services/genres/genres_service_impl.dart'; + +class MoviesBindings implements Bindings { + @override + void dependencies() { + Get.lazyPut( + () => GenresRepositoryImpl( + restClient: Get.find(), + ), + ); + Get.lazyPut( + () => GenresServiceImpl( + genreRepository: Get.find(), + ), + ); + Get.lazyPut( + () => MoviesController( + genresService: Get.find(), + ), + ); + } +} diff --git a/lib/modules/movies/movies_controller.dart b/lib/modules/movies/movies_controller.dart new file mode 100644 index 0000000..c93f9ac --- /dev/null +++ b/lib/modules/movies/movies_controller.dart @@ -0,0 +1,37 @@ +import 'package:get/get.dart'; + +import 'package:app_filmes/application/ui/messages/messages_mixin.dart'; +import 'package:app_filmes/models/genre_model.dart'; +import 'package:app_filmes/services/genres/genres_service.dart'; + +class MoviesController extends GetxController with MessagesMixin { + final GenresService _genresService; + final _message = Rxn(); + final genres = [].obs; + + MoviesController({ + required GenresService genresService, + }) : _genresService = genresService; + + @override + void onInit() { + super.onInit(); + messageListener(_message); + } + + @override + void onReady() async { + super.onReady(); + try { + final genres = await _genresService.getGenres(); + this.genres.assignAll(genres); + } catch (e) { + _message( + MessageModel.error( + title: 'Erro', + message: 'Erro ao buscar Categorias', + ), + ); + } + } +} diff --git a/lib/modules/movies/movies_page.dart b/lib/modules/movies/movies_page.dart index 57b78ca..668d42a 100644 --- a/lib/modules/movies/movies_page.dart +++ b/lib/modules/movies/movies_page.dart @@ -1,16 +1,28 @@ import 'package:flutter/material.dart'; - + +import 'package:get/get.dart'; + +import 'widgets/movies_filters.dart'; +import 'widgets/movies_group.dart'; +import 'widgets/movies_header.dart'; + class MoviesPage extends StatelessWidget { const MoviesPage({ Key? key }) : super(key: key); @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Movies'), + return SizedBox( + width: Get.width, + height: Get.height, + child: ListView( + children: const [ + MoviesHeader(), + MoviesFilters(), + MoviesGroup(title: 'Mais Populares'), + MoviesGroup(title: 'Top Filmes'), + ], ), - body: Container(), ); } } diff --git a/lib/modules/movies/widgets/filter_tag.dart b/lib/modules/movies/widgets/filter_tag.dart new file mode 100644 index 0000000..ac1cf0f --- /dev/null +++ b/lib/modules/movies/widgets/filter_tag.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +import 'package:app_filmes/models/genre_model.dart'; +import 'package:app_filmes/application/ui/theme_extension.dart'; + +class FilterTag extends StatelessWidget { + final GenreModel model; + final bool selected; + final VoidCallback onPressed; + + const FilterTag({ + Key? key, + required this.model, + this.selected = false, + required this.onPressed, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onPressed, + child: Container( + margin: const EdgeInsets.all(5), + padding: const EdgeInsets.all(5), + height: 20, + constraints: const BoxConstraints( + minWidth: 100, + minHeight: 30, + ), + decoration: BoxDecoration( + color: selected ? context.themeRed : Colors.black, + borderRadius: BorderRadius.circular(30), + ), + child: Align( + alignment: Alignment.center, + child: Text( + model.name, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), + ), + ), + ); + } +} diff --git a/lib/modules/movies/widgets/movies_filters.dart b/lib/modules/movies/widgets/movies_filters.dart new file mode 100644 index 0000000..8152809 --- /dev/null +++ b/lib/modules/movies/widgets/movies_filters.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +import 'package:get/get.dart'; + +import 'package:app_filmes/modules/movies/movies_controller.dart'; +import 'filter_tag.dart'; + +class MoviesFilters extends GetView { + const MoviesFilters({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 8), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Obx(() { + return Row( + children: controller.genres + .map( + (g) => FilterTag( + model: g, + onPressed: () {}, + selected: false, + ), + ) + .toList(), + ); + }), + ), + ); + } +} diff --git a/lib/modules/movies/widgets/movies_group.dart b/lib/modules/movies/widgets/movies_group.dart new file mode 100644 index 0000000..cdaef6c --- /dev/null +++ b/lib/modules/movies/widgets/movies_group.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +import 'package:app_filmes/application/ui/widgets/movie_card.dart'; + +class MoviesGroup extends StatelessWidget { + const MoviesGroup({Key? key, required this.title}) : super(key: key); + final String title; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + Text( + title, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + SizedBox( + height: 280, + child: ListView.builder( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + itemCount: 10, + itemBuilder: (context, index) { + return const MovieCard(); + } + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/movies/widgets/movies_header.dart b/lib/modules/movies/widgets/movies_header.dart new file mode 100644 index 0000000..a36b18c --- /dev/null +++ b/lib/modules/movies/widgets/movies_header.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +import 'package:get/get.dart'; + +class MoviesHeader extends StatelessWidget { + const MoviesHeader({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: Get.width, + height: 196, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + SizedBox( + width: Get.width, + child: Image.asset( + 'assets/images/header.png', + fit: BoxFit.cover, + ), + ), + Container( + width: Get.width * .9, + padding: const EdgeInsets.only(bottom: 20), + child: TextField( + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(30), + ), + fillColor: Colors.white, + filled: true, + labelText: 'Procurar filmes', + labelStyle: const TextStyle( + fontSize: 15, + color: Colors.grey, + ), + prefixIcon: const Icon(Icons.search), + contentPadding: EdgeInsets.zero, + floatingLabelBehavior: FloatingLabelBehavior.never, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/repositories/genres/genres_repository.dart b/lib/repositories/genres/genres_repository.dart new file mode 100644 index 0000000..4987f57 --- /dev/null +++ b/lib/repositories/genres/genres_repository.dart @@ -0,0 +1,5 @@ +import 'package:app_filmes/models/genre_model.dart'; + +abstract class GenresRepository { + Future> getGenres(); +} diff --git a/lib/repositories/genres/genres_repository_impl.dart b/lib/repositories/genres/genres_repository_impl.dart new file mode 100644 index 0000000..8e3617e --- /dev/null +++ b/lib/repositories/genres/genres_repository_impl.dart @@ -0,0 +1,40 @@ +import 'package:firebase_remote_config/firebase_remote_config.dart'; + +import 'package:app_filmes/application/rest_client/rest_client.dart'; +import 'package:app_filmes/models/genre_model.dart'; +import 'genres_repository.dart'; + +class GenresRepositoryImpl implements GenresRepository { + final RestClient _restClient; + + GenresRepositoryImpl({ + required RestClient restClient, + }) : _restClient = restClient; + + @override + Future> getGenres() async { + final result = await _restClient.get>( + '/genre/movie/list', + query: { + 'api_key': RemoteConfig.instance.getString('api_token'), + 'language': 'pt-br', + }, + decoder: (data) { + final resultData = data['genres']; + if (resultData != null) { + return resultData + .map( + (genre) => GenreModel.fromMap(genre), + ) + .toList(); + } + return []; + }, + ); + if (result.hasError) { + print('Status: ${result.statusText}'); + throw Exception('Erro ao buscar Categorias'); + } + return result.body ?? []; + } +} diff --git a/lib/services/genres/genres_service.dart b/lib/services/genres/genres_service.dart new file mode 100644 index 0000000..571b6ce --- /dev/null +++ b/lib/services/genres/genres_service.dart @@ -0,0 +1,5 @@ +import 'package:app_filmes/models/genre_model.dart'; + +abstract class GenresService { + Future> getGenres(); +} diff --git a/lib/services/genres/genres_service_impl.dart b/lib/services/genres/genres_service_impl.dart new file mode 100644 index 0000000..fb8e8fd --- /dev/null +++ b/lib/services/genres/genres_service_impl.dart @@ -0,0 +1,14 @@ +import 'package:app_filmes/models/genre_model.dart'; +import 'package:app_filmes/repositories/genres/genres_repository.dart'; +import 'genres_service.dart'; + +class GenresServiceImpl implements GenresService { + final GenresRepository _genreRepository; + + GenresServiceImpl({ + required GenresRepository genreRepository, + }) : _genreRepository = genreRepository; + + @override + Future> getGenres() => _genreRepository.getGenres(); +}