diff --git a/lib/components/locale/l10n/app_locale.dart b/lib/components/locale/l10n/app_locale.dart new file mode 100644 index 0000000..7ef6944 --- /dev/null +++ b/lib/components/locale/l10n/app_locale.dart @@ -0,0 +1,151 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_locale_en.dart'; +import 'app_locale_ru.dart'; + +/// Callers can lookup localized strings with an instance of AppLocale +/// returned by `AppLocale.of(context)`. +/// +/// Applications need to include `AppLocale.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'l10n/app_locale.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocale.localizationsDelegates, +/// supportedLocales: AppLocale.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocale.supportedLocales +/// property. +abstract class AppLocale { + AppLocale(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocale? of(BuildContext context) { + return Localizations.of(context, AppLocale); + } + + static const LocalizationsDelegate delegate = _AppLocaleDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('en'), + Locale('ru') + ]; + + /// No description provided for @search. + /// + /// In ru, this message translates to: + /// **'Поиск'** + String get search; + + /// No description provided for @liked. + /// + /// In ru, this message translates to: + /// **'понравился!'** + String get liked; + + /// No description provided for @disliked. + /// + /// In ru, this message translates to: + /// **'разонравился'** + String get disliked; + + /// No description provided for @arbEnding. + /// + /// In ru, this message translates to: + /// **'Чтобы не забыть про отсутствие запятой :)'** + String get arbEnding; +} + +class _AppLocaleDelegate extends LocalizationsDelegate { + const _AppLocaleDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocale(locale)); + } + + @override + bool isSupported(Locale locale) => ['en', 'ru'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocaleDelegate old) => false; +} + +AppLocale lookupAppLocale(Locale locale) { + + + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': return AppLocaleEn(); + case 'ru': return AppLocaleRu(); + } + + throw FlutterError( + 'AppLocale.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.' + ); +} diff --git a/lib/components/locale/l10n/app_locale_en.dart b/lib/components/locale/l10n/app_locale_en.dart new file mode 100644 index 0000000..f831be2 --- /dev/null +++ b/lib/components/locale/l10n/app_locale_en.dart @@ -0,0 +1,18 @@ +import 'app_locale.dart'; + +/// The translations for English (`en`). +class AppLocaleEn extends AppLocale { + AppLocaleEn([String locale = 'en']) : super(locale); + + @override + String get search => 'Search'; + + @override + String get liked => 'liked!'; + + @override + String get disliked => 'disliked'; + + @override + String get arbEnding => 'Чтобы не забыть про отсутствие запятой :)'; +} diff --git a/lib/components/locale/l10n/app_locale_ru.dart b/lib/components/locale/l10n/app_locale_ru.dart new file mode 100644 index 0000000..cc6fba4 --- /dev/null +++ b/lib/components/locale/l10n/app_locale_ru.dart @@ -0,0 +1,18 @@ +import 'app_locale.dart'; + +/// The translations for Russian (`ru`). +class AppLocaleRu extends AppLocale { + AppLocaleRu([String locale = 'ru']) : super(locale); + + @override + String get search => 'Поиск'; + + @override + String get liked => 'понравился!'; + + @override + String get disliked => 'разонравился'; + + @override + String get arbEnding => 'Чтобы не забыть про отсутствие запятой :)'; +} diff --git a/lib/components/utils/debounce.dart b/lib/components/utils/debounce.dart deleted file mode 100644 index 60a18e1..0000000 --- a/lib/components/utils/debounce.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'dart:async'; -import 'dart:ui'; - -class Debounce { //помощник - factory Debounce() => _instance; //обращение через статический конструктор - - Debounce._(); - - static final Debounce _instance = Debounce._(); - - static Timer? _timer; - - static void run(//создаём задержку для ввода пользователя в поисковую строку - VoidCallback action, { - Duration delay = const Duration(milliseconds: 500), - }) { - _timer?.cancel(); - _timer = Timer(delay, action); - } -} \ No newline at end of file diff --git a/lib/data/dtos/weapons_dto.dart b/lib/data/dtos/weapons_dto.dart index bd0692b..9a5fe3a 100644 --- a/lib/data/dtos/weapons_dto.dart +++ b/lib/data/dtos/weapons_dto.dart @@ -12,13 +12,15 @@ class WeaponsDto { @JsonSerializable(createToJson: false) class WeaponDto { + final String? uuid; final String? displayName; final String? displayIcon; final String? category; final int? magazineSize; final int? cost; - const WeaponDto({this.displayName, this.displayIcon, this.category, this.magazineSize, this.cost}); + const WeaponDto( + {this.uuid, this.displayName, this.displayIcon, this.category, this.magazineSize, this.cost}); factory WeaponDto.fromJson(Map json) => _$WeaponDtoFromJson(json); -} \ No newline at end of file +} diff --git a/lib/data/dtos/weapons_dto.g.dart b/lib/data/dtos/weapons_dto.g.dart index 6b07d22..212ef27 100644 --- a/lib/data/dtos/weapons_dto.g.dart +++ b/lib/data/dtos/weapons_dto.g.dart @@ -6,8 +6,7 @@ part of 'weapons_dto.dart'; // JsonSerializableGenerator // ************************************************************************** -WeaponsDto _$WeaponsDtoFromJson(Map json) => - WeaponsDto( +WeaponsDto _$WeaponsDtoFromJson(Map json) => WeaponsDto( data: (json['data'] as List?) ?.map((e) => WeaponDto.fromJson(e as Map)) .toList(), @@ -17,10 +16,11 @@ WeaponDto _$WeaponDtoFromJson(Map json) { var weaponStats = json['weaponStats'] as Map?; var shopData = json['shopData'] as Map?; return WeaponDto( + uuid: json['uuid'] as String?, displayName: json['displayName'] as String?, displayIcon: json['displayIcon'] as String?, category: shopData?['category'] as String?, magazineSize: weaponStats?['magazineSize'] as int?, cost: shopData?['cost'] as int?, ); -} \ No newline at end of file +} diff --git a/lib/data/mappers/weapons_mapper.dart b/lib/data/mappers/weapons_mapper.dart index accdc8b..2b59ba6 100644 --- a/lib/data/mappers/weapons_mapper.dart +++ b/lib/data/mappers/weapons_mapper.dart @@ -3,10 +3,11 @@ import '../dtos/weapons_dto.dart'; extension WeaponDtoToModel on WeaponDto { CardData toDomain() => CardData( - displayName ?? 'UNKNOWN', - categoryText: category ?? 'Описание отсутствует', - gameDesc: magazineSize, - gameDesc2: cost, - imageUrl: displayIcon, - ); + displayName ?? 'UNKNOWN', + uuid: uuid ?? 'UNKNOWN', + categoryText: category ?? 'Описание отсутствует', + gameDesc: magazineSize, + gameDesc2: cost, + imageUrl: displayIcon, + ); } diff --git a/lib/data/repositories/api_interface.dart b/lib/data/repositories/api_interface.dart index 3ff1042..eddb317 100644 --- a/lib/data/repositories/api_interface.dart +++ b/lib/data/repositories/api_interface.dart @@ -1,8 +1,7 @@ - import '../../domain/models/card.dart'; typedef OnErrorCallback = void Function(String? error); -abstract class ApiInterface{ +abstract class ApiInterface { Future?> loadData(); -} \ No newline at end of file +} diff --git a/lib/data/repositories/mock_repository.dart b/lib/data/repositories/mock_repository.dart index e6415a6..831c408 100644 --- a/lib/data/repositories/mock_repository.dart +++ b/lib/data/repositories/mock_repository.dart @@ -1,75 +1,72 @@ - import '../../domain/models/card.dart'; import 'api_interface.dart'; class MockRepository extends ApiInterface { @override - Future?> loadData() async{ + Future?> loadData() async { return [ CardData( + uuid: '1', 'Мама с сыном', categoryText: '-Чё вылупился?', - imageUrl: - 'https://pic.rutubelist.ru/video/8b/31/8b31b4f162bf11007c036be6787e9bb1.jpg', + imageUrl: 'https://pic.rutubelist.ru/video/8b/31/8b31b4f162bf11007c036be6787e9bb1.jpg', gameDesc: 2, - gameDesc2: 2 - ), + gameDesc2: 2), CardData( + uuid: '2', 'Ярускин Салих', categoryText: 'я мусор', imageUrl: - 'https://steamuserimages-a.akamaihd.net/ugc/2030615098399987630/EB9690C3D097504388EA8F057E48631354C05182/?imw=512&&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=false', + 'https://steamuserimages-a.akamaihd.net/ugc/2030615098399987630/EB9690C3D097504388EA8F057E48631354C05182/?imw=512&&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=false', gameDesc: 2, - gameDesc2: 2 - ), + gameDesc2: 2), CardData( + uuid: '3', 'Гламурная бабизяна', categoryText: 'сделала губки 5мл', imageUrl: - 'https://avatars.dzeninfra.ru/get-zen_doc/3310860/pub_602156c24849a6360821ff59_60215909390eb32b9bb9e012/scale_1200', + 'https://avatars.dzeninfra.ru/get-zen_doc/3310860/pub_602156c24849a6360821ff59_60215909390eb32b9bb9e012/scale_1200', gameDesc: 2, - gameDesc2: 2 - ), + gameDesc2: 2), CardData( + uuid: '4', 'Мелкий', categoryText: 'невдупленыш', imageUrl: - 'https://sun9-59.userapi.com/impg/DBnGgdqZnRtBw9jchGixY6rRN-zTWaAEVjLxXw/HrYCralVB-4.jpg?size=807x577&quality=96&sign=a84b4d87249b7d5ade53369663a3a9e0&c_uniq_tag=2XoQqW9aaCVDnvITURMqqPPY9yznsdCr4HWXaSv9Q_U&type=album', + 'https://sun9-59.userapi.com/impg/DBnGgdqZnRtBw9jchGixY6rRN-zTWaAEVjLxXw/HrYCralVB-4.jpg?size=807x577&quality=96&sign=a84b4d87249b7d5ade53369663a3a9e0&c_uniq_tag=2XoQqW9aaCVDnvITURMqqPPY9yznsdCr4HWXaSv9Q_U&type=album', gameDesc: 2, - gameDesc2: 2 - ), + gameDesc2: 2), CardData( + uuid: '5', 'Обезьянка Олежа', categoryText: 'Поняла смысл жизни...', imageUrl: - 'https://avatars.yandex.net/get-music-content/5417945/f468314f.a.20066160-1/m1000x1000?webp=false', + 'https://avatars.yandex.net/get-music-content/5417945/f468314f.a.20066160-1/m1000x1000?webp=false', gameDesc: 2, - gameDesc2: 2 - ), + gameDesc2: 2), CardData( + uuid: '6', 'Афанасьев Степан', categoryText: 'основатель ЧВК Мартышки', imageUrl: - 'https://steamuserimages-a.akamaihd.net/ugc/1621850006938404146/EA72DC3C31DED440C024F0DD1D9859C44B1BBDFF/?imw=512&&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=false', + 'https://steamuserimages-a.akamaihd.net/ugc/1621850006938404146/EA72DC3C31DED440C024F0DD1D9859C44B1BBDFF/?imw=512&&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=false', gameDesc: 2, - gameDesc2: 2 - ), + gameDesc2: 2), CardData( + uuid: '7', 'Лобашов Иван', categoryText: 'вычисляет противников', - imageUrl: - 'https://cdn1.ozone.ru/s3/multimedia-i/6449406306.jpg', + imageUrl: 'https://cdn1.ozone.ru/s3/multimedia-i/6449406306.jpg', gameDesc: 2, - gameDesc2: 2 - ), + gameDesc2: 2), CardData( + uuid: '8', 'Коренной представитель ЧВК Мартышки', categoryText: 'сейчас он где-то в Камеруне проветривает свои блохи', imageUrl: - 'https://otvet.imgsmail.ru/download/306401642_9fecf789a8802c5805c4e21291cba6a6_800.jpg', + 'https://otvet.imgsmail.ru/download/306401642_9fecf789a8802c5805c4e21291cba6a6_800.jpg', gameDesc: 2, - gameDesc2: 2 - ), + gameDesc2: 2), ]; } -} \ No newline at end of file +} diff --git a/lib/data/repositories/weapons_repository.dart b/lib/data/repositories/weapons_repository.dart index 6d3f2ff..ff74f05 100644 --- a/lib/data/repositories/weapons_repository.dart +++ b/lib/data/repositories/weapons_repository.dart @@ -16,7 +16,10 @@ class WeaponsRepository extends ApiInterface { static const String _baseUrl = 'https://valorant-api.com/v1/weapons'; @override - Future?> loadData({OnErrorCallback? onError,String? q,}) async { + Future?> loadData({ + OnErrorCallback? onError, + String? q, + }) async { try { // Формирование URL для запроса final Response response = await _dio.get(_baseUrl); @@ -30,8 +33,9 @@ class WeaponsRepository extends ApiInterface { // Фильтрация данных по displayName if (q != null && q.isNotEmpty) { - data = data?.where((Weapon) => - Weapon.text?.toLowerCase().contains(q.toLowerCase()) ?? false).toList(); + data = data + ?.where((Weapon) => Weapon.text?.toLowerCase().contains(q.toLowerCase()) ?? false) + .toList(); } return data; diff --git a/lib/domain/models/card.dart b/lib/domain/models/card.dart index 9972632..f3b67d6 100644 --- a/lib/domain/models/card.dart +++ b/lib/domain/models/card.dart @@ -7,13 +7,15 @@ class CardData { final String? imageUrl; final int? gameDesc; final int? gameDesc2; + late final String uuid; CardData( - this.text, { - required this.categoryText, - //this.icon = Icons.ac_unit_outlined, - this.imageUrl, - required this.gameDesc, - required this.gameDesc2, - }); -} \ No newline at end of file + this.text, { + required this.categoryText, + //this.icon = Icons.ac_unit_outlined, + this.imageUrl, + required this.gameDesc, + required this.gameDesc2, + required this.uuid, + }); +} diff --git a/lib/main.dart b/lib/main.dart index 855f396..6ce5bda 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,11 @@ +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test_app/presentation/home_page/bloc/bloc.dart'; +import 'package:flutter_test_app/presentation/like_bloc/like_bloc.dart'; +import 'package:flutter_test_app/presentation/locale_bloc/locale_bloc.dart'; +import 'package:flutter_test_app/presentation/locale_bloc/locale_state.dart'; +import 'components/locale/l10n/app_locale.dart'; import 'data/repositories/weapons_repository.dart'; import 'presentation/home_page/home_page.dart'; @@ -13,28 +18,36 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - debugShowCheckedModeBanner: false, - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange), - useMaterial3: true, - ), - home: RepositoryProvider( //даём нашему репозиторию доступ - lazy: true, //ленивое создание объекта - create: (_) => WeaponsRepository(), - child: BlocProvider( //даём доступ Bloc для нашей страницы - lazy: false, - create: (context) => HomeBloc(context.read()), //в конструктор нашего Блока передаём репозиторий с нашей апи, то есть у нас появляется доступ по дереву к апи - child: const MyHomePage(title: 'Каталог оружий Valorant'), - ), - ), + return BlocProvider( + lazy: false, + create: (context) => LocaleBloc(Locale(Platform.localeName)), + child: BlocBuilder(//Передаём текущую локаль + builder: (context, state) { + return MaterialApp( + title: 'Flutter Demo', + locale: state.currentLocale, + localizationsDelegates: AppLocale.localizationsDelegates, + supportedLocales: AppLocale.supportedLocales, // подключаем список доступных локалей + debugShowCheckedModeBanner: false, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurpleAccent), + useMaterial3: true, + ), + home: RepositoryProvider( + lazy: true, + create: (_) => WeaponsRepository(), + child: BlocProvider( + lazy: false, + create: (context) => LikeBloc(), + child: BlocProvider( + lazy: false, + create: (context) => HomeBloc(context.read()), + child: const MyHomePage(title: 'Каталог оружия Valorant'), + ), + ), + ), + ); + }), ); } } - - - - - - diff --git a/lib/presentation/details_page/details_page.dart b/lib/presentation/details_page/details_page.dart index 0c9776f..98ee978 100644 --- a/lib/presentation/details_page/details_page.dart +++ b/lib/presentation/details_page/details_page.dart @@ -33,7 +33,6 @@ class DetailsPage extends StatelessWidget { 'Кол-во патрон: ${data.gameDesc.toString()}, Стоимость: ${data.gameDesc2.toString()}', style: Theme.of(context).textTheme.bodyLarge, ) - ], ), ), diff --git a/lib/presentation/home_page/bloc/bloc.dart b/lib/presentation/home_page/bloc/bloc.dart index 3d39a03..be9f35c 100644 --- a/lib/presentation/home_page/bloc/bloc.dart +++ b/lib/presentation/home_page/bloc/bloc.dart @@ -3,28 +3,30 @@ import 'package:flutter_test_app/presentation/home_page/bloc/events.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../data/repositories/weapons_repository.dart'; -class HomeBloc extends Bloc{//наследуем от Bloc и передаём состояния +class HomeBloc extends Bloc { + //наследуем от Bloc и передаём состояния final WeaponsRepository rep; HomeBloc(this.rep) : super(const HomeState()) { on(_onLoadData); } - Future _onLoadData( - HomeLoadDataEvent event, Emitter emit) async { + Future _onLoadData(HomeLoadDataEvent event, Emitter emit) async { emit(state.copyWith(isLoading: true)); //метода emit изменяет наши состояния String? error; - final data = await rep.loadData( //загрузка данных + final data = await rep.loadData( + //загрузка данных q: event.search, onError: (e) => error = e, ); - emit(state.copyWith( //метода emit изменяет наши состояния + emit(state.copyWith( + //метода emit изменяет наши состояния isLoading: false, //флаг загрузки меняем на false data: data, error: error, )); } -} \ No newline at end of file +} diff --git a/lib/presentation/home_page/bloc/events.dart b/lib/presentation/home_page/bloc/events.dart index 70d3132..dc28622 100644 --- a/lib/presentation/home_page/bloc/events.dart +++ b/lib/presentation/home_page/bloc/events.dart @@ -1,9 +1,10 @@ -abstract class HomeEvent{ //наследуем все события +abstract class HomeEvent { + //наследуем все события const HomeEvent(); } -class HomeLoadDataEvent extends HomeEvent{ +class HomeLoadDataEvent extends HomeEvent { final String? search; //событие поиска const HomeLoadDataEvent({this.search}); //событие на загрузку данных -} \ No newline at end of file +} diff --git a/lib/presentation/home_page/bloc/state.dart b/lib/presentation/home_page/bloc/state.dart index fe51cd7..0a45d8b 100644 --- a/lib/presentation/home_page/bloc/state.dart +++ b/lib/presentation/home_page/bloc/state.dart @@ -3,7 +3,8 @@ import 'package:equatable/equatable.dart'; import '../../../domain/models/card.dart'; @CopyWith() -class HomeState extends Equatable{ //сравнение двух состояний через Equatable +class HomeState extends Equatable { + //сравнение двух состояний через Equatable final List? data; final bool isLoading; //отображение загрузки final String? error; //отображение ошибок @@ -14,7 +15,8 @@ class HomeState extends Equatable{ //сравнение двух состоян this.error, }); - HomeState copyWith({ //copyWith создаёт копию объекта с изменением некоторых данных + HomeState copyWith({ + //copyWith создаёт копию объекта с изменением некоторых данных List? data, bool? isLoading, String? error, @@ -27,8 +29,8 @@ class HomeState extends Equatable{ //сравнение двух состоян @override List get props => [ - data, - isLoading, - error, - ]; -} \ No newline at end of file + data, + isLoading, + error, + ]; +} diff --git a/lib/presentation/home_page/card.dart b/lib/presentation/home_page/card.dart index 9e1b0d2..24659ea 100644 --- a/lib/presentation/home_page/card.dart +++ b/lib/presentation/home_page/card.dart @@ -1,49 +1,49 @@ part of 'home_page.dart'; -typedef OnLikeCallback = void Function(String title, bool isLiked); +typedef OnLikeCallback = void Function(String? id, String title, bool isLiked); -class _Card extends StatefulWidget { +class _Card extends StatelessWidget { final String text; final String categoryText; //final IconData icon; final String? imageUrl; final OnLikeCallback? onLike; final VoidCallback? onTap; + final String uuid; + final bool isLiked; const _Card( - this.text, { - //this.icon = Icons.abc, - required this.categoryText, - this.imageUrl, - this.onLike, - this.onTap, - }); + this.text, { + //this.icon = Icons.abc, + required this.categoryText, + this.imageUrl, + this.onLike, + this.onTap, + required this.uuid, + this.isLiked = false, + }); factory _Card.fromData( - CardData data, { - OnLikeCallback? onLike, - VoidCallback? onTap, - }) => + CardData data, { + OnLikeCallback? onLike, + VoidCallback? onTap, + bool isLiked = false, + }) => _Card( + uuid: data.uuid, data.text!, categoryText: data.categoryText!, //icon: data.icon, imageUrl: data.imageUrl, onLike: onLike, onTap: onTap, + isLiked: isLiked, ); - @override - State<_Card> createState() => _CardState(); -} - -class _CardState extends State<_Card> { - bool isLiked = false; - @override Widget build(BuildContext context) { return GestureDetector( - onTap: widget.onTap, + onTap: onTap, child: Container( margin: const EdgeInsets.all(5), padding: const EdgeInsets.all(10), @@ -72,7 +72,7 @@ class _CardState extends State<_Card> { children: [ Positioned.fill( child: Image.network( - widget.imageUrl ?? '', + imageUrl ?? '', fit: BoxFit.contain, errorBuilder: (_, __, ___) => const Placeholder(), ), @@ -89,10 +89,8 @@ class _CardState extends State<_Card> { padding: const EdgeInsets.fromLTRB(8, 2, 8, 2), child: Text( 'New!!!!', - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith(color: Colors.black), + style: + Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.black), ), ), ) @@ -102,38 +100,33 @@ class _CardState extends State<_Card> { ), const SizedBox(height: 5), Text( - widget.text, + text, style: Theme.of(context).textTheme.headlineLarge, ), Text( - widget.categoryText, + categoryText, style: Theme.of(context).textTheme.bodyLarge?.copyWith( - fontSize: 20, - ), + fontSize: 20, + ), ), const SizedBox(height: 5), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ GestureDetector( - onTap: () { - setState(() { - isLiked = !isLiked; - }); - widget.onLike?.call(widget.text, isLiked); - }, + onTap: () => onLike?.call(uuid, text, isLiked), child: AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: isLiked ? const Icon( - Icons.favorite, - color: Colors.redAccent, - key: ValueKey(0), - ) + Icons.favorite, + color: Colors.redAccent, + key: ValueKey(0), + ) : const Icon( - Icons.favorite_border, - key: ValueKey(1), - ), + Icons.favorite_border, + key: ValueKey(1), + ), ), ), ], diff --git a/lib/presentation/home_page/home_page.dart b/lib/presentation/home_page/home_page.dart index 2de5b00..0f74fe8 100644 --- a/lib/presentation/home_page/home_page.dart +++ b/lib/presentation/home_page/home_page.dart @@ -1,16 +1,23 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test_app/components/extensions/context_x.dart'; +import 'package:flutter_test_app/data/repositories/mock_repository.dart'; +import 'package:flutter_test_app/main.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; - -import '../../components/utils/debounce.dart'; +import 'package:flutter_test_app/presentation/like_bloc/like_bloc.dart'; +import 'package:flutter_test_app/presentation/like_bloc/like_event.dart'; +import 'package:flutter_test_app/presentation/like_bloc/like_state.dart'; +import 'package:flutter_test_app/presentation/locale_bloc/locale_bloc.dart'; +import 'package:flutter_test_app/presentation/locale_bloc/locale_events.dart'; +import 'package:flutter_test_app/presentation/locale_bloc/locale_state.dart'; import '../../data/repositories/weapons_repository.dart'; import '../../domain/models/card.dart'; +import '../common/svg_objects.dart'; import '../details_page/details_page.dart'; - -import 'package:flutter_bloc/flutter_bloc.dart'; import 'bloc/bloc.dart'; import 'bloc/events.dart'; import 'bloc/state.dart'; - +import '../../components/utils/debounce.dart'; part 'card.dart'; class MyHomePage extends StatefulWidget { @@ -50,8 +57,13 @@ class _BodyState extends State { @override void initState() { - WidgetsBinding.instance.addPostFrameCallback((_) { // привязываем логику к кадру + SvgObjects.init(); + WidgetsBinding.instance.addPostFrameCallback((_) { + // привязываем логику к кадру context.read().add(const HomeLoadDataEvent()); // добавляем данные после считывания + context + .read() + .add(const LoadLikesEvent()); // загрузка лайков которые ставили в прошлый раз }); super.initState(); } @@ -62,57 +74,93 @@ class _BodyState extends State { _scrollController.dispose(); super.dispose(); } + Future _onRefresh() { - context - .read() - .add(HomeLoadDataEvent(search: searchController.text)); + context.read().add(HomeLoadDataEvent(search: searchController.text)); return Future.value(null); //прекращение отображения загрузки } + void _onLike(String? id, String title, bool isLiked) { + if (id != null) { + context.read().add(ChangeLikeEvent(id)); + _showSnackBar(context, title, !isLiked); + } + } + @override Widget build(BuildContext context) { return Column( children: [ Padding( padding: const EdgeInsets.all(12), - child: CupertinoSearchTextField( - controller: searchController, - onChanged: (search ) { //вызов задержки пока пользователь не перестанет печатать в поисковой строке - Debounce.run(() => context.read().add(HomeLoadDataEvent(search: search))); - }, + child: Row( + // наше поле для поиска + children: [ + Expanded( + child: CupertinoSearchTextField( + // переписали под наши локали + controller: searchController, + placeholder: context.locale.search, + 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(left: 12), + child: BlocBuilder( + builder: (context, state) { + return state.currentLocale.languageCode == 'ru' + ? const SvgRu() + : const SvgUk(); // иконки из ассетов + }, + ), + ), + ), + ), + ], ), ), - BlocBuilder( //ждёт изменения состояния + BlocBuilder( + //ждёт изменения состояния builder: (context, state) => state.error != null ? Text( - state.error ?? '', - style: Theme.of(context) - .textTheme - .headlineSmall - ?.copyWith(color: Colors.red), - ) + state.error ?? '', + style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: Colors.red), + ) : state.isLoading - ? const CircularProgressIndicator() - : Expanded( - child: RefreshIndicator( - onRefresh: _onRefresh, - child: ListView.builder( - padding: EdgeInsets.zero, - itemCount: state.data?.length ?? 0, - itemBuilder: (context, index) { - final data = state.data?[index]; - return data != null - ? _Card.fromData( - data, - onLike: (String title, bool isLiked) => - _showSnackBar(context, title, isLiked), - onTap: () => _navToDetails(context, data), - ) - : const SizedBox.shrink(); - }, - ), - ), - ), + ? const CircularProgressIndicator() + : BlocBuilder( + // проверяем в зависимости от стейта, лайкнута ли карточка + builder: (context, likeState) => Expanded( + child: RefreshIndicator( + onRefresh: _onRefresh, + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: state.data?.length ?? 0, + itemBuilder: (context, index) { + final data = state.data?[index]; + return data != null + ? _Card.fromData( + data, + onLike: _onLike, + isLiked: likeState.likedIds?.contains(data.uuid) == true, + onTap: () => _navToDetails(context, data), + ) + : const SizedBox.shrink(); + }, + ), + ), + ), + ), ), ], ); @@ -129,7 +177,7 @@ class _BodyState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( - 'News $title ${isLiked ? 'liked' : 'disliked '}', + '$title ${isLiked ? context.locale.liked : context.locale.disliked}', style: Theme.of(context).textTheme.bodyLarge, ), backgroundColor: Colors.deepPurpleAccent, diff --git a/pubspec.lock b/pubspec.lock index 6105b60..3aa64ff 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.2.0" + archive: + dependency: transitive + description: + name: archive + sha256: "08064924cbf0ab88280a0c3f60db9dd24fec693927e725ecb176f16c629d1cb8" + url: "https://pub.dev" + source: hosted + version: "4.0.1" args: dependency: transitive description: @@ -129,6 +137,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + url: "https://pub.dev" + source: hosted + version: "0.4.1" clock: dependency: transitive description: @@ -162,7 +178,7 @@ packages: source: hosted version: "3.1.1" copy_with_extension: - dependency: transitive + dependency: "direct main" description: name: copy_with_extension sha256: fbcf890b0c34aedf0894f91a11a579994b61b4e04080204656b582708b5b1125 @@ -241,6 +257,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" file: dependency: transitive description: @@ -270,6 +294,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.6" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + url: "https://pub.dev" + source: hosted + version: "0.13.1" flutter_lints: dependency: "direct dev" description: @@ -278,11 +310,29 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "8c5d68a82add3ca76d792f058b186a0599414f279f00ece4830b9b231b570338" + url: "https://pub.dev" + source: hosted + version: "2.0.7" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" frontend_server_client: dependency: transitive description: @@ -315,6 +365,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.15.4" + http: + dependency: transitive + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" + source: hosted + version: "1.1.0" http_multi_server: dependency: transitive description: @@ -331,6 +389,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: "599d08e369969bdf83138f5b4e0a7e823d3f992f23b8a64dd626877c37013533" + url: "https://pub.dev" + source: hosted + version: "4.4.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" io: dependency: transitive description: @@ -435,6 +509,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.3" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" + source: hosted + version: "1.0.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + url: "https://pub.dev" + source: hosted + version: "5.4.0" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" pool: dependency: transitive description: @@ -443,6 +573,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + posix: + dependency: transitive + description: + name: posix + sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a + url: "https://pub.dev" + source: hosted + version: "6.0.1" pretty_dio_logger: dependency: "direct main" description: @@ -475,6 +613,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + url: "https://pub.dev" + source: hosted + version: "2.2.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + url: "https://pub.dev" + source: hosted + version: "2.3.5" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" shelf: dependency: transitive description: @@ -584,6 +778,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" + url: "https://pub.dev" + source: hosted + version: "1.1.11+1" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da + url: "https://pub.dev" + source: hosted + version: "1.1.11+1" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" + url: "https://pub.dev" + source: hosted + version: "1.1.11+1" vector_math: dependency: transitive description: @@ -616,6 +834,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" + win32: + dependency: transitive + description: + name: win32 + sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" + xml: + dependency: transitive + description: + name: xml + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + url: "https://pub.dev" + source: hosted + version: "6.3.0" yaml: dependency: transitive description: @@ -626,4 +868,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.1.3 <4.0.0" - flutter: ">=1.16.0" + flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index b396825..62ff108 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,11 +38,17 @@ dependencies: cupertino_icons: ^1.0.8 dio: ^5.4.2+1 pretty_dio_logger: ^1.3.1 - json_annotation: ^4.8.1 + json_annotation: ^4.9.0 html: ^0.15.0 equatable: ^2.0.5 flutter_bloc: ^8.1.5 copy_with_extension_gen: ^5.0.4 + flutter_svg: 2.0.7 + flutter_localizations: + sdk: flutter + intl: ^0.18.1 + copy_with_extension: ^5.0.4 + shared_preferences: ^2.2.3 dev_dependencies: flutter_test: @@ -53,21 +59,26 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. + flutter_launcher_icons: 0.13.1 build_runner: ^2.4.9 json_serializable: ^6.7.1 flutter_lints: ^4.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec - +flutter_launcher_icons: + android: "ic_launcher" + image_path: "assets/ic_launcher.png" + min_sdk_android: 21 # The following section is specific to Flutter packages. flutter: - + generate: true # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true - + assets: + - assets/svg/ # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg