From c82d240d4c36b4fd99e3b42090abafa28e598b36 Mon Sep 17 00:00:00 2001 From: antoc0der <1@DESKTOP-K1L8ND3> Date: Tue, 15 Oct 2024 22:14:24 +0400 Subject: [PATCH] =?UTF-8?q?lab7=20=D0=B2=D0=B0=D0=BC=20=D1=87=D0=B0=D0=B9?= =?UTF-8?q?=20=D1=81=20=D1=81=D0=B0=D1=85=D0=B0=D1=80=D0=BE=D0=BC=3F=20?= =?UTF-8?q?=D0=B8=D0=BB=D0=B8=20=D1=81=20=D0=BC=D0=BE=D0=B8=D0=BC=D0=B8=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B5=D0=B7=D0=B0=D0=BC=D0=B8=3F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- l10n.yaml | 6 + l10n/app_en.arb | 7 + l10n/app_ru.arb | 7 + lib/components/extensions/context_x.dart | 6 + lib/components/utils/debounce.dart | 8 +- lib/data/dtos/news_dto.dart | 9 +- lib/data/mappers/news_mapper.dart | 19 +- lib/data/repositories/bbc_repository.dart | 34 ++- lib/data/repositories/mock_repository.dart | 3 +- lib/domain/models/card.dart | 3 + lib/domain/models/home.dart | 6 +- lib/main.dart | 58 ++++-- lib/presentation/common/svg_objects.dart | 34 +++ .../details_page/details_page.dart | 20 +- lib/presentation/dialogs/error_dialog.dart | 30 +-- lib/presentation/dialogs/show_dialog.dart | 8 +- lib/presentation/home_page/bloc/bloc.dart | 3 +- lib/presentation/home_page/bloc/events.dart | 7 +- lib/presentation/home_page/card.dart | 47 ++--- lib/presentation/home_page/home_page.dart | 113 +++++++--- lib/presentation/likes_bloc/likes_bloc.dart | 28 +++ lib/presentation/likes_bloc/likes_events.dart | 10 + lib/presentation/likes_bloc/likes_state.dart | 12 ++ lib/presentation/locale_bloc/locale_bloc.dart | 17 ++ .../locale_bloc/locale_events.dart | 6 + .../locale_bloc/locale_state.dart | 11 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + makefile | 9 +- pubspec.lock | 196 +++++++++++++++++- pubspec.yaml | 12 +- 30 files changed, 572 insertions(+), 159 deletions(-) create mode 100644 l10n.yaml create mode 100644 l10n/app_en.arb create mode 100644 l10n/app_ru.arb create mode 100644 lib/components/extensions/context_x.dart create mode 100644 lib/presentation/common/svg_objects.dart create mode 100644 lib/presentation/likes_bloc/likes_bloc.dart create mode 100644 lib/presentation/likes_bloc/likes_events.dart create mode 100644 lib/presentation/likes_bloc/likes_state.dart create mode 100644 lib/presentation/locale_bloc/locale_bloc.dart create mode 100644 lib/presentation/locale_bloc/locale_events.dart create mode 100644 lib/presentation/locale_bloc/locale_state.dart diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..d26d702 --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,6 @@ +arb-dir: l10n +template-arb-file: app_ru.arb +output-localization-file: app_locale.dart +output-dir: lib/components/locale/l10n +output-class: AppLocale +synthetic-package: false \ No newline at end of file diff --git a/l10n/app_en.arb b/l10n/app_en.arb new file mode 100644 index 0000000..b26d33d --- /dev/null +++ b/l10n/app_en.arb @@ -0,0 +1,7 @@ +{ + "@@locale": "en", + "search": "Search", + "liked": "liked!", + "disliked": "disliked :(", + "arbEnding": "Чтобы не забыть про отсутствие запятой :)" +} \ No newline at end of file diff --git a/l10n/app_ru.arb b/l10n/app_ru.arb new file mode 100644 index 0000000..b119483 --- /dev/null +++ b/l10n/app_ru.arb @@ -0,0 +1,7 @@ +{ + "@@locale": "ru", + "search": "Поиск", + "liked": "понравился!", + "disliked": "разонравился :(", + "arbEnding": "Чтобы не забыть про отсутствие запятой :)" +} \ 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..1b3a046 --- /dev/null +++ b/lib/components/extensions/context_x.dart @@ -0,0 +1,6 @@ +import 'package:flutter/cupertino.dart'; +import 'package:pmu/components/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/components/utils/debounce.dart b/lib/components/utils/debounce.dart index 5ee58f8..4a85e47 100644 --- a/lib/components/utils/debounce.dart +++ b/lib/components/utils/debounce.dart @@ -1,16 +1,16 @@ import 'dart:async'; import 'dart:ui'; -class Debounce{ +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), + 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/news_dto.dart b/lib/data/dtos/news_dto.dart index 205a076..856c5d0 100644 --- a/lib/data/dtos/news_dto.dart +++ b/lib/data/dtos/news_dto.dart @@ -13,8 +13,7 @@ class NewsDto { this.meta, }); - factory NewsDto.fromJson(Map json) => - _$NewsDtoFromJson(json); + factory NewsDto.fromJson(Map json) => _$NewsDtoFromJson(json); } @JsonSerializable(createToJson: false) @@ -36,8 +35,7 @@ class MetaDto { const MetaDto({this.pagination}); - factory MetaDto.fromJson(Map json) => - _$MetaDtoFromJson(json); + factory MetaDto.fromJson(Map json) => _$MetaDtoFromJson(json); } @JsonSerializable(createToJson: false) @@ -48,6 +46,5 @@ class PaginationDto { const PaginationDto({this.current, this.next, this.last}); - factory PaginationDto.fromJson(Map json) => - _$PaginationDtoFromJson(json); + factory PaginationDto.fromJson(Map json) => _$PaginationDtoFromJson(json); } diff --git a/lib/data/mappers/news_mapper.dart b/lib/data/mappers/news_mapper.dart index 395dc09..5c601d1 100644 --- a/lib/data/mappers/news_mapper.dart +++ b/lib/data/mappers/news_mapper.dart @@ -2,17 +2,18 @@ import 'package:pmu/data/dtos/news_dto.dart'; import 'package:pmu/domain/models/card.dart'; import 'package:pmu/domain/models/home.dart'; -extension NewDataDtoToModel on NewAttributesDataDto{ +extension NewDataDtoToModel on NewAttributesDataDto { CardData toDomain() => CardData( - text: title ?? 'UNKNOWN', - imageUrl: imagelink, - descText: description ?? 'NOTHING', - ); + text: title ?? 'UNKNOWN', + imageUrl: imagelink, + descText: description ?? 'NOTHING', + id: id, + ); } extension NewsDtoToModel on NewsDto { HomeData toDomain() => HomeData( - data: data?.map((e) => e.toDomain()).toList(), - nextPage: meta?.pagination?.next, - ); -} \ No newline at end of file + data: data?.map((e) => e.toDomain()).toList(), + nextPage: meta?.pagination?.next, + ); +} diff --git a/lib/data/repositories/bbc_repository.dart b/lib/data/repositories/bbc_repository.dart index ed6c349..ce639de 100644 --- a/lib/data/repositories/bbc_repository.dart +++ b/lib/data/repositories/bbc_repository.dart @@ -22,26 +22,24 @@ class BbcRepository extends ApiInterface { int pageSize = 10, }) async { try { - // final String url = '$_baseUrl/everything?q=$q&page=$page&pageSize=$pageSize$_apiKey'; + // final String url = '$_baseUrl/everything?q=$q&page=$page&pageSize=$pageSize$_apiKey'; final String url = '$_baseUrl/everything'; - final Response response = await _dio.get>( - url, - queryParameters: ( (q != null && !q.isEmpty) ? { - 'q': q, - 'page': page, - 'pageSize': pageSize, - 'apikey': 'b9848c2aa43e4a0ba12dfe925db8513c' - } : { - 'q': 'news', - 'page': page, - 'pageSize': pageSize, - 'apikey': 'b9848c2aa43e4a0ba12dfe925db8513c' - } - ) - ); - final NewsDto dto = - NewsDto.fromJson(response.data as Map); + final Response response = await _dio.get>(url, + queryParameters: ((q != null && !q.isEmpty) + ? { + 'q': q, + 'page': page, + 'pageSize': pageSize, + 'apikey': 'b9848c2aa43e4a0ba12dfe925db8513c' + } + : { + 'q': 'news', + 'page': page, + 'pageSize': pageSize, + 'apikey': 'b9848c2aa43e4a0ba12dfe925db8513c' + })); + final NewsDto dto = NewsDto.fromJson(response.data as Map); final HomeData data = dto.toDomain(); return data; } on DioException catch (e) { diff --git a/lib/data/repositories/mock_repository.dart b/lib/data/repositories/mock_repository.dart index d299efa..a2df9b3 100644 --- a/lib/data/repositories/mock_repository.dart +++ b/lib/data/repositories/mock_repository.dart @@ -34,8 +34,7 @@ class MockRepository extends ApiInterface { descText: '"Запущена официальная процедура смены фамилии", — прокомментировали РИА Новости в пресс-службе объединенной компании Wildberries и Russ (РВБ).', icon: Icons.add_call, - imageUrl: - 'https://i.pinimg.com/736x/df/91/dc/df91dc3de2580cffc66d01000c0c6d82.jpg', + imageUrl: 'https://i.pinimg.com/736x/df/91/dc/df91dc3de2580cffc66d01000c0c6d82.jpg', ), CardData( text: 'В этом мире?', diff --git a/lib/domain/models/card.dart b/lib/domain/models/card.dart index b37db41..49a6e79 100644 --- a/lib/domain/models/card.dart +++ b/lib/domain/models/card.dart @@ -5,11 +5,14 @@ class CardData { final IconData icon; final String descText; final String? imageUrl; + final String? id; + CardData({ required this.text, this.icon = Icons.accessibility_new, required this.descText, this.imageUrl, + this.id, }); } diff --git a/lib/domain/models/home.dart b/lib/domain/models/home.dart index f8668fa..54bcd58 100644 --- a/lib/domain/models/home.dart +++ b/lib/domain/models/home.dart @@ -1,8 +1,8 @@ import 'package:pmu/domain/models/card.dart'; -class HomeData{ +class HomeData { final List? data; final int? nextPage; - HomeData({this.data,this.nextPage}); -} \ No newline at end of file + HomeData({this.data, this.nextPage}); +} diff --git a/lib/main.dart b/lib/main.dart index 011823a..116bb7c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,14 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pmu/components/locale/l10n/app_locale.dart'; import 'package:pmu/data/repositories/bbc_repository.dart'; import 'package:pmu/presentation/home_page/bloc/bloc.dart'; import 'package:pmu/presentation/home_page/home_page.dart'; - +import 'package:pmu/presentation/likes_bloc/likes_bloc.dart'; +import 'package:pmu/presentation/locale_bloc/locale_bloc.dart'; +import 'package:pmu/presentation/locale_bloc/locale_state.dart'; void main() { runApp(const MyApp()); @@ -14,26 +19,39 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.lightBlueAccent), - useMaterial3: true, - ), - home: RepositoryProvider(// чтобы ниже по дереву(в блок провайдере) был доступ к нашему репозиторию - lazy: true, - create: (_) => BbcRepository(), - child: BlocProvider(// обернули чтобы сделать блок доступным внутри нашей страницы - lazy: false, - create: (context) => HomeBloc(context.read()), - child: const MyHomePage()), + 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, + theme: ThemeData( + colorScheme: + ColorScheme.fromSeed(seedColor: Colors.lightBlueAccent), + useMaterial3: true, + ), + home: RepositoryProvider( + // чтобы ниже по дереву(в блок провайдере) был доступ к нашему репозиторию + lazy: true, + create: (_) => BbcRepository(), + child: BlocProvider( + // обернули чтобы сделать блок доступным внутри нашей страницы + lazy: false, + create: (context) => LikeBloc(), + child: BlocProvider( + lazy: false, + create: (context) => HomeBloc(context.read()), + child: const MyHomePage(), + ), + ), + ), + ); + }, ), ); } } - - - - - - diff --git a/lib/presentation/common/svg_objects.dart b/lib/presentation/common/svg_objects.dart new file mode 100644 index 0000000..a7c0257 --- /dev/null +++ b/lib/presentation/common/svg_objects.dart @@ -0,0 +1,34 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:pmu/components/resources/resources.g.dart'; + +abstract class SvgObjects{ + static void init(){ + final pics = [ + R.ASSETS_SVG_RU_SVG, + R.ASSETS_SVG_UK_SVG + ]; + for (final String p in pics){ + final loader = SvgAssetLoader(p); + svg.cache.putIfAbsent(loader.cacheKey(null), () => loader.loadBytes(null)); + } + } +} + +class SvgRu extends StatelessWidget{ + const SvgRu({super.key}); + + @override + Widget build(BuildContext context){ + return SvgPicture.asset(R.ASSETS_SVG_RU_SVG); + } +} + +class SvgUk extends StatelessWidget{ + const SvgUk({super.key}); + + @override + Widget build(BuildContext context){ + return SvgPicture.asset(R.ASSETS_SVG_UK_SVG); + } +} \ No newline at end of file diff --git a/lib/presentation/details_page/details_page.dart b/lib/presentation/details_page/details_page.dart index 539b18f..ddc8dd4 100644 --- a/lib/presentation/details_page/details_page.dart +++ b/lib/presentation/details_page/details_page.dart @@ -9,26 +9,21 @@ class DetailsPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - backgroundColor: Colors.lightBlue, foregroundColor: Colors.white), + appBar: AppBar(backgroundColor: Colors.lightBlue, foregroundColor: Colors.white), body: SingleChildScrollView( - child:Column( + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.only( - bottom: 4.0, top: 8.0, left: 8.0, right: 8.0), + padding: const EdgeInsets.only(bottom: 4.0, top: 8.0, left: 8.0, right: 8.0), child: Text( data.text, - style: const TextStyle( - color: Colors.black, - fontWeight: FontWeight.w500, - fontSize: 26), + style: + const TextStyle(color: Colors.black, fontWeight: FontWeight.w500, fontSize: 26), ), ), Padding( - padding: const EdgeInsets.only( - bottom: 16.0, top: 8.0, left: 8.0, right: 8.0), + padding: const EdgeInsets.only(bottom: 16.0, top: 8.0, left: 8.0, right: 8.0), child: SizedBox( width: double.infinity, child: ClipRRect( @@ -39,8 +34,7 @@ class DetailsPage extends StatelessWidget { )), )), Padding( - padding: const EdgeInsets.only( - bottom: 4.0, top: 8.0, left: 8.0, right: 8.0), + padding: const EdgeInsets.only(bottom: 4.0, top: 8.0, left: 8.0, right: 8.0), child: Text( data.descText, style: Theme.of(context).textTheme.bodyMedium, diff --git a/lib/presentation/dialogs/error_dialog.dart b/lib/presentation/dialogs/error_dialog.dart index cea54d7..8de67d2 100644 --- a/lib/presentation/dialogs/error_dialog.dart +++ b/lib/presentation/dialogs/error_dialog.dart @@ -14,22 +14,22 @@ class ErrorDialog extends StatelessWidget { margin: const EdgeInsets.all(36), padding: const EdgeInsets.all(20), decoration: const BoxDecoration( - color: Colors.grey, - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.error, color: Colors.white), - const SizedBox(height: 12), - Text( - error ?? 'UNKNOWN', - style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.white), - ), - ], + color: Colors.grey, + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.error, color: Colors.white), + const SizedBox(height: 12), + Text( + error ?? 'UNKNOWN', + style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.white), + ), + ], + ), ), ), - ), ); } -} \ No newline at end of file +} diff --git a/lib/presentation/dialogs/show_dialog.dart b/lib/presentation/dialogs/show_dialog.dart index 5cc4783..b80aebb 100644 --- a/lib/presentation/dialogs/show_dialog.dart +++ b/lib/presentation/dialogs/show_dialog.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import 'package:pmu/presentation/dialogs/error_dialog.dart'; void showErrorDialog( - BuildContext context, { - required String? error, - }) { + BuildContext context, { + required String? error, +}) { showDialog( context: context, builder: (_) => ErrorDialog(error), ); -} \ No newline at end of file +} diff --git a/lib/presentation/home_page/bloc/bloc.dart b/lib/presentation/home_page/bloc/bloc.dart index 9640219..175586e 100644 --- a/lib/presentation/home_page/bloc/bloc.dart +++ b/lib/presentation/home_page/bloc/bloc.dart @@ -11,8 +11,7 @@ class HomeBloc extends Bloc { } // мы должны изменить наше состояние, передаем внутрь новое состояние - Future _onLoadData( - HomeLoadDataEvent event, Emitter emit) async { + Future _onLoadData(HomeLoadDataEvent event, Emitter emit) async { if (event.nextPage == null) { emit(state.copyWith(isLoading: true)); } else { diff --git a/lib/presentation/home_page/bloc/events.dart b/lib/presentation/home_page/bloc/events.dart index b6103e9..c34b911 100644 --- a/lib/presentation/home_page/bloc/events.dart +++ b/lib/presentation/home_page/bloc/events.dart @@ -1,8 +1,9 @@ -abstract class HomeEvent{ +abstract class HomeEvent { const HomeEvent(); } -class HomeLoadDataEvent extends HomeEvent{ + +class HomeLoadDataEvent extends HomeEvent { final String? search; final int? nextPage; const HomeLoadDataEvent({this.search, this.nextPage}); -} \ No newline at end of file +} diff --git a/lib/presentation/home_page/card.dart b/lib/presentation/home_page/card.dart index 8b25fb8..58d10bd 100644 --- a/lib/presentation/home_page/card.dart +++ b/lib/presentation/home_page/card.dart @@ -1,26 +1,33 @@ 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 IconData icon; final String descText; final String? imageUrl; final OnLikeCallBack onLike; final VoidCallback? onTap; + final String? id; + final bool isLiked; - const _Card(this.text, - {this.icon = Icons.ac_unit_sharp, - required this.descText, - this.imageUrl, - this.onLike, - this.onTap}); + const _Card( + this.text, { + this.icon = Icons.ac_unit_sharp, + required this.descText, + this.imageUrl, + this.onLike, + this.onTap, + this.id, + this.isLiked = false, + }); factory _Card.fromData( CardData data, { OnLikeCallBack onLike, VoidCallback? onTap, + bool isLiked = false, }) => _Card( data.text, @@ -29,21 +36,16 @@ 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 Padding( padding: const EdgeInsets.only(left: 8.0, right: 8.0), child: GestureDetector( - onTap: widget.onTap, + onTap: onTap, child: SizedBox( height: 150, child: Container( @@ -68,7 +70,7 @@ class _CardState extends State<_Card> { height: double.infinity, width: 120, child: Image.network( - widget.imageUrl ?? '', + imageUrl ?? '', fit: BoxFit.cover, errorBuilder: (_, __, ___) => const Placeholder(), ), @@ -82,7 +84,7 @@ class _CardState extends State<_Card> { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - widget.text, + text, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( @@ -91,7 +93,7 @@ class _CardState extends State<_Card> { fontSize: 26), ), Text( - widget.descText, + descText, maxLines: 5, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall, @@ -106,12 +108,7 @@ class _CardState extends State<_Card> { padding: const EdgeInsets.only( left: 8.0, top: 4.0, right: 8.0, bottom: 4.0), 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), child: isLiked diff --git a/lib/presentation/home_page/home_page.dart b/lib/presentation/home_page/home_page.dart index 7722dc5..ff9c3b4 100644 --- a/lib/presentation/home_page/home_page.dart +++ b/lib/presentation/home_page/home_page.dart @@ -1,15 +1,23 @@ 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/bbc_repository.dart'; import 'package:pmu/data/repositories/mock_repository.dart'; import 'package:pmu/domain/models/card.dart'; +import 'package:pmu/presentation/common/svg_objects.dart'; import 'package:pmu/presentation/details_page/details_page.dart'; import 'package:pmu/presentation/dialogs/show_dialog.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 'package:pmu/presentation/likes_bloc/likes_bloc.dart'; +import 'package:pmu/presentation/likes_bloc/likes_events.dart'; +import 'package:pmu/presentation/likes_bloc/likes_state.dart'; +import 'package:pmu/presentation/locale_bloc/locale_bloc.dart'; +import 'package:pmu/presentation/locale_bloc/locale_events.dart'; +import 'package:pmu/presentation/locale_bloc/locale_state.dart'; part 'card.dart'; @@ -47,8 +55,10 @@ class _BodyState extends State<_Body> { // final repo = BbcRepository(); @override void initState() { + SvgObjects.init(); WidgetsBinding.instance.addPostFrameCallback((_) { context.read().add(const HomeLoadDataEvent()); + context.read().add(const LoadLikesEvent()); }); //data = repo.loadData(onError: (e) => showErrorDialog(context, error: e)); scrollController.addListener(_onNextPageListener); @@ -63,7 +73,6 @@ class _BodyState extends State<_Body> { bloc.add(HomeLoadDataEvent( search: searchController.text, nextPage: 2, - )); } } @@ -81,16 +90,41 @@ class _BodyState extends State<_Body> { return Padding( padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), child: Column(children: [ - Padding( - padding: const EdgeInsets.all(12.0), - 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 @@ -103,26 +137,33 @@ class _BodyState extends State<_Body> { ) : 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(); - }, - ), - ), + : 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( @@ -138,7 +179,7 @@ class _BodyState extends State<_Body> { WidgetsBinding.instance.addPostFrameCallback((_) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( - 'The New $title ${isLiked ? 'liked!' : 'disliked'}', + 'The New ${isLiked ? context.locale.liked : context.locale.disliked}', style: Theme.of(context).textTheme.bodyLarge, ), backgroundColor: Colors.lightBlue, @@ -154,6 +195,14 @@ class _BodyState extends State<_Body> { ); } + void _onLike(String? id, String title, bool isLiked) { + if (id != null) { + context.read().add(ChangeLikeEvent(id)); + _showSnackBar(context, title, !isLiked); + } + } + + Future _onRefresh() { context .read() diff --git a/lib/presentation/likes_bloc/likes_bloc.dart b/lib/presentation/likes_bloc/likes_bloc.dart new file mode 100644 index 0000000..46c0282 --- /dev/null +++ b/lib/presentation/likes_bloc/likes_bloc.dart @@ -0,0 +1,28 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pmu/presentation/likes_bloc/likes_events.dart'; +import 'package:pmu/presentation/likes_bloc/likes_state.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +const String _likedPrefsKey = 'liked'; +class LikeBloc extends Bloc { + LikeBloc() : super(const LikeState(likedIds: [])) { + on(_onChangeLike); + on(_onLoadLikes); + } + Future _onLoadLikes(LoadLikesEvent event, Emitter emit) async { + final prefs = await SharedPreferences.getInstance(); + final data = prefs.getStringList(_likedPrefsKey); + emit(state.copyWith(likedIds: data)); + } + Future _onChangeLike(ChangeLikeEvent event, Emitter emit) async { + final updatedList = List.from(state.likedIds ?? []); + if (updatedList.contains(event.id)) { + updatedList.remove(event.id); + } else { + updatedList.add(event.id); + } + final prefs = await SharedPreferences.getInstance(); + prefs.setStringList(_likedPrefsKey, updatedList); + emit(state.copyWith(likedIds: updatedList)); + } +} \ No newline at end of file diff --git a/lib/presentation/likes_bloc/likes_events.dart b/lib/presentation/likes_bloc/likes_events.dart new file mode 100644 index 0000000..1b589f3 --- /dev/null +++ b/lib/presentation/likes_bloc/likes_events.dart @@ -0,0 +1,10 @@ +abstract class LikeEvent { + const LikeEvent(); +} +class LoadLikesEvent extends LikeEvent { + const LoadLikesEvent(); +} +class ChangeLikeEvent extends LikeEvent { + final String id; + const ChangeLikeEvent(this.id); +} \ No newline at end of file diff --git a/lib/presentation/likes_bloc/likes_state.dart b/lib/presentation/likes_bloc/likes_state.dart new file mode 100644 index 0000000..9785caa --- /dev/null +++ b/lib/presentation/likes_bloc/likes_state.dart @@ -0,0 +1,12 @@ +import 'package:equatable/equatable.dart'; +import 'package:copy_with_extension/copy_with_extension.dart'; + +part 'likes_state.g.dart'; + +@CopyWith() +class LikeState extends Equatable { + final List? likedIds; + const LikeState({required this.likedIds}); + @override + List get props => [likedIds]; +} \ No newline at end of file diff --git a/lib/presentation/locale_bloc/locale_bloc.dart b/lib/presentation/locale_bloc/locale_bloc.dart new file mode 100644 index 0000000..3fd0092 --- /dev/null +++ b/lib/presentation/locale_bloc/locale_bloc.dart @@ -0,0 +1,17 @@ +import 'dart:ui'; + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pmu/components/locale/l10n/app_locale.dart'; +import 'package:pmu/presentation/locale_bloc/locale_events.dart'; +import 'package:pmu/presentation/locale_bloc/locale_state.dart'; + +class LocaleBloc extends Bloc { + LocaleBloc(Locale defaultLocale) : super(LocaleState(currentLocale: defaultLocale)) { + on(_onChangeLocale); + } + Future _onChangeLocale(ChangeLocaleEvent event, Emitter emit) async { + final toChange = AppLocale.supportedLocales + .firstWhere((e) => e.languageCode != state.currentLocale.languageCode); + emit(state.copyWith(currentLocale: toChange)); + } +} \ No newline at end of file diff --git a/lib/presentation/locale_bloc/locale_events.dart b/lib/presentation/locale_bloc/locale_events.dart new file mode 100644 index 0000000..8cd48b4 --- /dev/null +++ b/lib/presentation/locale_bloc/locale_events.dart @@ -0,0 +1,6 @@ +abstract class LocaleEvent { + const LocaleEvent(); +} +class ChangeLocaleEvent extends LocaleEvent { + const ChangeLocaleEvent(); +} \ No newline at end of file diff --git a/lib/presentation/locale_bloc/locale_state.dart b/lib/presentation/locale_bloc/locale_state.dart new file mode 100644 index 0000000..18211c4 --- /dev/null +++ b/lib/presentation/locale_bloc/locale_state.dart @@ -0,0 +1,11 @@ +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +part 'locale_state.g.dart'; +@CopyWith() +class LocaleState extends Equatable { + final Locale currentLocale; + const LocaleState({required this.currentLocale}); + @override + List get props => [currentLocale]; +} \ No newline at end of file diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..724bb2a 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/makefile b/makefile index fe8c57c..32e1127 100644 --- a/makefile +++ b/makefile @@ -3,4 +3,11 @@ gen: icon: flutter pub run flutter_launcher_icons:main init_res: - dart pub global activate flutter_asset_generator \ No newline at end of file + dart pub global activate flutter_asset_generator +format: + dart format . --line-length 100 +res: + fgen --output lib/components/resources/resources.g.dart --no-watch --no-preview +loc: + flutter gen-l10n; \ + make format \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 71a4808..87d73b5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -238,6 +238,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: @@ -275,11 +283,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: @@ -304,6 +330,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + http: + dependency: transitive + description: + name: http + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + url: "https://pub.dev" + source: hosted + version: "1.2.2" http_multi_server: dependency: transitive description: @@ -320,6 +354,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" io: dependency: transitive description: @@ -456,6 +498,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + 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: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + 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: @@ -496,6 +594,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: @@ -605,6 +759,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: @@ -653,6 +831,22 @@ 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: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" yaml: dependency: transitive description: @@ -663,4 +857,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.5.2 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index c1211a7..2071284 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,6 +34,7 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. + flutter_svg: 2.0.7 cupertino_icons: ^1.0.8 json_annotation: ^4.8.1 dio: ^5.4.2+1 @@ -42,6 +43,13 @@ dependencies: flutter_bloc: ^8.1.5 copy_with_extension_gen: ^5.0.4 + flutter_localizations: + sdk: flutter + intl: 0.19.0 + + shared_preferences: 2.2.3 + + dev_dependencies: flutter_test: sdk: flutter @@ -66,7 +74,7 @@ flutter_icons: mid_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. @@ -102,3 +110,5 @@ flutter: # # For details regarding fonts from package dependencies, # see https://flutter.dev/to/font-from-package + assets: + - assets/svg/