diff --git a/I10n.yaml b/lib/I10n.yaml similarity index 63% rename from I10n.yaml rename to lib/I10n.yaml index d26d702..a396b3a 100644 --- a/I10n.yaml +++ b/lib/I10n.yaml @@ -1,6 +1,6 @@ arb-dir: l10n template-arb-file: app_ru.arb output-localization-file: app_locale.dart -output-dir: lib/components/locale/l10n +output-dir: lib/components/l10n output-class: AppLocale synthetic-package: false \ No newline at end of file diff --git a/lib/components/extensions/context_x.dart b/lib/components/extensions/context_x.dart new file mode 100644 index 0000000..a611371 --- /dev/null +++ b/lib/components/extensions/context_x.dart @@ -0,0 +1,6 @@ +import 'package:flutter/widgets.dart'; +import '../locale/l10n/app_locale.dart'; + +extension LocalContextX on BuildContext { + AppLocale get locale => AppLocale.of(this)!; +} \ No newline at end of file diff --git a/lib/data/mappers/characters_mapper.dart b/lib/data/mappers/characters_mapper.dart index 8aaf537..5b465a0 100644 --- a/lib/data/mappers/characters_mapper.dart +++ b/lib/data/mappers/characters_mapper.dart @@ -17,6 +17,7 @@ extension CharacterDataDtoToModel on CharacterDataDto { attributes?.name ?? 'UNKNOWN', imageUrl: attributes?.image ?? _imagePlaceholder, descriptionText: _makeDescriptionText(attributes?.born, attributes?.died), + id: id, ); String _makeDescriptionText(String? born, String? died) { diff --git a/lib/domain/models/card.dart b/lib/domain/models/card.dart index 05707d6..64c37cc 100644 --- a/lib/domain/models/card.dart +++ b/lib/domain/models/card.dart @@ -5,11 +5,13 @@ class CardData { final String descriptionText; final IconData icon; final String? imageUrl; + final String? id; CardData( this.text, { required this.descriptionText, this.icon = Icons.catching_pokemon, this.imageUrl, + this.id, }); } diff --git a/lib/main.dart b/lib/main.dart index 48edea9..d56123c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,13 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pmu/presentation/home_page/bloc/bloc.dart'; import 'package:pmu/presentation/home_page/home_page.dart'; +import 'package:pmu/presentation/like_bloc/like_bloc.dart'; +import 'package:pmu/presentation/locale_bloc/locale_bloc.dart'; +import 'package:pmu/presentation/locale_bloc/locale_state.dart'; +import 'components/locale/l10n/app_locale.dart'; import 'data/repositories/potter_repository.dart'; void main() { @@ -13,21 +19,36 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - debugShowCheckedModeBanner: false, - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange), - useMaterial3: true, - ), - home: RepositoryProvider( - lazy: true, - create: (_) => PotterRepository(), - child: BlocProvider( - lazy: false, - create: (context) => HomeBloc(context.read()), - child: const MyHomePage(title: 'Сафиулова Камилия Наилевна'), - ), + 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.deepPurple), + useMaterial3: true, + ), + home: RepositoryProvider( + lazy: true, + create: (_) => PotterRepository(), + child: BlocProvider( + lazy: false, + create: (context) => LikeBloc(), + child: BlocProvider( + lazy: false, + create: (context) => HomeBloc(context.read()), + child: const MyHomePage(title: 'Сафиулова Камилия Наилевна'), + ), + ), + ), + ); + }, ), ); } diff --git a/lib/presentation/common/svg_objects.dart b/lib/presentation/common/svg_objects.dart index 66cbca7..f6dcf66 100644 --- a/lib/presentation/common/svg_objects.dart +++ b/lib/presentation/common/svg_objects.dart @@ -31,4 +31,4 @@ class SvgUk extends StatelessWidget { Widget build(BuildContext context) { return SvgPicture.asset(R.ASSETS_SVG_UK_SVG); } -} \ No newline at end of file +} diff --git a/lib/presentation/home_page/card.dart b/lib/presentation/home_page/card.dart index 11c8d19..806f9a5 100644 --- a/lib/presentation/home_page/card.dart +++ b/lib/presentation/home_page/card.dart @@ -1,29 +1,34 @@ 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 descriptionText; final IconData icon; final String? imageUrl; final OnLikeCallback onLike; final VoidCallback? onTap; + final String? id; + final bool isLiked; const _Card( - this.text, { - this.icon = Icons.catching_pokemon, - required this.descriptionText, - this.imageUrl, - this.onLike, - this.onTap, - }); + this.text, { + this.icon = Icons.ac_unit_outlined, + required this.descriptionText, + this.imageUrl, + this.onLike, + this.onTap, + this.id, + this.isLiked = false, + }); factory _Card.fromData( - CardData data, { - OnLikeCallback onLike, - VoidCallback? onTap, - }) => + CardData data, { + OnLikeCallback onLike, + VoidCallback? onTap, + bool isLiked = false, + }) => _Card( data.text, descriptionText: data.descriptionText, @@ -31,26 +36,28 @@ class _Card extends StatefulWidget { imageUrl: data.imageUrl, onLike: onLike, onTap: onTap, + isLiked: isLiked, + id: data.id, ); - @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(16), - constraints: const BoxConstraints(minHeight: 140), + constraints: const BoxConstraints(minHeight: 160), decoration: BoxDecoration( color: Colors.white70, borderRadius: BorderRadius.circular(20), - border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(.5), + spreadRadius: 4, + offset: const Offset(0, 5), + blurRadius: 8, + ), + ], ), child: IntrinsicHeight( child: Row( @@ -63,11 +70,17 @@ class _CardState extends State<_Card> { ), child: SizedBox( height: double.infinity, - width: 160, - child: Image.network( - widget.imageUrl ?? '', - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => const Placeholder(), + width: 120, + child: Stack( + children: [ + Positioned.fill( + child: Image.network( + imageUrl ?? '', + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => const Placeholder(), + ), + ), + ], ), ), ), @@ -78,13 +91,13 @@ class _CardState extends State<_Card> { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - widget.text, - style: Theme.of(context).textTheme.headlineLarge, + text, + style: Theme.of(context).textTheme.headlineSmall, ), Text( - widget.descriptionText, + descriptionText, style: Theme.of(context).textTheme.bodyLarge, - ), + ) ], ), ), @@ -92,30 +105,22 @@ class _CardState extends State<_Card> { Align( alignment: Alignment.bottomRight, child: Padding( - padding: const EdgeInsets.only( - left: 8.0, - right: 16.0, - bottom: 16.0, - ), + padding: + const EdgeInsets.only(left: 8, right: 16, bottom: 16), child: GestureDetector( - onTap: () { - setState(() { - isLiked = !isLiked; - }); - widget.onLike?.call(widget.text, isLiked); - }, + onTap: () => onLike?.call(id, text, isLiked), child: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), + duration: const Duration(milliseconds: 200), 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 af69d51..ee28822 100644 --- a/lib/presentation/home_page/home_page.dart +++ b/lib/presentation/home_page/home_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pmu/components/extensions/context_x.dart'; import 'package:pmu/components/utils/debounce.dart'; import 'package:pmu/data/repositories/potter_repository.dart'; import 'package:pmu/domain/models/card.dart'; @@ -9,8 +10,13 @@ import 'package:pmu/presentation/details_page/details_page.dart'; import 'package:pmu/presentation/home_page/bloc/bloc.dart'; import 'package:pmu/presentation/home_page/bloc/events.dart'; import 'package:pmu/presentation/home_page/bloc/state.dart'; - import '../common/svg_objects.dart'; +import '../like_bloc/like_bloc.dart'; +import '../like_bloc/like_event.dart'; +import '../like_bloc/like_state.dart'; +import '../locale_bloc/locale_bloc.dart'; +import '../locale_bloc/locale_events.dart'; +import '../locale_bloc/locale_state.dart'; part 'card.dart'; @@ -43,10 +49,13 @@ class BodyState extends State { @override void initState() { - WidgetsBinding.instance.addPostFrameCallback((_) { - context.read().add(const HomeLoadDataEvent()); - }); SvgObjects.init(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().add(const HomeLoadDataEvent()); + context.read().add(const LoadLikesEvent()); + }); + scrollController.addListener(_onNextPageListener); super.initState(); @@ -77,44 +86,73 @@ class BodyState extends State { padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), child: Column( children: [ - Padding( - padding: const EdgeInsets.all(12), - child: CupertinoSearchTextField( - controller: searchController, - onChanged: (search) { - Debounce.run(() => context.read().add(HomeLoadDataEvent(search: search))); - }, - ), + Row( + children: [ + Expanded( + flex: 4, + child: Padding( + padding: const EdgeInsets.all(12), + 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(right: 12), + child: BlocBuilder( + builder: (context, state) { + return state.currentLocale.languageCode == 'ru' + ? const SvgRu() + : const SvgUk(); + }, + ), + ), + ), + ), + ], ), 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( - 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, - onLike: (title, isLiked) => - _showSnackBar(context, title, isLiked), - onTap: () => _navToDetails(context, data), - ) - : const SizedBox.shrink(); - }, - ), - ), - ), + ? const CircularProgressIndicator() + : BlocBuilder( + builder: (context, likeState) { + return 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, + onLike: _onLike, + isLiked: likeState.likedIds?.contains(data.id) == true, + onTap: () => _navToDetails(context, data), + ) + : const SizedBox.shrink(); + }, + ), + ), + ); + }, + ), ), BlocBuilder( builder: (context, state) => state.isPaginationLoading @@ -142,7 +180,7 @@ class BodyState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( - '$title ${isLiked ? 'liked!' : 'disliked :('}', + '$title ${isLiked ? context.locale.liked : context.locale.disliked}', style: Theme.of(context).textTheme.bodyLarge, ), backgroundColor: Colors.orangeAccent, @@ -150,4 +188,11 @@ class BodyState extends State { )); }); } + + void _onLike(String? id, String title, bool isLiked) { + if (id != null) { + context.read().add(ChangeLikeEvent(id)); + _showSnackBar(context, title, !isLiked); + } + } } diff --git a/makefile b/makefile index 343fd82..cb6377b 100644 --- a/makefile +++ b/makefile @@ -20,5 +20,4 @@ res: make format loc: - flutter gen-l10n; \ - make format \ No newline at end of file + flutter gen-l10n \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index e68e924..1d9ca8d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -254,6 +254,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" file: dependency: transitive description: @@ -317,6 +325,11 @@ packages: 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: @@ -525,6 +538,30 @@ packages: 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: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" petitparser: dependency: transitive description: @@ -533,6 +570,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + 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: @@ -573,6 +626,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: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" shelf: dependency: transitive description: @@ -754,6 +863,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" xml: dependency: transitive description: @@ -772,4 +889,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.5.3 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 36afd9e..0c4fdcf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,6 +27,8 @@ dependencies: intl: ^0.19.0 + shared_preferences: 2.2.3 + dev_dependencies: flutter_test: