diff --git a/assets/icon.jpg b/assets/icon.jpg deleted file mode 100644 index 9d71906..0000000 Binary files a/assets/icon.jpg and /dev/null differ diff --git a/lib/data/dtos/movies_dto.dart b/lib/data/dtos/movies_dto.dart index 25ae0e8..3aec70f 100644 --- a/lib/data/dtos/movies_dto.dart +++ b/lib/data/dtos/movies_dto.dart @@ -31,21 +31,53 @@ class MoviePaginationDto { @JsonSerializable(createToJson: false) class MovieDataDto { @JsonKey(name: 'id') - final int? id; // Это поле может использоваться для идентификации фильма - final String? name; // Название фильма - final String? description; // Описание фильма - final PosterDto? poster; // Постер фильма + final int? id; + final String? name; + final String? description; + final PosterDto? poster; + @JsonKey(name: 'year', defaultValue: 0) + final int year; + final List? genres; + final List? countries; - const MovieDataDto(this.name, this.description, this.poster, {this.id}); + const MovieDataDto( + this.name, + this.description, + this.poster, { + this.id, + this.year = 0, + this.genres, + this.countries, + }); factory MovieDataDto.fromJson(Map json) => _$MovieDataDtoFromJson(json); } +@JsonSerializable(createToJson: false) +class GenreDto { + final String? name; + + const GenreDto({this.name}); + + factory GenreDto.fromJson(Map json) => + _$GenreDtoFromJson(json); +} + +@JsonSerializable(createToJson: false) +class CountryDto { + final String? name; + + const CountryDto({this.name}); + + factory CountryDto.fromJson(Map json) => + _$CountryDtoFromJson(json); +} + @JsonSerializable(createToJson: false) class PosterDto { - final String? url; // URL постера - final String? previewUrl; // URL миниатюры постера + final String? url; + final String? previewUrl; const PosterDto({this.url, this.previewUrl}); diff --git a/lib/data/dtos/movies_dto.g.dart b/lib/data/dtos/movies_dto.g.dart index b328948..6208385 100644 --- a/lib/data/dtos/movies_dto.g.dart +++ b/lib/data/dtos/movies_dto.g.dart @@ -30,6 +30,21 @@ MovieDataDto _$MovieDataDtoFromJson(Map json) => MovieDataDto( ? null : PosterDto.fromJson(json['poster'] as Map), id: (json['id'] as num?)?.toInt(), + year: (json['year'] as num?)?.toInt() ?? 0, + genres: (json['genres'] as List?) + ?.map((e) => GenreDto.fromJson(e as Map)) + .toList(), + countries: (json['countries'] as List?) + ?.map((e) => CountryDto.fromJson(e as Map)) + .toList(), + ); + +GenreDto _$GenreDtoFromJson(Map json) => GenreDto( + name: json['name'] as String?, + ); + +CountryDto _$CountryDtoFromJson(Map json) => CountryDto( + name: json['name'] as String?, ); PosterDto _$PosterDtoFromJson(Map json) => PosterDto( diff --git a/lib/data/mappers/movies_mapper.dart b/lib/data/mappers/movies_mapper.dart index 366ef38..21000c6 100644 --- a/lib/data/mappers/movies_mapper.dart +++ b/lib/data/mappers/movies_mapper.dart @@ -2,18 +2,24 @@ import 'package:pmd_labs/data/dtos/movies_dto.dart'; import 'package:pmd_labs/domain/models/carddata.dart'; import 'package:pmd_labs/presentation/home_page/home_page.dart'; +const _imagePlaceholder = + 'https://upload.wikimedia.org/wikipedia/en/archive/b/b1/20210811082420%21Portrait_placeholder.png'; + extension MovieDataDtoMapper on MovieDataDto { CardData toDomain() => CardData( - name ?? 'UNKNOWN', // Исправлено с title на name - imageUrl: poster?.url, // Обратите внимание, что используем правильно поле - id: id?.toString() ?? '0', // Защита от null, если id нет - descriptionText: description ?? 'Нет описания', // Используем реальное описание + name ?? 'UNKNOWN', + imageUrl: poster?.url ?? _imagePlaceholder, + id: id?.toString() ?? '0', + descriptionText: description ?? 'Нет описания', + year: year, + genres: genres?.map((genre) => genre.name ?? 'UNKNOWN').toList() ?? [], + countries: countries?.map((country) => country.name ?? 'UNKNOWN').toList() ?? [], ); } extension MoviesDtoToModel on MoviesDto { HomeData toDomain() => HomeData( - data: docs?.map((e) => e.toDomain()).toList(), // Изменено с data на docs + data: docs?.map((e) => e.toDomain()).toList(), nextPage: (pagination?.hasNextPage ?? false) ? ((pagination?.currentPage ?? 0) + 1) : null diff --git a/lib/data/repositories/mock_repository.dart b/lib/data/repositories/mock_repository.dart deleted file mode 100644 index ad0f969..0000000 --- a/lib/data/repositories/mock_repository.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:pmd_labs/components/utils/error_callback.dart'; -import 'package:pmd_labs/data/repositories/api_interface.dart'; -import 'package:pmd_labs/domain/models/carddata.dart'; -import 'package:pmd_labs/presentation/home_page/home_page.dart'; - -class MockRepository extends ApiInterface { - @override - Future loadData({OnErrorCallback? onError}) async { - return HomeData( - data: [ - CardData('JoJo’s Bizarre Adventure', descriptionText: 'kono dio da', imageUrl: 'https://i1.sndcdn.com/avatars-253MmMf9QZzxVBJi-rvlyeg-t1080x1080.jpg'), - CardData('Example', descriptionText: 'what is this?', imageUrl: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQvaBQ6nAedlqvXsh-dLXZi2Gexy1RkDbTUKQ&s'), - CardData('Mock data', descriptionText: 'Mock data description', imageUrl: 'https://cdn-user30887.skyeng.ru/uploads/6692a339c6989979804399.png'), - ], - ); - } - -} \ No newline at end of file diff --git a/lib/data/repositories/movie_repository.dart b/lib/data/repositories/movie_repository.dart index def30c1..978998b 100644 --- a/lib/data/repositories/movie_repository.dart +++ b/lib/data/repositories/movie_repository.dart @@ -14,14 +14,14 @@ class MovieRepository extends ApiInterface { )); static const String _baseUrl = 'https://api.kinopoisk.dev'; - static const String _apiKey = 'HQTFY5N-8D34FT0-HXQQQ1S-KPREHDX'; // Ваш API-ключ + static const String _apiKey = 'HQTFY5N-8D34FT0-HXQQQ1S-KPREHDX'; @override Future loadData({ OnErrorCallback? onError, String? q, int page = 1, - int pageSize = 15, + int pageSize = 10, }) async { try { const String url = '$_baseUrl/v1.4/movie/search'; @@ -30,7 +30,7 @@ class MovieRepository extends ApiInterface { url, queryParameters: { 'page': page, - 'limit': pageSize, + 'pageSize': pageSize, 'query': q, }, options: Options( diff --git a/lib/domain/models/carddata.dart b/lib/domain/models/carddata.dart index c6d0595..285ff9b 100644 --- a/lib/domain/models/carddata.dart +++ b/lib/domain/models/carddata.dart @@ -1,12 +1,17 @@ - class CardData { final String text; final String descriptionText; final String? imageUrl; final String? id; - - CardData(this.text, - {required this.descriptionText, - this.imageUrl, - this.id}); + final int? year; + final List? genres; + final List? countries; + CardData(this.text, { + required this.descriptionText, + this.imageUrl, + this.id, + this.year, + this.genres, + this.countries, + }); } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 13f3106..f7a3dca 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,16 +21,16 @@ class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { - return BlocProvider( + return BlocProvider( // lazy: false, create: (context) => LocaleBloc(Locale(Platform.localeName)), - child: BlocBuilder( + child: BlocBuilder( // builder: (context, state) { return MaterialApp( title: 'Flutter Demo', - locale: state.currentLocale, - localizationsDelegates: AppLocale.localizationsDelegates, - supportedLocales: AppLocale.supportedLocales, + locale: state.currentLocale, // передаем текущую локаль + localizationsDelegates: AppLocale.localizationsDelegates, // делегат (подключение локали) + supportedLocales: AppLocale.supportedLocales, // список доступных локалей (подключение локали) debugShowCheckedModeBanner: false, theme: ThemeData( colorScheme: @@ -40,7 +40,7 @@ class MyApp extends StatelessWidget { home: RepositoryProvider( lazy: true, create: (_) => MovieRepository(), - child: BlocProvider( + child: BlocProvider( // добавили BlocProvider lazy: false, create: (context) => LikeBloc(), child: BlocProvider( diff --git a/lib/presentation/details_page/details_page.dart b/lib/presentation/details_page/details_page.dart index b59b376..625fbe9 100644 --- a/lib/presentation/details_page/details_page.dart +++ b/lib/presentation/details_page/details_page.dart @@ -11,45 +11,86 @@ class DetailsPage extends StatelessWidget { return Scaffold( appBar: AppBar( title: Text("Детали"), + backgroundColor: Colors.purpleAccent, // Фиолетовый цвет для AppBar ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Изображение слева - ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(20)), - child: Image.network( - data.imageUrl ?? '', - height: 600, // Задайте фиксированную высоту для изображения - width: 600, // Задайте фиксированную ширину для изображения - fit: BoxFit.cover, // Обеспечьте хороший аспект изображения + body: Container( + color: Colors.purple[100], // Светло-сиреневый фон для всей страницы + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Изображение слева + ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(20)), + child: Image.network( + data.imageUrl ?? '', + height: 600, // Задайте фиксированную высоту для изображения + width: 600, // Задайте фиксированную ширину для изображения + fit: BoxFit.cover, // Обеспечьте хороший аспект изображения + ), ), - ), - SizedBox(width: 16), // Промежуток между изображением и текстом + SizedBox(width: 16), // Промежуток между изображением и текстом - // Текст справа - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Text( - data.text, - style: Theme.of(context).textTheme.headlineLarge, + // Текст справа + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Text( + data.text, + style: Theme.of(context).textTheme.headlineLarge?.copyWith( + color: Colors.purple, // Простой сиреневый цвет текста заголовка + fontWeight: FontWeight.bold, // Жирный шрифт + ), + ), ), - ), - Text( - data.descriptionText, - style: Theme.of(context).textTheme.bodyLarge, - ), - ], + Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Text( + 'Год: ${data.year}', // Отображение года + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Colors.purple[700], // Темный сиреневый цвет текста года + fontStyle: FontStyle.italic, // Курсив для выделения года + ), + ), + ), + Text( + data.descriptionText ?? '', // Обработаем случай, если описания нет + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Colors.purple[600], // Темный сиреневый цвет текста описания + ), + ), + // Отображение жанров + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + 'Жанры: ${data.genres?.join(', ') ?? 'Нет жанров'}', // Проверка на null + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Colors.purple[600], // Темный сиреневый цвет текста жанров + fontStyle: FontStyle.italic, // Курсив для выделения жанров + ), + ), + ), + // Отображение стран + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + 'Страны: ${data.countries?.join(', ') ?? 'Нет стран'}', // Проверка на null + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Colors.purple[600], // Темный сиреневый цвет текста стран + fontStyle: FontStyle.italic, // Курсив для выделения стран + ), + ), + ), + ], + ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/presentation/home_page/card.dart b/lib/presentation/home_page/card.dart index c1407f2..01f0d22 100644 --- a/lib/presentation/home_page/card.dart +++ b/lib/presentation/home_page/card.dart @@ -2,7 +2,7 @@ part of 'home_page.dart'; typedef OnLikeCallback = void Function(String? id, String title, bool isLiked)?; -class _Card extends StatelessWidget { +class _Card extends StatelessWidget { // состояние карточки регулируется извне; виджеты с точкой убираем final String text; final String descriptionText; final String? imageUrl; @@ -11,22 +11,20 @@ class _Card extends StatelessWidget { final String? id; final bool isLiked; - const _Card( - this.text, { - required this.descriptionText, - this.imageUrl, - this.onLike, - this.onTap, - this.id, - this.isLiked = false, - }); + const _Card(this.text, { + required this.descriptionText, + this.imageUrl, + this.onLike, + this.onTap, + this.id, + this.isLiked = false, + }); - factory _Card.fromData( - CardData data, { - OnLikeCallback onLike, - VoidCallback? onTap, - bool isLiked = false, - }) => + factory _Card.fromData(CardData data, { + OnLikeCallback onLike, + VoidCallback? onTap, + bool isLiked = false, + }) => _Card( data.text, descriptionText: data.descriptionText, @@ -43,13 +41,14 @@ class _Card extends StatelessWidget { onTap: onTap, child: Container( margin: const EdgeInsets.all(16), - constraints: const BoxConstraints(minHeight: 160), + constraints: const BoxConstraints(minHeight: 130), decoration: BoxDecoration( - color: Colors.white70, + color: Colors.white.withOpacity(0.9), borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.purpleAccent, width: 2), boxShadow: [ BoxShadow( - color: Colors.grey.withOpacity(.5), + color: Colors.grey.withOpacity(0.5), spreadRadius: 4, offset: const Offset(0, 5), blurRadius: 8, @@ -68,33 +67,32 @@ class _Card extends StatelessWidget { child: SizedBox( height: double.infinity, width: 120, - child: Stack( - children: [ - Positioned.fill( - child: Image.network( - imageUrl ?? '', - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => const Placeholder(), - ), - ), - ], + child: Image.network( + imageUrl ?? '', + fit: BoxFit.cover, + + errorBuilder: (_, __, ___) => const Placeholder(), ), ), ), Expanded( child: Padding( - padding: const EdgeInsets.only(left: 16.0), + padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - text, - style: Theme.of(context).textTheme.headlineSmall, + Center( + child: Text( + text, + style: Theme + .of(context) + .textTheme + .headlineSmall + ?.apply(color: Colors.purple), + textAlign: TextAlign.center, + ), ), - Text( - descriptionText, - style: Theme.of(context).textTheme.bodyLarge, - ) + SizedBox(height: 4), ], ), ), @@ -102,20 +100,35 @@ class _Card extends StatelessWidget { Align( alignment: Alignment.bottomRight, child: Padding( - padding: const EdgeInsets.only(left: 8, right: 16, bottom: 16), + padding: const EdgeInsets.only( + left: 8, right: 16, bottom: 16), child: GestureDetector( onTap: () => onLike?.call(id, text, isLiked), child: AnimatedSwitcher( duration: const Duration(milliseconds: 200), - child: isLiked - ? const Icon( - Icons.favorite, - color: Colors.redAccent, - key: ValueKey(0), - ) - : const Icon( - Icons.favorite_border, - key: ValueKey(1), + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: isLiked ? Colors.transparent : Colors.purple, + width: 2, + ), + borderRadius: BorderRadius.circular(20), + ), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.all(8), + child: isLiked + ? const Icon( + Icons.favorite, + color: Colors.purple, + key: ValueKey(0), + ) + : const Icon( + Icons.favorite_border, + color: Colors.purple, + key: ValueKey(1), + ), + ), ), ), ), diff --git a/lib/presentation/home_page/home_page.dart b/lib/presentation/home_page/home_page.dart index 934d07e..0eaef0d 100644 --- a/lib/presentation/home_page/home_page.dart +++ b/lib/presentation/home_page/home_page.dart @@ -32,7 +32,7 @@ class _HomePageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, + backgroundColor: Colors.purpleAccent, ), body: const Body(), ); @@ -56,7 +56,7 @@ class _BodyState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { context.read().add(const HomeLoadDataEvent()); - context.read().add(const LoadLikesEvent()); + context.read().add(const LoadLikesEvent()); // событие на изменение лайка }); scrollController.addListener(_onNextPageListener); @@ -84,7 +84,7 @@ class _BodyState extends State { final MovieRepository repo = MovieRepository(); var data = MovieRepository().loadData(); - void _onLike(String? id, String title, bool isLiked) { + void _onLike(String? id, String title, bool isLiked) { // обработчик лайков print("$id $title, $isLiked"); if (id != null) { context.read().add(ChangeLikeEvent(id)); @@ -96,7 +96,7 @@ class _BodyState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( - ' ${isLiked ? context.locale.liked : context.locale.disliked} $title', + ' ${isLiked ? context.locale.liked : context.locale.disliked} $title', //переписали константные строки под локаль style: Theme.of(context).textTheme.bodyLarge, ), backgroundColor: Colors.orangeAccent, @@ -124,58 +124,64 @@ class _BodyState extends State { child: Column( children: [ Padding( - padding: const EdgeInsets.all(12), - child: CupertinoSearchTextField( - controller: searchController, - onChanged: (search) { - Debounce.run(() => context - .read() - .add(HomeLoadDataEvent(search: search))); - })), - GestureDetector( - onTap: () => - context.read().add(const ChangeLocaleEvent()), - child: SizedBox.square( - dimension: 50, - child: Padding( - padding: const EdgeInsets.only(right: 12), - child: BlocBuilder( - builder: (context, state) { - return state.currentLocale.languageCode == 'ru' - ? const SvgRu() - : const SvgUk(); - }, + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Expanded( + child: CupertinoSearchTextField( + controller: searchController, + onChanged: (search) { + Debounce.run(() => context + .read() + .add(HomeLoadDataEvent(search: search))); + }, + ), ), - ), + const SizedBox(width: 12), // Отступ между полем поиска и иконкой + GestureDetector( + onTap: () => + context.read().add(const ChangeLocaleEvent()), // смена иконки локализации + child: SizedBox.square( + dimension: 50, + child: BlocBuilder( + builder: (context, state) { + return state.currentLocale.languageCode == 'ru' + ? const SvgRu() + : const SvgUk(); + }, + ), + ), + ), + ], ), ), - BlocBuilder( + BlocBuilder( // обертка списка с карточками builder: (context, state) => state.isLoading ? CircularProgressIndicator() : BlocBuilder( - builder: (context, likeState) => Expanded( - child: RefreshIndicator( - onRefresh: _onRefresh, - child: ListView.builder( - controller: scrollController, - padding: EdgeInsets.zero, - itemCount: state.data?.data?.length ?? 0, - itemBuilder: (context, index) { - final data = state.data?.data?[index]; - return data != null - ? _Card.fromData( - data, - isLiked: likeState.likedIds - ?.contains(data.id) == - true, - onLike: _onLike, - onTap: () => - _navToDetails(context, data), - ) - : const SizedBox.shrink(); - }), - ), - ))), + builder: (context, likeState) => Expanded( + child: RefreshIndicator( + onRefresh: _onRefresh, + child: ListView.builder( + controller: scrollController, + padding: EdgeInsets.zero, + itemCount: state.data?.data?.length ?? 0, + itemBuilder: (context, index) { + final data = state.data?.data?[index]; + return data != null + ? _Card.fromData( + data, + isLiked: likeState.likedIds + ?.contains(data.id) == + true, + onLike: _onLike, + onTap: () => + _navToDetails(context, data), + ) + : const SizedBox.shrink(); + }), + ), + ))), BlocBuilder( builder: (context, state) => state.isPaginationLoading ? const CircularProgressIndicator() diff --git a/pubspec.yaml b/pubspec.yaml index c57d1a8..8d9fa38 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,7 +43,7 @@ dev_dependencies: flutter_icons: android: "ic_launcher" ios: true - image_path: "assets/launcher.jpg" + image_path: "assets/icon1.jpg" min_sdk_android: 21 flutter: